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"
}
| Field | Type | Description |
|---|---|---|
id | string | Buyer id, mode-prefixed (vp_by_test_* / vp_by_live_*). |
external_id | string (≤ 200) | null | Your 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. |
email | string (≤ 254) | null | Buyer's email (valid email, max 254 chars). The fallback lookup key when external_id is absent. Unique per merchant across buyer records. |
name | string (≤ 200) | null | Buyer'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. |
phone | string (≤ 32) | null | Phone 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. |
company | string (≤ 200) | null | Company name (1–200 chars). |
address | object | null | Buyer-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. |
metadata | object | Free-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_at | string | When the buyer record was first created (ISO 8601). |
last_seen_at | string | Touched 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_idis 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 with400. Create a new buyer instead.- Setting an
emailthat already belongs to another of your buyers returns409buyer_email_conflict— emails are unique per merchant across buyer records, and rejecting beats silently merging two buyer records into one. metadatamerges — 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_foundposture as the retrieve applies. first_name/last_nameare 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:
| Where | What clears it |
|---|---|
| The settlement ledger — an encrypted copy kept with the payment for financial-audit retention | Nothing. It remains for the life of that retention. |
| Checkout sessions from the recent past | An 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 time | Your 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, butGET /v1/buyers?email=will no longer locate it. If you rely on email lookup, keep your own reference inexternal_idbefore you erase — that keeps the record findable afterwards.
null erases just as permanently as a deliberate oneA 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.
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.
| Surface | Casing | Nested object keys | Flat fields (still supported) |
|---|---|---|---|
POST /v1/sessions | camelCase | externalId, email, name, phone, company, address (addressLine1, addressLine2, city, state, postalCode, country), metadata | buyerId, buyerName, buyerEmail |
POST /v1/payment_intents | snake_case | external_id, email, name, first_name, last_name, phone, company, address (address_line1, …), metadata | buyer_id, buyer_email |
POST /v1/tokens | snake_case | same as payment intents | buyer_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/externalIdoremail) — either directly or via the surface's flat field. A nested buyer with no identity from either source returns400 validation_error. - Sending a flat field AND its nested counterpart with equal values is fine. Different identity values return
400 validation_errorrather than silently preferring one (on payment intents,buyer_idalso doubles as the saved-payment-method ownership assertion — the server never guesses which value you meant). - The accepted
buyerobject is echoed back on the create response (present only when you sent it;buyer.metadatais echoed post-scrub — the stored value), so a successful write is verifiable. - On
POST /v1/payment_intents, when the storedpayment_methodalready 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
| Status | Code | Cause |
|---|---|---|
400 | validation_error | A 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. |
403 | auth_key_type_forbidden | A publishable key was used — the Buyers API is secret-key only. |
404 | buyer_not_found | The vp_by_* id is unknown to this merchant (opaque — missing, another merchant's, or a test/live mode mismatch). |
409 | buyer_email_conflict | A PATCH tried to set an email that already belongs to another of your buyers. |
429 | rate_limit_exceeded | /v1/buyers* shares the payment-routes limit (300 requests / 60s per secret key) — see Rate Limits. |
Related
- Create a session — the flat
buyerId/buyerEmailfields and buyer identification guidance - Payment Intents — charging with buyer attribution, and what reaches your payment provider
- Address verification — what
billing_addressbuys you on a charge - Tokens — associating a vaulted payment method with a buyer
- Error codes — the full error catalog