Metadata
metadata is a free-form map you attach to a payment so you can find it again
in your own systems — an order number, a customer reference, an internal ledger
key. We store it, echo it back, and return it whenever you read the object.
It is the answer to "which of my orders is this payment?" — and it is deliberately not an answer to "can I prove what happened?" or "is this private?". Both of those are covered below, because getting them wrong costs real money.
Where you can set it
| You are calling | Key limit | Value limit | Whole-map cap |
|---|---|---|---|
POST /v1/sessions (hosted checkout) | none | 500 characters, strings only | none — see below |
POST /v1/payment_intents | none | any JSON value | 8 KB serialized |
POST /v1/refunds | none | any JSON value | 8 KB serialized |
POST /v1/tokens | none | any JSON value | 8 KB serialized |
POST /v1/buyers | none | any JSON value | 8 KB serialized |
PATCH /v1/buyers/{id} | none | any JSON value | 8 KB serialized |
| Order mirroring | 1–64 characters | 500 characters, strings only | 4 KB on the whole mirror block |
The limits genuinely differ. A value that a payment intent accepts can be rejected by a checkout session, and a key a payment intent accepts can be rejected by order mirroring. If you write one helper that builds metadata for every call, size it to the strictest row you use.
The size cap is not the same on every call
On /v1/payment_intents, /v1/refunds, /v1/tokens and /v1/buyers the whole
map, serialized, is capped at 8 KB. Over that the request is rejected with
validation_error — nothing is truncated and nothing is stored, so you find out
at the call rather than discovering a half-written record later.
⛔ Two surfaces do not work that way, and both will surprise you:
POST /v1/sessionshas no metadata size cap at all. Nothing rejects a large map on the hosted-checkout path. That is not permission to send one — see the forwarding section below, because the whole map travels onward from there and the receiving system has its own limits you cannot see.- Order mirroring is capped at 4 KB across the WHOLE mirror block, not at
8 KB on metadata alone. That budget is shared with line items, addresses,
shipping, coupon and voucher data, and going over returns
mirror_too_large— a different error fromvalidation_error, so a handler branching only on the latter will not catch it. The session is never created and the buyer never reaches a pay page.
⚠ A nested buyer.metadata is capped separately. Some calls let you send a
buyer inline, and that buyer carries its own metadata map with its own 8 KB
budget rather than sharing the top-level one — so a request can be within the
cap at the top level and still be rejected for the nested map, or vice versa.
What comes back is not always what you sent
On /v1/payment_intents, /v1/refunds, /v1/tokens and /v1/buyers, a fixed
list of key names is removed before your metadata is stored. It exists to keep
card and buyer data out of our records.
⛔ It matches on the NAME, not on what you put in the value — and that cuts both ways. Two consequences, and the first one bites ordinary integrations:
- Common names are removed even when your value is harmless.
number,address,phone,email,shipping,card,last4andbuyerare on the list. Sometadata: { number: "SO-4471" }— an order number — is silently deleted before storage, and you still get a2xx. Qualify your keys:order_number,customer_ref,ledger_key. Names that are clearly yours are never touched. - It is not a privacy control and must not be used as one. A key named
customer_emailholding a real email address is not on the list and is stored exactly as sent. The list protects our records from a handful of known names; it does not inspect your data and cannot protect you.
⛔ The order-mirroring block is exempt on every connection. If you send a
mirror block, it is written back into stored metadata unfiltered — so a
buyer email inside a mirrored order is held in plain text regardless of the list
above. Treat mirrored order data as data you have chosen to store.
⛔ And the filter does not run at all on POST /v1/sessions. Top-level metadata on the
hosted-checkout path is stored as sent. A nested buyer.metadata on that same
call is filtered; the top-level map is not.
So: read the response rather than trusting the echo. If a key matters, confirm
it is in the object you get back. And keep personal data out of metadata
entirely — nothing here will do that for you, and the next section is the reason
it matters.
⛔ It is not private
How much of your metadata reaches the payment processor depends on which call you made AND which connection your account is on. There is no single answer:
| You called | What travels |
|---|---|
POST /v1/sessions (hosted checkout) | the whole map |
POST /v1/payment_intents | narrowed to a single order reference on one connection; every key except a handful we reserve on the others |
We are deliberately not telling you which connection you are on, because you should not build on the answer: it is a property of your account's setup, it can change without your integration changing, and the safe assumption is the permissive one.
There is no per-request flag that changes this, and no way to mark a key local. So the rule is simple:
Do not put anything in
metadatathat you would not be comfortable handing to your payment processor.
If you need something to travel to the processor on purpose — so their rules
can route, challenge or decline a charge — that is what
rule_tags is for. It is the
explicit "forward this" channel, and it is matched on. metadata is not.
Some keys are reserved, and some values are silently dropped
⛔ mirror is not yours to use — and the two surfaces fail differently:
- On
/v1/payment_intentsthat key is the order-mirroring slot. An object there is read as an instruction to push an order to a connected store; a string is rejected outright withmirror_malformed, so the charge never runs. - On
/v1/sessionsa string under that key is the legacy mirroring shape. The request succeeds with aDeprecationheader and the string is never parsed — no order is mirrored. (An object cannot be sent there: session metadata values must be strings.)
Either way, pick a different name.
⚠ On one connection, any value longer than 80 characters is dropped rather than truncated before it reaches the processor — even though this page's table lets you store 500. It is still stored on our side; it just will not be on the processor's record. A handful of internal reconciliation keys are dropped the same way. So a key you expect to see on the processor may simply not be there: reconcile against our API, not against your processor's dashboard.
⛔ It is not evidence
⚠ And do not read the table above as saying the opposite either. On the
discrete charge path with the narrowing connection, only the order reference
reaches the processor — so anything else you filed under metadata is invisible
when you contest a chargeback. You cannot tell from your own code which case you
are in, which is exactly why metadata is not dependable evidence in either
direction. Evidence has to travel as a real field (billing_address,
buyer_email) or as a rule_tags label to be dependable.
What reaches your payment provider covers which fields travel and why.
Buyer metadata merges — it does not replace
On a buyer record, writing metadata adds to what is already stored. A write
never bulk-replaces the map, and keys are never removed.
⚠ "Never removed" is not "never changed". Send a key that already exists and your new value overwrites the old one — that is deliberate, so you can correct a stored value. What you cannot do is delete one.
This is the opposite of what most APIs do with a map field, so it is worth stating plainly: you cannot clear a buyer's metadata by sending an empty object. If you are storing something you may later need to remove, keep it somewhere you control instead.
Related
- What reaches your payment provider — which fields travel, and which never do
- Payment intents —
rule_tags, the deliberate forward-to-processor channel - Buyers — the buyer record and its merge behaviour
- Statement descriptors — what the buyer actually sees on their statement