BillKit/Docs Console
Core concepts

Customers

A customer is the party you bill. The record is deliberately thin, because the payment credentials that matter live at Mollie, not here.

Updated Sep 13, 2026 · API version 2026-09-13

The object

FieldNotes
idstringcus_…
emailstring | nullOptional. Where invoices, receipts, trial reminders and dunning notices go. A customer without one gets no email from BillKit.
namestring | nullUp to 255 characters. Shown on invoices and in the portal.
country_codestring | nullISO 3166-1 alpha-2. This is what selects the VAT rate and filters the payment methods offered at checkout. BillKit fills it in from the buyer’s own answer at checkout when you leave it empty, and never overwrites one you set. See Charge and record EU VAT.
vat_numberstring | nullSet through its own endpoint, not on create or update.
vat_number_validatedbooleanTrue only when VIES confirmed the number.
metadataobjectUp to 50 keys, keys up to 40 characters, values up to 500. Echoed on the object and on its events.
test_clockstring | nullTest mode only, set at create time. See Test mode.
livemodeboolean
createdintegerUnix seconds.

Every field is optional on create, including email. Update with POST /v1/customers/{id}, not PATCH. Request bodies reject unknown fields with a 422, so a typo like ammount_cents fails loudly instead of being ignored.

shell
curl https://api.billkit.eu/v1/customers \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "country_code": "NL",
  "metadata": {"workspace_id": "ws_4471"}
}'

Use metadata to carry your own identifier. It comes back on the customer and on every customer.* event, which saves you a lookup table on the receiving end.

BillKit never deduplicates by email. Two calls with the same address give you two customers. If you want one record per person, look them up yourself before creating, or keep your own id in metadata and key on that.

Payment methods are not on the customer

There is no default_payment_method field, and no endpoint to attach a card. A usable payment credential in BillKit is a Mollie mandate, and a mandate only exists because a real first payment went through Mollie. So the flow is always: checkout session, customer pays, mandate stored against the resulting subscription.

Customers who already have a mandate can manage their saved methods from the customer portal or the Payment Method Element, which read GET /v1/checkout/payment-methods. That surface is authenticated by the customer’s own short-lived token, not by your secret key.

VAT numbers

POST /v1/customers/{id}/vat_number takes vat_number and an optional country_code, and runs the number through VIES server-side. There is no way to mark a number valid by hand, which is deliberate: a manual override is a one-call way to silently stop charging VAT to a customer, and that is not something that should be possible without a trace.

shell
curl https://api.billkit.eu/v1/customers/cus_Ja7Ye2vMq9xN4TdWpr/vat_number \
-H "Authorization: Bearer $BILLKIT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"vat_number": "NL123456789B01", "country_code": "NL"}'

The shape is checked first: a two-letter country code followed by 2 to 12 letters or digits, with ., -, / and spaces allowed and ignored. A value that cannot be a registration number is a 400 and is never sent to VIES. Send the country code as part of the number, so NL123456789B01 and not 123456789B01, because VIES takes the country as its own field and answers a bare body with a flat rejection.

Three outcomes after that, all recorded in the audit log:

OutcomeWhat happens
validVIES recognised the number. vat_number_validated becomes true, and a cross-border EU customer is reverse-charged from then on.
invalidThe call returns 400 parameter_invalid with reason: "vat_not_recognised", and nothing is written, so the customer keeps the registration they had before.
unverifiableVIES was unreachable, the member state did not answer, or the country is outside the register it covers (a UK or Swiss number, say). The number is stored, VAT keeps being charged, and the next write re-checks.

A valid result is cached for 24 hours, so re-sending the same number and country is a no-op. Changing either forces a fresh check, and invalid or unverifiable always re-checks. Setting vat_number to null clears the number and the validation state.

Full behaviour, including how reverse charge affects the invoice, is in Charge and record EU VAT correctly.

Abandoned checkouts

A checkout that captures an email creates the customer before the payment, not after. So someone who opens your checkout, types their address and then closes the tab leaves a customer row behind. This is not a bug, and it is not something you can switch off: the customer has to exist before the charge for the payment to be attributable to anyone.

Those rows carry provisional: true. The flag clears the moment a payment settles, and never comes back.

GET /v1/customers returns both kinds by default. Filter to get one or the other:

# People who have actually paid you.
curl -s https://api.billkit.eu/v1/customers?provisional=false \
  -H "Authorization: Bearer $BILLKIT_API_KEY"

# Abandoned checkouts: the cart-recovery worklist.
curl -s https://api.billkit.eu/v1/customers?provisional=true \
  -H "Authorization: Bearer $BILLKIT_API_KEY"

The dashboard applies the same split: All shows customers, and Abandoned checkouts is its own tab.

Two things to know before you build on this. Provisional customers are deleted after a retention window (30 days by default), along with their unpaid checkout sessions, so treat them as a recovery window rather than a durable record. If you want them permanently, copy what you need into your own system. And BillKit never deduplicates by email, so one person who abandons three checkouts is three provisional rows; deduplicate on your side before emailing them.

Nothing is ever deleted once it converts, regardless of age.

Deleting versus purging

Two different operations, for two different reasons.

DELETE /v1/customers/{id} removes the customer from the API. GET /v1/customers/{id} returns 404, they stop appearing in GET /v1/customers, and customer.deleted fires. The response is {"id": "cus_...", "object": "customer", "deleted": true}, not the customer. Their payments, invoices and refunds are untouched, and so is their personal data, so this is not an erasure.

It is refused while the customer has a subscription that can still charge them, which is any subscription in active, trialing, past_due or incomplete. A trial counts, because it turns into a real charge on its own. The refusal is a 400 parameter_invalid with reason: "customer_has_active_subscription". Cancel the subscription first. Nothing is cancelled for you, because a delete call should never end someone’s service on its own. A customer who is still being billed but can no longer be seen is the worst of both.

POST /v1/customers/{id}/purge with {"confirmed": true} is the GDPR erasure. Email is overwritten with an unusable placeholder, name and metadata are cleared, the VAT registration (vat_number and its validation state) is removed, and purged_at is stamped. The row survives so invoices and payments keep their foreign keys, which is what tax retention law requires. The customer.deleted event carries a snapshot taken before redaction, so your own systems can still match it to their records.

The same call also redacts that customer from your event log. event.data is a snapshot of the whole object at the moment it was emitted, so without this their email and VAT number would stay readable through GET /v1/events for the rest of the event’s 90-day retention window. Both their own customer.* events and the buyer fields denormalised onto events that merely reference them — invoices, checkout sessions — are rewritten. The one exception is the customer.deleted event above, which keeps its pre-redaction snapshot on purpose so the erasure is something your systems can act on.

What purge does not touch is the issued accounting documents. Each invoice, credit note and payment holds its own copy of the buyer’s name, email, VAT number and country, taken at the moment it was issued, so that a document never changes after the fact. Those copies survive, because an invoice you have issued is a record you are required to keep for 7–10 years depending on member state — and on a reverse-charged intra-EU B2B supply the buyer’s VAT number is a required element of the invoice, so removing it would make the document defective rather than private. That is the legal-obligation ground in GDPR Art. 17(3)(b). Plan your erasure process around it rather than around the assumption that one call removes every trace.

Purge refuses on the same subscription statuses as delete, and additionally with 409 operation_pending while a payment operation is in flight for the customer. Cancel first. A paused subscription reads active, because the customer has paid for the period they are in, so it is refused too.

Purge is the one call that still reaches a customer you have already deleted. Everything else treats a soft-deleted customer as gone and answers 404, but delete-then-erase is the normal order to do those two things in, so an erasure request weeks after the deletion still works. Purging twice is a 404.

confirmed: true is required because DELETE and purge are one keystroke apart and only one of them is reversible.

How the buyer’s country gets set

country_code is the tenant’s to set, and BillKit only ever fills a blank. There are three ways it arrives, strongest first:

  1. You set it on POST /v1/customers, or the customer sets it in the billing portal. This is an answer, and nothing later overwrites it.
  2. The buyer answers it at checkout. When the customer carries no country, the embedded element shows a country select and will not let the payment through until it is answered. An unanswered country means no VAT is charged, and the tenant carries that.
  3. A suggestion prefills that select, resolved from the buyer’s IP address. It only fills the control in: the select stays visible, stays required, and what the buyer settles on is what is stored.

The third step never decides anything on its own, and that is deliberate. A stored country_code outranks anything the buyer asserts afterwards, so a country inferred without their confirmation would be one they could not correct at checkout.

The IP lookup runs against a local database inside BillKit, so the buyer’s address is never sent to a geolocation service. IP geolocation by DB-IP.