Skip to main content

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 callingKey limitValue limitWhole-map cap
POST /v1/sessions (hosted checkout)none500 characters, strings onlynone — see below
POST /v1/payment_intentsnoneany JSON value8 KB serialized
POST /v1/refundsnoneany JSON value8 KB serialized
POST /v1/tokensnoneany JSON value8 KB serialized
POST /v1/buyersnoneany JSON value8 KB serialized
PATCH /v1/buyers/{id}noneany JSON value8 KB serialized
Order mirroring1–64 characters500 characters, strings only4 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/sessions has 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 from validation_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, last4 and buyer are on the list. So metadata: { number: "SO-4471" } — an order number — is silently deleted before storage, and you still get a 2xx. 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_email holding 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 calledWhat travels
POST /v1/sessions (hosted checkout)the whole map
POST /v1/payment_intentsnarrowed 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 metadata that 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_intents that 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 with mirror_malformed, so the charge never runs.
  • On /v1/sessions a string under that key is the legacy mirroring shape. The request succeeds with a Deprecation header 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.