Appearance
Admin API
/admin/v1 is TenkeyBridge's provisioning control plane — the API you automate against to create realms, register OAuth clients, issue agent tokens, and manage API keys, without emailing anyone. It's versioned independently of the QBO-compatible surface and public: integrators are expected to script against it.
It is deliberately not the QBO surface. Errors here look nothing like a QBO Fault and nothing like the RFC 6749 error_description shape /oauth2/v1 uses — see Errors below. Confusing the two shapes would misrepresent what a caller is talking to.
Where this fits
There's also a self-serve portal UI — a browser in front of exactly these endpoints, for anyone who'd rather click through sign-up, org/realm/client setup, and API-key minting than script it.
Authentication
Two credentials are accepted, resolved in this order:
- An API key, in the
x-api-keyheader. Minted byPOST /admin/v1/api-keys(session only — see Things that are shown once), prefixedtkb_, scoped to exactly one organization for its whole life. - A session cookie — what the portal sends, issued by signing in at
/admin/v1/auth.
During early access, the entire /admin/v1 surface also sits behind HTTP Basic auth (ADMIN_GATE_USER/ADMIN_GATE_PASSWORD) as a stopgap until the portal ships its own account-signup flow. A valid x-api-key satisfies this Basic gate on its own — no -u flag needed — except in front of /admin/v1/auth/* (sign-in, sign-up, OAuth callbacks, magic links), which stays Basic-gated unconditionally. An API key must never be a way into the sign-up surface this gate exists to hide.
An API key cannot mint another API key — POST /admin/v1/api-keys and DELETE /admin/v1/api-keys/:id require a session. A leaked key that could re-mint itself would outlive any attempt to revoke it.
API keys are also rate-limited: 120 requests/minute, per key. Exceeding it returns 429 with { "error": "rate_limited", "message": "..." }.
Organizations and roles
Every realm, OAuth client, agent token, and API key belongs to an organization, never to an individual user. A user reaches an org through membership, at one of three roles:
| Role | Can do |
|---|---|
owner | Everything |
admin | Create/revoke realms, clients, and agent tokens; invite members; issue/revoke API keys |
member | Read-only: list realms, clients, members, API keys |
An API key's permissions are its creator's current role, re-resolved from the org's member table on every request — not baked into the key at issue time. Demote or remove the creator from the org and every key they made is de-fanged immediately, with no separate revocation step.
GET /admin/v1/orgs lists the organizations the caller belongs to, with their role in each:
bash
curl -s https://api.tenkeybridge.com/admin/v1/orgs \
-H "x-api-key: $TKB_API_KEY"json
{
"orgs": [
{ "id": "org_abc123", "name": "ExampleCo", "slug": "exampleco", "role": "owner" }
]
}An API-key principal only ever sees the one org its key is scoped to, even if the key's creator belongs to others.
GET /admin/v1/orgs/:orgId/members (any member) lists the org's members:
bash
curl -s https://api.tenkeybridge.com/admin/v1/orgs/org_abc123/members \
-H "x-api-key: $TKB_API_KEY"json
{
"members": [
{
"id": "mem_1",
"userId": "usr_1",
"email": "owner@exampleco.example",
"name": "Pat Owner",
"role": "owner",
"createdAt": "2026-08-01T00:00:00.000Z"
}
]
}Endpoints
Every endpoint below requires org membership at the listed role (orgId is either a body field on create, a query parameter on list, or resolved server-side from the path resource on everything else).
Realms
POST /admin/v1/realms (admin) — create a realm (tenant) under an org.
bash
curl -s -X POST https://api.tenkeybridge.com/admin/v1/realms \
-H "x-api-key: $TKB_API_KEY" -H "content-type: application/json" \
-d '{"orgId": "org_abc123", "name": "Acme Plumbing"}'json
{
"realm": {
"id": "9130351234567890",
"name": "Acme Plumbing",
"orgId": "org_abc123",
"createdAt": "2026-08-10T00:00:00.000Z"
}
}realm.id is a QBO-shaped realm id ([1-9][0-9]{15}) — the same realmId you'll pass to /oauth2/v1/authorize and use in every /v3/company/{realmId}/... call.
GET /admin/v1/realms?orgId=... (member) — list an org's realms.
GET /admin/v1/realms/:realmId (member) — realm detail, including agent connectivity and its agent tokens (never the token secrets themselves):
bash
curl -s https://api.tenkeybridge.com/admin/v1/realms/9130351234567890 \
-H "x-api-key: $TKB_API_KEY"json
{
"realm": {
"id": "9130351234567890",
"name": "Acme Plumbing",
"orgId": "org_abc123",
"createdAt": "2026-08-10T00:00:00.000Z"
},
"agent": { "online": true },
"agentTokens": [
{
"id": "tok_1",
"label": "front office",
"createdAt": "2026-08-10T00:00:00.000Z",
"lastUsedAt": null,
"revoked": false,
"revokedAt": null
}
]
}POST /admin/v1/realms/:realmId/agent-tokens (admin, optional { "label": "..." } body) — issue an agent token for that realm, for the edge agent's appsettings.json.
bash
curl -s -X POST https://api.tenkeybridge.com/admin/v1/realms/9130351234567890/agent-tokens \
-H "x-api-key: $TKB_API_KEY" -H "content-type: application/json" \
-d '{"label": "front office"}'json
{ "id": "tok_1", "label": "front office", "token": "tkba_..." }DELETE /admin/v1/agent-tokens/:tokenId (admin) — revoke an agent token. 204 on success. This is a top-level route, not nested under a realm — you don't need to know a token's realm to revoke it by id.
Realms cannot be deleted in v1 — DELETE /admin/v1/realms/:realmId 404s. A QuickBooks company is real-world state (invoices, customers, years of history) that outlives any particular integration; there is no safe "delete a company" operation to expose here, and nothing else in the product needs one yet.
OAuth clients
POST /admin/v1/clients (admin) — register an OAuth client.
bash
curl -s -X POST https://api.tenkeybridge.com/admin/v1/clients \
-H "x-api-key: $TKB_API_KEY" -H "content-type: application/json" \
-d '{"orgId": "org_abc123", "name": "ExampleCo", "redirectUris": ["https://app.exampleco.example/cb"]}'json
{
"client": {
"id": "tkbc_...",
"name": "ExampleCo",
"redirectUris": ["https://app.exampleco.example/cb"],
"orgId": "org_abc123",
"createdAt": "2026-08-10T00:00:00.000Z"
},
"clientSecret": "tkbs_..."
}redirectUris is a non-empty array (max 10) of absolute URLs, each validated strictly: https required, except for loopback hosts (localhost, 127.0.0.1, [::1]), which may use plain http for local development; no userinfo; no fragment; no * wildcard; and the value must already be in normalized form (the exact string new URL(...) would produce — no default port, no trailing whitespace, no embedded tab/newline) since the authorize-time matcher compares byte-for-byte. A rejected URI returns 400 invalid_request naming what's wrong.
GET /admin/v1/clients?orgId=... (member) — list an org's clients. Never includes clientSecret.
PATCH /admin/v1/clients/:clientId (admin, { "redirectUris": [...] }) — replace a client's redirect URIs (same validation as create). This is a full replacement, not a merge.
DELETE /admin/v1/clients/:clientId (admin) — delete an OAuth client. 204 on success. This deletes the client's authorization codes and access/refresh tokens in the same transaction — every company connected through that client is disconnected immediately. There's no cascade-free "just deregister the app" option: a token whose issuing client no longer exists is not a credential TenkeyBridge is willing to keep honoring.
API keys
POST /admin/v1/api-keys (admin, session only — an API key cannot call this).
bash
curl -s -X POST https://api.tenkeybridge.com/admin/v1/api-keys \
-H "cookie: $SESSION_COOKIE" -H "content-type: application/json" \
-d '{"orgId": "org_abc123", "name": "provisioning"}'json
{
"id": "key_1",
"name": "provisioning",
"orgId": "org_abc123",
"key": "tkb_...",
"createdAt": "2026-08-10T00:00:00.000Z"
}name is at most 32 characters. orgId is baked into the key's metadata for its whole life — a key is never re-scoped to a different org.
GET /admin/v1/api-keys?orgId=... (member) — list an org's keys. Never includes the key value; a key made by someone who has since left the org is still listed (but no longer authorizes anything — see above) until it's explicitly revoked.
DELETE /admin/v1/api-keys/:id (admin, session only) — revoke a key immediately. 204 on success.
Errors
Every error on this surface is the same two-field shape:
json
{ "error": "unauthenticated", "message": "..." }error | HTTP status | Meaning |
|---|---|---|
unauthenticated | 401 | No session and no valid x-api-key. |
forbidden | 403 | Authenticated, but your role in this org is too low for the action. |
invalid_request | 400 | Malformed body, missing required field, or a redirect URI that fails validation. |
not_found | 404 | No such resource — or a resource that belongs to an organization you're not a member of. |
rate_limited | 429 | An API key crossed 120 requests/minute. |
internal_error | 500 | Unexpected server error. |
A resource belonging to another organization and a resource that doesn't exist return the byte-identical not_found. This is deliberate: a distinguishable 403 ("yes, that realm exists, you just can't see it") would let a caller enumerate which realm ids, client ids, org ids, and agent-token ids exist by brute-forcing responses. not_found is the one answer that confirms nothing.
Things that are shown once
Three kinds of secret are returned exactly once, at creation, and never again:
- OAuth client secrets (
clientSecretonPOST /clients) - Agent tokens (
tokenonPOST /realms/:realmId/agent-tokens) - API keys (
keyonPOST /api-keys)
Every list/detail endpoint that returns the parent resource omits the secret entirely — there is no endpoint, anywhere on this surface, that can hand one back to you a second time. Store it when it's issued, or issue a new one.