subject is your id for one of your end users. You already have one — a row id, a UUID,
a user id from your auth provider — so we never generate a second. Rows carry it, keys can be
bound to it, and a bound key can only see and create rows for that one person.
The rule, in one line. A key with a
subject sees only that subject’s rows. A key without
one sees the whole tenant, exactly as it always has.Why this exists
Scopes narrow what a key may do.subject narrows whose data it
may do it to. You need the second axis for the same reason you need the first, only more so.
The holder of a Farthing key is frequently an autonomous agent, and that agent spends its
run reading merchant pages — untrusted text, written by someone else, quoted back to the model
in option names, form labels and progress messages. “List the buyer profiles” is a completely
plausible call for a confused or prompt-injected agent to make. It is also a call that, on a
tenant-wide key, returns every name, email and shipping address you have ever stored.
The blast radius of that call should be one person. That is the whole feature.
Concretely, on a tenant serving thousands of end users through one agent, a leaked or
manipulated tenant-wide key exposes all of them at once. A bound key exposes the one buyer it
was minted for, and cannot mint itself anything wider.
Nothing changes until you opt in
Every key issued before this existed carries no subject, which means tenant-wide, which is exactly what those keys have always been. There is no migration, no behaviour change, and no new failure mode on an integration that ignores this page. Isolation is something you turn on per key, at creation.Bind a key to an end user
subject on POST /v1/api-keys is the only place a binding is created. It is fixed for the
life of the key, like mode and scopes. The call at the top of this page answers with:
Response (201)
subject is present on the creation response only when the key is bound. On
GET /v1/api-keys it is always present and null for a tenant-wide key — which is how you
audit, from your admin key, which of your outstanding keys act for whom.
string
1 to 200 characters, trimmed. Opaque to us: never parsed, never validated against anything,
never sent to a merchant, and never typed into a checkout form. There is no registry of
subjects and no endpoint that lists them — a subject exists because a row or a key mentions it.Prefer your own internal id over an email address or a name. It is stored with us and echoed
back on reads, and an opaque id is the version of that data you would rather have in both
places.
400
message is Invalid profile on POST /v1/buyer-profiles and Invalid request on the other
two; the issues are the same. Surrounding whitespace is stripped before the value is stored, so
" user_a91f " and "user_a91f" are the same subject rather than two.
What a bound key can reach
Every lookup a bound key makes is filtered on(tenant, subject), and the endpoints that answer
account-level questions refuse it outright.
The four account-level
403s — the key list, revocation, usage, webhooks — are all the same
judgement: those calls answer questions about the account, not about a buyer. An end user’s key
enumerating its siblings, revoking the account’s own key,
reading the account’s total spend, or subscribing to everyone’s checkout activity would each be
a leak between your own customers.
403 — a bound key reading account usage
Authority passes down, never sideways
A bound key can mint keys, which is useful — a per-session credential for a single buyer, narrower in scope than the one that issued it. What it cannot do is widen:- Omitting
subjectinherits the binding. It does not mean tenant-wide. There is no body that mints a tenant-wide key from a bound one. - Naming a different
subjectis a403.
403
mode: a key can only ever hand out authority it already holds. Without it, one POST turns a
single buyer’s credential into the account’s.
Which buyer profile a checkout may use
buyerProfileId on POST /v1/checkouts is validated against the checkout’s subject, not the
authenticating key’s. That distinction is the whole point: a tenant-wide admin key passes any
key-level fence trivially, so a check written against the key would have let one POST create a
checkout stamped subject: "user_bob" while spending Alice’s stored address — handing her address
to a merchant, her order to a stranger, and writing any merchant login that run captured back onto
her profile.
400 — a bound key reaching for another end user's profile
403 — a tenant-wide key mixing two end users
400 rather than a 403 because from a bound key’s position the profile genuinely
does not exist — the ownership check and the existence check are the same query, and a 403 would
confirm that some other end user has a profile by that id.
An untagged profile with a tagged checkout stays legal, and so does the reverse. Only a
genuine mismatch between two different end users is refused.That is deliberate: adoption is incremental. Profiles you created before you started tagging have
no subject, a shared operations address is a reasonable thing to keep untagged, and refusing those
combinations would mean no tenant could turn
subject on without a backfill it has no endpoint
for.Tagging rows from a tenant-wide key
subject is also accepted on the create bodies, which is how rows get tagged in the first place
if you would rather keep one admin key than mint one per user:
403.
403
A typo cannot produce an untagged row
All three bodies that acceptsubject — POST /v1/api-keys, POST /v1/checkouts and
POST /v1/buyer-profiles — reject unknown top-level fields. A misspelled subjectt is a 400:
400
201 carrying an untagged row that
every key on the account can read. A permissive schema drops what it does not recognise, and the
field most worth misspelling here is the one that decides who can see the row.
Nested objects stay permissive on purpose — target, constraints and metadata are extension
points, and unknown keys inside them are ignored rather than refused. The strictness is at the top
level, where subject lives.
How subject comes back
Three shapes, and they differ. Worth reading once if you are writing a client:
Read it with a null-and-absent-tolerant accessor rather than a presence check, and none of this
matters.
Listing one end user’s runs
GET /v1/checkouts?subject=user_a91f narrows the list to one end user, so a tenant that tags from
an admin key can answer “what is this person’s agent doing” without fetching every checkout:
status, limit and cursor, and takes the same shape as every other
subject — 1 to 200 characters, or a 400. A well-formed subject that no checkout carries is an
empty page, not an error. Rows carry subject when the checkout has one, so you can also read it
off an unfiltered page.
A bound key does not need the parameter — its page is already narrowed to one person — and asking
for somebody else’s is refused rather than ignored:
403
A profile cannot be reassigned
PATCH /v1/buyer-profiles/{id} does not accept subject, and does not ignore it either:
400
200 and a profile that is
still where it was. There is no move endpoint: create a profile under the right subject and
delete the wrong one.
What isolation does not cover
Honest boundaries. These are tenant-level facts, and asubject does not partition them.
Quotas are per tenant, not per end user
Quotas are per tenant, not per end user
The default 5 concurrent checkouts and 200 per rolling 24 hours are counted across the whole
account. One end user’s five parked runs will
429 everybody else’s next create. If you are
fanning a real user base through this, the concurrency cap is the number to talk to us about
before the subject model is. See Quotas.Webhook endpoints are tenant-wide
Webhook endpoints are tenant-wide
One endpoint receives the status transitions of every checkout on the account, for every end
user. There is no per-subject endpoint, no per-subject delivery filter, and a bound key cannot
register or read an endpoint at all (
403).The payload does name the end user — subject is on it whenever the checkout has one — so
routing a delivery to the right person needs neither a callback nor a map of your own. See
the event.Usage and billing are the account's
Usage and billing are the account's
GET /v1/usage reports completed checkouts and gross merchandise volume for the tenant.
There is no per-subject breakdown, and a bound key cannot read it at all. Attribute spend
yourself from the subject on each checkout — ?subject= on the checkout list is the cheapest
way to gather them.Key mode is a separate axis
Key mode is a separate axis
A bound key is still a
ck_test_ or ck_live_ key, and mode is enforced independently — a
bound test key is confined to sandbox merchants exactly like any other test key. Binding a
key does not make it safe to spend, and making it live does not widen whose data it sees.There is no subject registry
There is no subject registry
Subjects are not objects. Nothing creates, lists, renames or deletes one; a subject exists
because a key or a row mentions the string. You cannot ask the API which subjects you have
used, so keep that list on your side — it is your user table.
Isolation is enforced at the API surface
Isolation is enforced at the API surface
The rule is applied to authenticated requests. Our own orchestrator drives runs as the
system rather than on behalf of a caller, because it has to be able to complete any checkout
it is handed — so
subject is a fence around your keys, not an encryption boundary. If your
threat model needs the latter, talk to us rather than inferring it from this page.Adopting it on an existing account
1
Use the id you already have
Your user table’s primary key. Anything stable and opaque, up to 200 characters.
2
Create the buyer profile under that subject
Either with a bound key (which stamps it automatically) or with your admin key and an
explicit
"subject". Do this before the first checkout for that user, because a profile
cannot be moved afterwards.3
Mint one bound key per end user
Keys are cheap and revocation is a timestamp, so a key per user — or per session — is a
reasonable shape. Narrow the scopes at the same time:
["checkouts.write"] plus the
buyerProfileId in your own config is the tightest useful pair.4
Keep one tenant-wide key for the account
Wherever you keep your other deployment secrets. It is the only credential that can list
keys, read usage, manage webhooks, and see across your users — which is the point of it
being the one you do not hand to an agent.
Idempotency keys are namespaced by subject
AnIdempotency-Key is unique per (tenant, subject), not
per tenant. Two of your end users independently choosing "order-1" — a timestamp, a retry
counter, a cart id — is ordinary, and they have no way to coordinate because they cannot see
each other. Each gets their own checkout; neither can replay the other’s.
The window is 24 hours, and it is per key rather than per subject: after that the same string is
free again for whoever sends it next.
Errors
Next
Authentication
Scopes, key mode, rotation — the other two dimensions of what a key may do.
Buyer profiles
The identity a checkout ships to, and the row
subject most matters on.Buyer sessions
Stored merchant logins. They inherit isolation through the profile they belong to.
Idempotency
Safe retries on
POST /v1/checkouts, keyed per end user.