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.
What's a "buyer"? A buyer is your customer — the person paying. "Merchant" is your business. That's why this resource is called buyers rather than customers: it names the paying side of the transaction unambiguously.
Required key: secret key (vp_sk_*). Publishable keys are rejected with 403. Buyer profile fields are PII — encrypted at rest and returned decrypted only to the merchant that owns them, on this server-to-server surface. 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 | 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 | 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 | null | Buyer's full name (1–200 chars). |
phone | string | 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 | 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.
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; there is no field clearing in v1 (send a new value to replace).
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 with append semantics — existing keys are preserved, new keys are added, keys are never removed.- The same uniform
404 buyer_not_foundposture as the retrieve applies.
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, phone, company, address (address_line1, …), metadata | buyer_id, buyer_email |
POST /v1/tokens | snake_case | same as payment intents | buyer_id |
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.
This is a server-to-server surface only; browser-facing endpoints never return buyer identity.
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 (or an 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-operations bucket (100 requests / 60s per key) — see Rate Limits. |
Related
- Create a session — the flat
buyerId/buyerEmailfields and buyer identification guidance - Payment Intents — charging with buyer attribution
- Tokens — associating a vaulted payment method with a buyer
- Error codes — the full error catalog