Skip to content

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.

Keep the two sides straight: a realm is the QuickBooks side — one QuickBooks company file, reached by an agent token or a Web Connector connection. An OAuth client or API key is your app's side — the credential that says who is calling. The client and the key are each scoped to your whole organization, not to any one realm — but an OAuth access token, once minted through the authorize flow, is bound to the single realm you authorized it for; run the flow again to get a token for another realm. The realm id in a /v3/company/{realmId}/... URL picks the company file; the credential in the request proves the caller.

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:

  1. An API key, in the x-api-key header. Minted by POST /admin/v1/api-keys (session only — see Things that are shown once), prefixed tkb_, scoped to exactly one organization for its whole life.
  2. A session cookie — what the portal sends, issued by signing in at /admin/v1/auth.

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:

RoleCan do
ownerEverything, including deleting a realm
adminCreate/revoke realms, clients, and agent tokens; invite members; issue/revoke API keys
memberRead-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. Each row includes connected: true once the realm has a live, non-revoked Web Connector credential that has actually polled — the same "connected" a company file's row shows in the portal — computed in this one query, not a per-realm follow-up read.

json
{
  "realms": [
    {
      "id": "9130351234567890",
      "name": "Acme Plumbing",
      "orgId": "org_abc123",
      "createdAt": "2026-08-10T00:00:00.000Z",
      "connected": true
    }
  ]
}

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"
  },
  "settings": { "departmentsFromClasses": false },
  "agent": { "online": true },
  "agentTokens": [
    {
      "id": "tok_1",
      "label": "front office",
      "createdAt": "2026-08-10T00:00:00.000Z",
      "lastUsedAt": null,
      "revoked": false,
      "revokedAt": null
    }
  ]
}

PATCH /admin/v1/realms/:realmId/settings (admin) — change a realm's settings. Today there is one: departmentsFromClasses (boolean, default false), which serves the QuickBooks Online Department API as QuickBooks classes. See Departments. The change reaches every gateway machine within about 15 seconds. It works on a sandbox realm too.

bash
curl -s -X PATCH https://api.tenkeybridge.com/admin/v1/realms/9130351234567890/settings \
  -H "x-api-key: $TKB_API_KEY" -H "content-type: application/json" \
  -d '{"departmentsFromClasses": true}'
json
{ "settings": { "departmentsFromClasses": true } }

POST /admin/v1/realms/:realmId/agent-tokens (admin, optional { "label": "..." } body) — issue an agent token for that realm's Windows agent (appsettings.json). Agent tokens are not needed for Web Connector, the connection every customer uses — see Connect QuickBooks; the agent itself is not currently offered to new customers.

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.

DELETE /admin/v1/realms/:realmId (owner — one rank above every other realm route) — permanently delete a realm. 204 on success.

bash
curl -s -X DELETE https://api.tenkeybridge.com/admin/v1/realms/9130351234567890 \
  -H "x-api-key: $TKB_API_KEY"

An admin or member gets 403 forbidden ("Only organization owners can delete a company file."); an unknown realm, or one belonging to another org, gets the same 404 every other realm route uses — no existence oracle.

This deletes the realm's OAuth access/refresh tokens, authorization codes, and agent tokens in the same transaction, then — once that transaction has committed — closes the connected edge agent's WebSocket, if one is dialed in, with close code 4003 and reason "realm deleted". Every app and every agent connected to that company loses access immediately and permanently; there is no undo.

Spec reversal: the original design spec (§6 P2) said realms are not deletable — a QuickBooks company is real-world state (invoices, customers, years of history) that outlives any particular integration, so "delete a company" had no safe meaning. That call is now overridden: an organization owner may delete a realm, with the full cascade above. The QuickBooks company file itself is untouched — this only removes TenkeyBridge's connection to it.

Sandbox ​

Each organization has at most one sandbox realm (see Sandbox). Realm objects carry "kind": "sandbox" or "kind": "production", and GET /admin/v1/realms/:realmId on a sandbox adds a sandbox object with today's usage and the limits (dailyCalls, maxRecords).

POST /admin/v1/realms/:realmId/sandbox/reset (member) — delete everything in the sandbox and reload a template. template is "sample" or "empty".

bash
curl -s -X POST https://api.tenkeybridge.com/admin/v1/realms/9130351234567891/sandbox/reset \
  -H "x-api-key: $TKB_API_KEY" -H "content-type: application/json" \
  -d '{"template": "sample"}'
json
{ "template": "sample", "records": 477, "nextSeq": 865 }

A reset costs 50 calls toward the sandbox's daily limit; 429 sandbox_daily_limit (with Retry-After) when they're used up; 409 production_realm for a real realm.

POST /admin/v1/realms/sandbox (admin) — re-create the organization's sandbox. Only needed if the sandbox was deleted.

bash
curl -s -X POST https://api.tenkeybridge.com/admin/v1/realms/sandbox \
  -H "x-api-key: $TKB_API_KEY" -H "content-type: application/json" \
  -d '{"orgId": "org_abc123"}'

Returns 201 with { "realm": … }; 409 sandbox_exists if the organization already has one. Sandboxes are free and never touch billing.

A sandbox realm can't have an agent token or a Web Connector connection: those calls return 409 sandbox_realm.

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.

Billing ​

See the billing guide for the plans, metering, and grace-period rules these endpoints expose. billing is only present when the gateway has Stripe configured — see Gateway ops — but GET always works, since entitlement is a plan-rule computation independent of Stripe.

GET /admin/v1/orgs/:orgId/billing (member) — plan, entitlement, and 30-day usage for the org.

bash
curl -s https://api.tenkeybridge.com/admin/v1/orgs/org_abc123/billing \
  -H "x-api-key: $TKB_API_KEY"
json
{
  "plan": "trial",
  "status": "none",
  "entitled": true,
  "reason": "trial",
  "realmCount": 1,
  "realmLimit": 1,
  "quantity": 0,
  "currentPeriodEnd": null,
  "graceUntil": null,
  "usage30d": { "requests": 128, "errors": 0, "byDay": [{ "day": "2026-08-27", "requests": 128, "errors": 0 }] },
  "billingEnabled": true
}

POST /admin/v1/orgs/:orgId/billing/checkout (admin) — start a Stripe Checkout session for the org's Production subscription; response is { "url": "https://checkout.stripe.com/..." } to redirect the browser to. 503 billing_disabled when the gateway has no Stripe configuration; 409 enterprise_plan when the org is already on the Enterprise plan (managed off-platform — see Enterprise).

POST /admin/v1/orgs/:orgId/billing/portal-session (admin) — open the Stripe Billing Portal for the org's existing Stripe customer (update payment method, change plan, cancel, view invoices); response is the same { "url": "..." } shape. 409 no_customer when the org has never started checkout, so there's no Stripe customer to open a portal session for.

Creating a realm on a trial org that already has one connected returns 409 plan_limit_realms from POST /admin/v1/realms (not a /billing sub-route) — see Admin API errors.

Errors ​

Every error on this surface is the same two-field shape (a specific code may add extra fields, like plan_limit_realms's checkoutPath below — always additive, never in place of error/message):

json
{ "error": "unauthenticated", "message": "..." }
errorHTTP statusMeaning
unauthenticated401No session and no valid x-api-key.
forbidden403Authenticated, but your role in this org is too low for the action.
invalid_request400Malformed body, missing required field, or a redirect URI that fails validation.
not_found404No such resource — or a resource that belongs to an organization you're not a member of.
rate_limited429An API key crossed 120 requests/minute.
plan_limit_realms409(#92) The org is on the trial plan and already has its one allowed realm; body carries a checkoutPath to start a subscription. See billing.
billing_disabled503(#92) Billing checkout/portal-session called on a gateway with no Stripe configuration.
enterprise_plan409(#92) Checkout called for an org already on the Enterprise plan — managed off-platform.
no_customer409(#92) Billing-portal-session called for an org with no Stripe customer yet — start checkout first.
internal_error500Unexpected 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 (clientSecret on POST /clients)
  • Agent tokens (token on POST /realms/:realmId/agent-tokens)
  • API keys (key on POST /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.

TenkeyBridge is an independent product, not affiliated with, endorsed by, or sponsored by Intuit Inc. QuickBooks, QuickBooks Online, and QuickBooks Desktop are trademarks of Intuit Inc., used only to describe compatibility.