Skip to main content

Buyers

The Buyers API gives you first-class buyer records: create or update a profile, look it up by your own reference or by email, and read it back by id. Sessions, payment intents, and tokens all link to the same records, so a buyer you reference at checkout is the same buyer you manage here.

A buyer is your customer — the person paying.

Required key: secret key (vp_sk_*). Publishable keys are rejected with 403; browser-facing endpoints never return buyer identity.

Buyers are mode-scoped: a buyer created with a test key gets a vp_by_test_* id and is invisible to live keys (and vice-versa).

The buyer object​

Every /v1/buyers operation returns the full stored profile. Every key is always present; fields with no stored value are null.

{
"id": "vp_by_live_k7x9m2n4p3q5r6s8",
"external_id": "user_8f3a2b",
"email": "jane@example.com",
"name": "Jane Doe",
"phone": "+1 212 555 0100",
"company": "Acme Inc",
"address": {
"address_line1": "1 Market St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US"
},
"metadata": { "tier": "gold" },
"created_at": "2026-07-16T10:00:00Z",
"last_seen_at": "2026-07-16T10:05:00Z"
}
FieldTypeDescription
idstringBuyer id, mode-prefixed (vp_by_test_* / vp_by_live_*).
external_idstring (≤ 200) | nullYour own stable customer reference (1–200 chars), e.g. user_8f3a2b — the same value you pass as buyerId on session create / buyer_id on the discrete surfaces. The primary lookup key; wins over email when both are present. Not patchable after create. null for email-only buyers.
emailstring (≤ 254) | nullBuyer's email (valid email, max 254 chars). The fallback lookup key when external_id is absent. Unique per merchant across buyer records.
namestring (≤ 200) | nullBuyer's full name (1–200 chars). Stored, but never split into given and family parts and never sent to your payment provider. The name that reaches the processor is the separate first_name / last_name pair sent on the charge, which is not stored here — see Putting a name on the payment.
phonestring (≤ 32) | nullPhone number (3–32 chars) — digits with an optional leading + and common separators (spaces, dots, hyphens, parentheses). Deliberately not strict E.164; forward whatever your checkout collected.
companystring (≤ 200) | nullCompany name (1–200 chars).
addressobject | nullBuyer-level address on file — same shape as billing_address: address_line1 (1–100) + postal_code (1–16) + country (2-letter uppercase ISO 3166-1 alpha-2) required; address_line2 / city / state optional (each ≤ 100). Distinct from the charge-scoped billing_address on payment-intent create — this one persists on the buyer profile.
metadataobjectFree-form merchant metadata. PII keys are scrubbed before storage. Merges with append semantics — existing keys are preserved, new keys are added; a write never bulk-replaces the stored metadata, and keys are never removed. Serialized size capped at 8 KB on the /v1/buyers write surfaces. {} when never set.
created_atstringWhen the buyer record was first created (ISO 8601).
last_seen_atstringTouched on every write that references this buyer — direct /v1/buyers writes and buyer-linked session / payment-intent / token creates alike.

Create or update a buyer​

POST /v1/buyers creates or updates a buyer record (upsert). The write is keyed on external_id when present, else on email — at least one of the two identity fields is required (a profile with no lookup key could never be retrieved or charged against). When both are sent, external_id wins as the lookup key and the email is stored on the profile.

A buyer first seen with an email only picks up your reference. When you send an external_id no buyer has yet with an email that already belongs to a buyer without one (for example, a shopper who checked out as a guest with that email), the external_id is attached to that buyer and the call returns 200. No second buyer is created, and the buyer's saved cards take the same reference, so they list under GET /v1/payment_methods?buyer_id=. If that buyer already has a different external_id, the call returns 409 buyer_email_conflict and nothing changes. The same rule applies to the buyer on POST /v1/sessions, POST /v1/payment_intents and POST /v1/tokens, except that a conflict there never fails the request: it succeeds and the buyer is not linked.

curl https://checkout.vonpay.com/v1/buyers \
-H "Authorization: Bearer vp_sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"external_id": "user_8f3a2b",
"email": "jane@example.com",
"name": "Jane Doe",
"phone": "+1 212 555 0100",
"company": "Acme Inc",
"address": {
"address_line1": "1 Market St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US"
},
"metadata": { "tier": "gold" }
}'

Returns 201 when the call created the buyer and 200 when it updated an existing one; either way the body is the full stored profile, so a successful write is never silent — what you read back is what was persisted (e.g. metadata after the append-merge).

{
"id": "vp_by_live_k7x9m2n4p3q5r6s8",
"external_id": "user_8f3a2b",
"email": "jane@example.com",
"name": "Jane Doe",
"phone": "+1 212 555 0100",
"company": "Acme Inc",
"address": {
"address_line1": "1 Market St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US"
},
"metadata": { "tier": "gold" },
"created_at": "2026-07-16T10:00:00Z",
"last_seen_at": "2026-07-16T10:00:00Z"
}

Look up a buyer​

GET /v1/buyers is an exact lookup by ?external_id= or ?email=. At least one of the two query parameters is required — there is deliberately no unfiltered buyer listing in v1; a request with neither returns 400 validation_error. When both are supplied, external_id wins (the same identity-resolution order as the upsert write).

curl "https://checkout.vonpay.com/v1/buyers?external_id=user_8f3a2b" \
-H "Authorization: Bearer vp_sk_live_YOUR_KEY"

Returns { "data": [...] } with zero or one profiles — both lookup keys are unique per merchant, so a match is never ambiguous. An empty data array means no buyer matched; this surface does not 404 on a miss (the opaque 404 applies to the id-keyed read below).

{
"data": [
{
"id": "vp_by_live_k7x9m2n4p3q5r6s8",
"external_id": "user_8f3a2b",
"email": "jane@example.com",
"name": "Jane Doe",
"phone": "+1 212 555 0100",
"company": "Acme Inc",
"address": {
"address_line1": "1 Market St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US"
},
"metadata": { "tier": "gold" },
"created_at": "2026-07-16T10:00:00Z",
"last_seen_at": "2026-07-16T10:05:00Z"
}
]
}

Retrieve a buyer​

GET /v1/buyers/{id} returns one buyer profile by vp_by_* id.

curl https://checkout.vonpay.com/v1/buyers/vp_by_live_k7x9m2n4p3q5r6s8 \
-H "Authorization: Bearer vp_sk_live_YOUR_KEY"

The response is the buyer object. It returns a uniform 404 buyer_not_found when the id does not exist, belongs to a different merchant, or its test/live mode does not match the API key's mode — the same body in every case, so buyer ids cannot be probed across merchants or modes. To find a buyer by your own reference, use the lookup instead.

Update a buyer​

PATCH /v1/buyers/{id} is a sparse profile update — send only the fields to change (email, name, phone, company, address, metadata). Omitted fields are preserved.

email is the one field that can be cleared as well as replaced: sending email: null erases it. See Erasing a buyer's email below — including why an accidental null is a real hazard worth guarding against in your client.

curl -X PATCH https://checkout.vonpay.com/v1/buyers/vp_by_live_k7x9m2n4p3q5r6s8 \
-H "Authorization: Bearer vp_sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Jane A. Doe", "phone": "+1 212 555 0199" }'

The response is the full post-update profile. Rules to know:

  • external_id is NOT patchable — it is the identity key saved payment methods and the checkout flows hang off, and re-keying it would silently detach them. Sending it is rejected with 400. Create a new buyer instead.
  • Setting an email that already belongs to another of your buyers returns 409 buyer_email_conflict — emails are unique per merchant across buyer records, and rejecting beats silently merging two buyer records into one.
  • metadata merges — existing keys are preserved, new keys are added, keys are never removed, and an existing key you send again is overwritten; an empty object does not clear it (Metadata).
  • The same uniform 404 buyer_not_found posture as the retrieve applies.
  • first_name / last_name are not accepted here — they are not stored on the buyer record at all. See Putting a name on the payment.

Erasing a buyer's email​

Send email: null on the update to erase the stored address:

curl -X PATCH https://checkout.vonpay.com/v1/buyers/vp_by_live_k7x9m2n4p3q5r6s8 \
-H "Authorization: Bearer vp_sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": null }'

This clears the address on the buyer record and the lookup hash that finds a buyer by email, so the buyer stops being findable by address. It also asks your payment provider to drop their copy — but a 200 here does not confirm that happened. That request is dispatched after we answer you, is never awaited, and reports no failure back: a provider error, a timeout or an open circuit breaker all pass silently.

⛔ And for a buyer you only ever identified by email, the retraction cannot be performed at all — the key we would need to reach the provider's record was derived from the address that was just erased. Do not tell a shopper the provider's copy is gone on the strength of a success response.

It is not a complete erasure of the address, and you should not describe it to a shopper as one. Copies survive elsewhere. These are the ones we have confirmed:

WhereWhat clears it
The settlement ledger — an encrypted copy kept with the payment for financial-audit retentionNothing. It remains for the life of that retention.
Checkout sessions from the recent pastAn age-based anonymiser at 90 days — so immediately after this call the address is still on every session from the last quarter.
An address already forwarded to your connected store at order timeYour store. It is an external system and there is no retraction path from here.

The table is not an inventory of every copy, and it does not include your own systems. For a formal erasure request, treat this call as the profile half and contact us for the retained copies.

What survives and what doesn't:

  • The buyer record survives, and so does every payment linked to it — the id keeps working, and reporting and reconciliation are unaffected.
  • The address is gone from the buyer record and cannot be restored there. There is no undo — and, per the table above, this is the buyer record only, not every copy.
  • An email-only buyer becomes unfindable by lookup. The record is still reachable by its vp_by_* id, but GET /v1/buyers?email= will no longer locate it. If you rely on email lookup, keep your own reference in external_id before you erase — that keeps the record findable afterwards.
An accidental null erases just as permanently as a deliberate one

A JSON serialiser that emits absent optional fields as null turns a routine phone-number update into an email erasure that returns 200. Send only the keys you intend to change.

Putting a name on the payment​

If you want the shopper's name to appear on the payment at your processor, send first_name and last_name on the charge — POST /v1/payment_intents, either as flat fields or inside the nested buyer object:

curl https://checkout.vonpay.com/v1/payment_intents \
-H "Authorization: Bearer vp_sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 1499,
"currency": "USD",
"payment_method": { "id": "vp_pmt_live_..." },
"buyer": {
"external_id": "user_8f3a2b",
"first_name": "Jane",
"last_name": "Doe"
}
}'

They belong to the charge, not to the profile. first_name and last_name are not stored on the buyer record — name is the stored field — and they are accepted on POST /v1/buyers and POST /v1/tokens only so the buyer shape is the same everywhere; only the charge forwards them. There is nothing to backfill: PATCH /v1/buyers/{id} rejects them with 400 validation_error, and charges already made cannot be amended — send them on every charge from now on.

A combined name is never split into given and family parts. Send the structured pair or nothing; when you send both, first_name / last_name take priority. Each is capped at 100 characters (shorter than name's 200) and a longer value is rejected, not shortened.

Whether the name reaches your provider is an account-level setting

The billing block that carries these fields is forwarded per account, not per request, so a successful response does not by itself confirm it was sent. The fields are accepted either way. Ask us if you need it switched on and aren't sure it is.

billing_address is the other half of the same block, and it is what makes address verification run at all — without it, that check never happens on your charges.

Nested buyer on the create calls​

You don't have to call POST /v1/buyers separately — the three create calls accept an optional nested buyer object that upserts the same buyer record and links the session / charge / token to it. It's the richer alternative to the flat buyer fields, which remain fully supported.

SurfaceCasingNested object keysFlat fields (still supported)
POST /v1/sessionscamelCaseexternalId, email, name, phone, company, address (addressLine1, addressLine2, city, state, postalCode, country), metadatabuyerId, buyerName, buyerEmail
POST /v1/payment_intentssnake_caseexternal_id, email, name, first_name, last_name, phone, company, address (address_line1, …), metadatabuyer_id, buyer_email
POST /v1/tokenssnake_casesame as payment intentsbuyer_id

On POST /v1/sessions every one of these is secret key only: a publishable-key request carrying buyerId, buyerEmail, buyerName or the nested buyer object is refused with 403 auth_key_type_forbidden. A buyer identity is what a saved card is stored under, so it may only be asserted from your server; see Buyer identification.

⚠️ first_name / last_name are the exception to "upserts the same buyer record". They are accepted on all three surfaces for a consistent shape, but they are not part of the stored profile anywhere — and only POST /v1/payment_intents does anything with them. See Putting a name on the payment.

The rules, on all three surfaces:

  • The nested object must carry an identity (external_id / externalId or email) — either directly or via the surface's flat field. A nested buyer with no identity from either source returns 400 validation_error.
  • Sending a flat field AND its nested counterpart with equal values is fine. Different identity values return 400 validation_error rather than silently preferring one (on payment intents, buyer_id also doubles as the saved-payment-method ownership assertion — the server never guesses which value you meant).
  • The accepted buyer object is echoed back on the create response (present only when you sent it; buyer.metadata is echoed post-scrub — the stored value), so a successful write is verifiable.
  • On POST /v1/payment_intents, when the stored payment_method already carries a buyer from vault time, that saved buyer still wins for attribution.

Example — session create with a nested buyer (camelCase):

curl https://checkout.vonpay.com/v1/sessions \
-H "Authorization: Bearer vp_sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 1499,
"currency": "USD",
"buyer": {
"externalId": "user_8f3a2b",
"email": "jane@example.com",
"name": "Jane Doe",
"address": {
"addressLine1": "1 Market St",
"city": "San Francisco",
"state": "CA",
"postalCode": "94105",
"country": "US"
}
}
}'

Reading buyer identity back from a charge​

When a charge is linked to a buyer (a buyer_id / buyer_email sent at create, a nested buyer, or a saved payment method that belongs to a buyer), GET /v1/payment_intents/{id} includes buyer_id (your customer reference) and buyer_email — so you can go from a charge back to the buyer later (support, reconciliation). Charges with no buyer linkage omit both keys.

The linkage applies to automatic and manual captures — a capture_method: "manual" (authorize-then-capture) charge sent with buyer identity at create carries it through the capture, so it reads back the same as an automatic capture.

Errors​

StatusCodeCause
400validation_errorA field failed validation; neither external_id nor email on create; neither lookup parameter on GET /v1/buyers; external_id, first_name, last_name (or any other unknown field) in a PATCH body; or a flat buyer field and its nested buyer counterpart sent with different identity values on a create call.
403auth_key_type_forbiddenA publishable key was used — the Buyers API is secret-key only.
404buyer_not_foundThe vp_by_* id is unknown to this merchant (opaque — missing, another merchant's, or a test/live mode mismatch).
409buyer_email_conflictA PATCH tried to set an email that already belongs to another of your buyers.
429rate_limit_exceeded/v1/buyers* shares the payment-routes limit (300 requests / 60s per secret key) — see Rate Limits.