Authentication
Merchant API keys
Authorization: Bearer tbbn_sk_live_... for live, tbbn_sk_sandbox_... for test mode — the prefix
determines the mode, not the host you call. A test key works only on your Merchant's test account,
so nothing it does reaches live data; see Test mode. Create keys in your
Merchant Dashboard; you'll see the full value exactly once, so store it like any other secret. Keys
are independently revocable — rotating one doesn't affect any human session on the same account.
A key the dashboard creates can call the whole API. A key can instead be limited with scopes: a
list of API areas, the first part of the path after /v1/ (listings, offers,
trade-sessions…). A request outside them gets 403 SCOPE_NOT_GRANTED.
Check a key works with GET /v1/auth/me — any key may call it, whatever its scopes. It
answers with the Merchant the key belongs to and whether it's a test key:
{
"principalType": "merchant_api_key",
"merchantId": "6f1c…",
"merchant": {
"id": "6f1c…",
"displayName": "Your store",
"status": "APPROVED",
"livemode": true
},
"livemode": true,
"restriction": null
}
New keys start tbbn_sk_. Keys from before November 5, 2026 start sk_live_ or sk_sandbox_ and
keep working, so don't check for a particular prefix in your own code.
A key found in public is revoked. TBBN watches for new keys in public code with GitHub. A real one
found there is revoked straight away, and you get an email and the api_key.exposed webhook.
Limit a key when you create it or with PATCH /v1/api-keys/{id}:
expiresAt: after this time the key answers401 API_KEY_EXPIRED.allowedIps: the IP addresses or CIDR ranges it works from; from anywhere else it answers403 API_KEY_IP_NOT_ALLOWED.
A 401 means the key is wrong, revoked or rotated. restriction: "WIND_DOWN" means your
production access is paused (an unpaid plan or a usage balance below zero): you can still finish
or unwind trades under way, but not start new activity.
Merchant representatives and step-up verification
People on your team sign in to the Merchant Dashboard with a one-time code and get a session
access token. Changing your merchant's own settings — PATCH /v1/merchants/{id}, for example
checkoutEnabled, matchingEnabled or counterpartyHoldDays — needs that session, from a
representative with the OWNER or ADMIN role. An API key is refused. Once your merchant is approved,
the change also needs a step-up token:
POST /v1/auth/step-up/requestwith the session — a verification code goes to the representative's email.POST /v1/auth/step-up/confirmwith{ "code": "123456", "scope": "merchant:update" }— returns a token valid for 5 minutes, for this kind of change only.- Send the change with both headers:
PATCH /v1/merchants/{id}
Authorization: Bearer <session access token>
X-TBBN-Step-Up-Token: <step-up token>
Content-Type: application/json
{ "checkoutEnabled": false }
The dashboard's settings page runs these steps for you.
The same check, with "scope": "credentials:manage", applies to creating, rotating or limiting a
live API key, rotating an OAuth client secret, adding a webhook or rotating its secret from a session
on a live account, and opening a finished Stripe payout account. Without the token the answer is
403 STEP_UP_REQUIRED, with the scope to confirm in details.scope. A token lasts 5 minutes and can
be used for several of these.
Seller sign-in (for embedded widgets or your own UI)
Sellers never have a password. Sign-in is a one-time code sent by email, or optionally phone, or a magic link — issued by TBBN, not something you implement yourself if you're using an official widget or SDK flow. If you're building a fully custom experience against the raw API: the code is 6 digits, expires in 10 minutes, and is capped at 5 send-requests per hour per address — build your UI to handle that rate limit gracefully (a clear "too many attempts, try again in a bit" state) rather than retrying silently.
Acting on behalf of a seller
Any route that mutates one seller's state (accept/reject/cancel an offer, cancel a trade session, confirm a peer-to-peer exchange) needs to know which seller you're acting for. There are exactly two valid ways:
- The seller themself, authenticated with their own session — they can only ever act for themselves.
- A merchant the seller is linked to — the core integration case, where your platform drives the flow through the API on behalf of your own seller. This is also the only way to act for a headless seller (one with no linked TBBN account at all).
An actingSellerId field on a request body is a statement of intent, not a grant of identity — it
still has to match one of the two cases above. Naming a seller you don't actually have a
relationship to gets rejected, not honored.
Rate limits
Every response carries the standard RateLimit and RateLimit-Policy headers. See
API conventions.
