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.

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"
}
FieldTypeDescription
idstringBuyer id, mode-prefixed (vp_by_test_* / vp_by_live_*).
external_idstring | 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 | nullBuyer's email (valid email, max 254 chars). The fallback lookup key when external_id is absent. Unique per merchant across buyer records.
namestring | nullBuyer's full name (1–200 chars).
phonestring | 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 | 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 emailat 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_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 with append semantics — existing keys are preserved, new keys are added, keys are never removed.
  • The same uniform 404 buyer_not_found posture 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.

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, phone, company, address (address_line1, …), metadatabuyer_id, buyer_email
POST /v1/tokenssnake_casesame as payment intentsbuyer_id

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.

This is a server-to-server surface only; browser-facing endpoints never return buyer identity.

Errors

StatusCodeCause
400validation_errorA 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.
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-operations bucket (100 requests / 60s per key) — see Rate Limits.
  • Create a session — the flat buyerId / buyerEmail fields 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