Skip to content
Hardcount
Run it locally
Pre-1.0 · one maintainer · Stripe and cards only · no SDKs (client libraries) yet · no production users yet. Docs describe v0.9.1 (29 Oct 2026) · What’s new →

Create or replace the customer's billing profile

PUT
/v1/customers/{id}/billing-profile
curl --request PUT \
--url http://localhost:8080/v1/customers/example/billing-profile \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "legal_name": "example", "phone": "example", "address_line1": "example", "address_line2": "example", "city": "example", "state": "example", "postal_code": "example", "country": "example", "currency": "example", "tax_status": "standard", "tax_exempt_reason": "example", "tax_id": "example", "tax_id_type": "example" }'

Writes the whole profile. Fields you leave out are stored empty, so send every field you want to keep. profile_status is worked out by the server: ready when legal name, address line 1, city and country are all set, otherwise incomplete.

Checks, each answered with 422 and param set to the field: country must be an ISO 3166 alpha-2 code; currency must be a supported ISO 4217 code (zero-decimal currencies such as JPY are refused); phone is 7 to 20 digits, spaces or +-(); tax_id must match tax_id_type when the type is a known one; tax_status: exempt needs tax_exempt_reason; tax_status: reverse_charge needs tax_id. Country and currency are stored in upper case.

A currency that differs from the plan currency of a subscription that still bills is refused with 409 invalid_state, because invoices would show plan prices in the new currency without conversion.

After saving, the profile is copied to the linked Stripe customer (best effort), and draft invoices whose tax failed on bad customer data are retried. The change is written to the audit log.

id
required
string
Idempotency-Key
string

Makes the call safe to retry. Send a unique value (a UUID works) and the same value on every retry. For 24 hours, a repeat with the same key, method, path and body gets the first response again, with an Idempotent-Replayed: true header, instead of running again. The error cases are in docs/api-surface.md. The usage routes do not take it: each usage event carries its own idempotency_key.

Media typeapplication/json

The billing details printed on invoices and used for tax. The whole profile is replaced on every write.

object
legal_name
string
phone

7 to 20 characters of digits, spaces, +, -, (, ).

string
address_line1
string
address_line2
string
city
string
state
string
postal_code
string
country

ISO 3166 alpha-2 code, such as US or IN.

string
currency

ISO 4217 code, such as USD. When set, invoices for this customer use it instead of the plan currency.

string
tax_status

Defaults to standard.

string
Allowed values: standard exempt reverse_charge
tax_exempt_reason

Required when tax_status is exempt.

string
tax_id

Buyer tax ID. Required when tax_status is reverse_charge.

string
tax_id_type

Tax ID kind, stored as its Stripe code (for example eu_vat, in_gst).

string

The stored profile.

Media typeapplication/json
object
legal_name
string
phone

7 to 20 characters of digits, spaces, +, -, (, ).

string
address_line1
string
address_line2
string
city
string
state
string
postal_code
string
country

ISO 3166 alpha-2 code, such as US or IN.

string
currency

ISO 4217 code, such as USD. When set, invoices for this customer use it instead of the plan currency.

string
tax_status
required

Defaults to standard.

string
Allowed values: standard exempt reverse_charge
tax_exempt_reason

Required when tax_status is exempt.

string
tax_id
required

Buyer tax ID. Required when tax_status is reverse_charge.

string
tax_id_type
required

Tax ID kind, stored as its Stripe code (for example eu_vat, in_gst).

string
customer_id
required
string
tenant_id
string
profile_status
required

Set by the server. ready when legal name, address line 1, city and country are all set.

string
Allowed values: incomplete ready
created_at
required
string format: date-time
updated_at
required
string format: date-time

Example

{
"tax_status": "standard",
"profile_status": "incomplete"
}

The body is not valid JSON.

Media typeapplication/json
object
error
object
type

One of invalid_request_error, authentication_error, rate_limit_error or api_error. Every error the API answers has this shape, including an unknown path (404, code route_not_found) and a method the path does not take (405, code method_not_allowed, with an Allow header). The few exceptions are listed in docs/api-surface.md.

string
code
string
message
string
request_id
string
param

The request field the error is about. Sent only when the error names one. A request body may hold only the fields its operation lists, spelled and cased as listed: any other field, or one the server sets (such as id), is refused with 422 and param naming it. An operation with no request body lists no fields: send no body, {} or null. The Stripe and Postmark webhooks and the LiteLLM callback take their sender’s body as it comes.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}

Customer not found.

Media typeapplication/json
object
error
object
type

One of invalid_request_error, authentication_error, rate_limit_error or api_error. Every error the API answers has this shape, including an unknown path (404, code route_not_found) and a method the path does not take (405, code method_not_allowed, with an Allow header). The few exceptions are listed in docs/api-surface.md.

string
code
string
message
string
request_id
string
param

The request field the error is about. Sent only when the error names one. A request body may hold only the fields its operation lists, spelled and cased as listed: any other field, or one the server sets (such as id), is refused with 422 and param naming it. An operation with no request body lists no fields: send no body, {} or null. The Stripe and Postmark webhooks and the LiteLLM callback take their sender’s body as it comes.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}

Currency conflicts with a billing subscription’s plan currency (invalid_state).

Media typeapplication/json
object
error
object
type

One of invalid_request_error, authentication_error, rate_limit_error or api_error. Every error the API answers has this shape, including an unknown path (404, code route_not_found) and a method the path does not take (405, code method_not_allowed, with an Allow header). The few exceptions are listed in docs/api-surface.md.

string
code
string
message
string
request_id
string
param

The request field the error is about. Sent only when the error names one. A request body may hold only the fields its operation lists, spelled and cased as listed: any other field, or one the server sets (such as id), is refused with 422 and param naming it. An operation with no request body lists no fields: send no body, {} or null. The Stripe and Postmark webhooks and the LiteLLM callback take their sender’s body as it comes.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}

A field failed validation, or the body has a key the profile does not have, such as tax_exempt (set tax_status instead), or one the server sets, such as customer_id or created_at. param names the field.

Media typeapplication/json
object
error
object
type

One of invalid_request_error, authentication_error, rate_limit_error or api_error. Every error the API answers has this shape, including an unknown path (404, code route_not_found) and a method the path does not take (405, code method_not_allowed, with an Allow header). The few exceptions are listed in docs/api-surface.md.

string
code
string
message
string
request_id
string
param

The request field the error is about. Sent only when the error names one. A request body may hold only the fields its operation lists, spelled and cased as listed: any other field, or one the server sets (such as id), is refused with 422 and param naming it. An operation with no request body lists no fields: send no body, {} or null. The Stripe and Postmark webhooks and the LiteLLM callback take their sender’s body as it comes.

string

Example

{
"error": {
"type": "invalid_request_error"
}
}