API conventions
Versioning
Path-based: /v1/.... No header-based versioning. A breaking change ships as /v2/... alongside
/v1/..., which stays available for a published deprecation window.
Auth headers
- Merchant API keys:
Authorization: Bearer sk_live_...(production) orAuthorization: Bearer sk_sandbox_...(sandbox). The prefix determines the environment — it is never inferred from the host. - Sessions:
Authorization: Bearer <session access token>, issued by TBBN's one-time-code or magic-link sign-in — for a merchant representative signed in to the Merchant Dashboard, or a member acting through TBBN Marketplace or your embedded widget. - Step-up verification: a few sensitive changes — for example editing an approved Merchant's
settings with
PATCH /v1/merchants/{id}— need a representative's session plus a fresh verification code. Request one withPOST /v1/auth/step-up/request, confirm it withPOST /v1/auth/step-up/confirm, and send the token you get back asX-TBBN-Step-Up-Tokenon the change. An API key can't make these changes. See Authentication.
Request ids
Every response carries an X-Request-Id header (req_...), and every error body repeats it as
requestId. Quote it whenever you contact support. You may send your own X-Request-Id (req_ followed by 8–64
letters or digits) to correlate a call with your logs; otherwise TBBN assigns one.
Errors
Every error — from any endpoint, including authentication, rate limiting and idempotency — uses one shape:
{
"error": {
"code": "NOT_FOUND",
"message": "Listing not found",
"requestId": "req_8f3a1c2b9d4e5f60718293a4"
}
}
codeis stable, machine-readable SCREAMING_SNAKE_CASE. Branch on it, never onmessage.messageis for people and its wording may change.requestIdis always present and matches theX-Request-Idheader.detailsappears only when there is structured context:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "currency must be a 3-letter ISO 4217 code; price must be a positive number",
"requestId": "req_1b2c3d4e5f6a7b8c9d0e1f2a",
"details": {
"messages": ["currency must be a 3-letter ISO 4217 code", "price must be a positive number"]
}
}
}
| Status | Typical code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED, BAD_REQUEST, IDEMPOTENCY_KEY_INVALID | The request is malformed. Fix it before retrying. |
| 401 | UNAUTHENTICATED, SIGN_IN_REQUIRED, SESSION_EXPIRED, SANDBOX_KEY_RETIRED | Missing, invalid or expired credential; a signed-in session that has ended (sign in again); or a test key made before test mode. |
| 402 | PLAN_REQUIRED, PAYMENT_REQUIRED, USAGE_BALANCE_EXHAUSTED | Live use needs a paid plan, or production access is paused for billing. Test keys are unaffected. |
| 403 | FORBIDDEN, SCOPE_NOT_GRANTED | The credential is valid but can't do this. |
| 404 | NOT_FOUND | No such resource, or not one you can see. |
| 409 | CONFLICT, IDEMPOTENCY_* | The request conflicts with the resource's current state or an earlier request with this key. |
| 413 | PAYLOAD_TOO_LARGE | The body is over 1 MB. |
| 422 | UNPROCESSABLE | Well-formed but breaks a business rule. |
| 429 | RATE_LIMITED | Too many requests. See "Rate limiting". |
| 5xx | INTERNAL_ERROR, BAD_GATEWAY, SERVICE_UNAVAILABLE, GATEWAY_TIMEOUT | A fault on TBBN's side. The message is generic; quote the requestId. |
Idempotency
Send an Idempotency-Key header (any unique string up to 255 characters, e.g. a UUID) on a
POST, PUT, PATCH or DELETE made with a credential, and you can retry it safely. Keys belong
to the credential's owner (your merchant, Business or user), the method and the path, and are kept
for 24 hours. The header is optional, but send it whenever a timeout could leave you unsure whether
a call went through — above all when creating offers, bookings or listings.
Same key, same body. The first request runs. A retry gets the first response back — same
status, same body — with the header Idempotent-Replayed: true, and nothing runs twice. The
replayed body is the original, so its requestId is the first request's.
A refusal isn't kept. When the first request is refused with a 4xx — invalid input, not
allowed, not found, a conflict — nothing happened, so nothing is stored against the key: fix the
cause (or wait for it to be fixed) and retry with the same key, and it runs for real. A replayed
response always carries Idempotent-Replayed: true, so you can tell a stored answer from a fresh
one.
Same key, different body — 409:
{
"error": {
"code": "IDEMPOTENCY_KEY_REUSE",
"message": "This Idempotency-Key was already used for a different request. Use a new key for a new request.",
"requestId": "req_5e6f7a8b9c0d1e2f3a4b5c6d"
}
}
Retry while the first request is still running — 409; wait a moment and retry:
{
"error": {
"code": "IDEMPOTENCY_IN_PROGRESS",
"message": "A request with this Idempotency-Key is still being processed. Retry shortly.",
"requestId": "req_7a8b9c0d1e2f3a4b5c6d7e8f"
}
}
Key longer than 255 characters — 400 IDEMPOTENCY_KEY_INVALID. Body over 1 MB with a
key — 413 PAYLOAD_TOO_LARGE.
After a server error
A server error doesn't prove nothing happened: the change may have been saved before the failure. So what a retry with the same key does depends on what TBBN knows about the first attempt:
| What happened to the first request | A retry with the same key |
|---|---|
It never reached a TBBN service — 502 BAD_GATEWAY, or a 429 or billing 402 | Runs for real. Nothing can have happened. |
It was refused — any 4xx | Runs for real. Nothing happened, so a corrected or later retry isn't blocked by the earlier refusal. |
It was handled and answered — a 2xx, or a 5xx | Gets that same response replayed. It is never re-run, so a change that was saved before the error can't be made twice. |
| Its outcome is unknown — the connection dropped mid-request, or the result couldn't be recorded | 409 IDEMPOTENCY_OUTCOME_UNKNOWN (below). |
{
"error": {
"code": "IDEMPOTENCY_OUTCOME_UNKNOWN",
"message": "The first request with this Idempotency-Key didn't finish cleanly, so its result can't be replayed. Check the resource's current state; if the change didn't happen, retry with a new key.",
"requestId": "req_9c0d1e2f3a4b5c6d7e8f9a0b"
}
}
When you get a replayed 5xx or IDEMPOTENCY_OUTCOME_UNKNOWN, read the resource (for example
GET /v1/offers/{id} or your list of offers) to see whether the change happened. If it did, carry
on; if not, send the request again with a new key.
Trade reports are safe to repeat on their own
Reporting a payment, fulfillment or matching-only settlement outcome that is already recorded
returns success and changes nothing, with or without a key. A report that contradicts what was
recorded (say, payment.failed after payment.completed) is 409 with a message saying what was
recorded.
Pagination
Paginated lists take cursor and limit and return { "data": [...], "nextCursor": "..." }.
Pass nextCursor back as cursor for the next page; null means there are no more. Each
endpoint's reference lists its maximum limit.
Rate limiting
Each credential (or, without one, each network address) has a per-minute request ceiling, and
sign-in endpoints have tighter limits of their own. Every response carries the standard
RateLimit and RateLimit-Policy headers — for example RateLimit: limit=600, remaining=598, reset=42 — and going over returns 429 RATE_LIMITED. Back off until the reset seconds have
passed. Rate limits are separate from usage billing: see Pricing.
Webhook signatures
X-TBBN-Signature: t=<ts>,v1=<hmac>, 5-minute replay window, verification helper shipped in every
SDK. See Webhooks & events.
Formats
Money amounts travel with an ISO 4217 currency code. Phone numbers are E.164. Dates and times are
ISO 8601 in UTC.
