Skip to content

Authentication ​

TenkeyBridge clones the QuickBooks Online OAuth 2.0 authorization-code flow. If your app already connects to QBO, you keep your OAuth code and swap two URLs.

QuickBooks OnlineTenkeyBridge
Authorize URLhttps://appcenter.intuit.com/connect/oauth2https://api.tenkeybridge.com/oauth2/v1/authorize
Token URLhttps://oauth.platform.intuit.com/oauth2/v1/tokens/bearerhttps://api.tenkeybridge.com/oauth2/v1/tokens

Getting credentials ​

Credentials are self-serve through the Admin API:

  • Register an OAuth client — POST /admin/v1/clients with your app's name and redirect URIs. Returns a client_id and a client_secret, shown once.
  • Provision a realm per customer — POST /admin/v1/realms, one per QuickBooks company you connect. Returns a realmId — you already hold this by the time you send anyone to the authorize URL, because you're the one who created it.

Both live under one organization; see Organizations and roles for who on your team can do what.

Self-serve

Sign in at https://api.tenkeybridge.com/portal with a magic link, create an organization, and register your OAuth client there. Provisioning clients and realms through /admin/v1 works exactly as described above either way — the portal is a UI over the same endpoints.

1. Send the user to the authorize URL ​

text
https://api.tenkeybridge.com/oauth2/v1/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://yourapp.example.com/callback
  &state=RANDOM_OPAQUE_STRING
  &scope=com.intuit.quickbooks.accounting
  &realm_id=THE_REALM_ID
  • response_type must be code (anything else is rejected with unsupported_response_type, HTTP 400).
  • redirect_uri must exactly match one of your registered redirect URIs.
  • scope is accepted for QBO compatibility and defaults to com.intuit.quickbooks.accounting.
  • realm_id is required: the realmId of the QuickBooks company being connected. You already hold it — you're the one who created the realm through the Admin API — so this is the one parameter Intuit's authorize URL doesn't have.

The consent page names your app and the company (realm_id resolves server-side to a company name) and has Connect and Cancel. On Connect the browser is redirected to your redirect_uri with code, your state, and realmId in the query string — the same shape Intuit sends. On Cancel it is redirected to your redirect_uri with error=access_denied and your state (RFC 6749 §4.1.2.1), so handle that the way you already handle a user declining Intuit's consent screen.

The server enforces that realm_id belongs to the same organization that owns client_id, on both the GET that renders this page and the POST it submits to — independently, so a value that only made it through a browser round trip is never trusted on its own. With that ownership check server-side, this page is honest about what it is: it preserves the QBO consent-screen shape so your existing OAuth code keeps working unmodified, but the click itself isn't the access control — the same-org check is. A realm_id that doesn't exist, or belongs to a different organization than the client, answers with the identical error either way (see below), so the page can't be used to probe which realm ids exist.

2. Exchange the code for tokens ​

POST /oauth2/v1/tokens with HTTP Basic auth (client_id:client_secret) and a form body:

bash
curl -s https://api.tenkeybridge.com/oauth2/v1/tokens \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code="$CODE" \
  -d redirect_uri=https://yourapp.example.com/callback

redirect_uri must match the one the code was issued against. A mismatch returns invalid_grant and consumes the code — restart from step 1.

Response:

json
{
  "token_type": "bearer",
  "access_token": "…",
  "expires_in": 3600,
  "refresh_token": "…",
  "x_refresh_token_expires_in": 8640000,
  "realmId": "1234567890"
}

Access tokens last 1 hour; refresh tokens last 100 days.

3. Refresh ​

Same endpoint, same Basic auth:

bash
curl -s https://api.tenkeybridge.com/oauth2/v1/tokens \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token="$REFRESH_TOKEN"

Refresh tokens rotate: each refresh returns a new pair. Persist the new access_token and refresh_token together before using either.

Rotation is soft, matching Intuit: the previous refresh token stays usable for about 5 minutes after rotation. This exists for apps that refresh concurrently (a scheduled sync job racing a user-triggered one, say) without a refresh mutex — the race's "loser" isn't punished. Presenting the previous token within that window returns a fresh, independently valid pair (not the same one the winner got); both the winner's and the loser's pairs keep working. Presenting it again beyond the window returns invalid_grant. Revoking any token in the chain (see below) kills refresh for the whole connection immediately, window or not.

4. Call the API ​

bash
curl -s "https://api.tenkeybridge.com/v3/company/$REALM_ID/query" \
  --get --data-urlencode "query=select * from Customer maxresults 5" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json"

Same paths and JSON as QBO — see the Quickstart and the compatibility contract.

Revoking ​

POST /oauth2/v1/revoke with Basic auth and token=<access-or-refresh-token>. Idempotent — always returns 200 (RFC 7009). Revoking any token for a connection kills refresh for that whole connection right away: a still-in-window previous refresh token (see rotation, above) stops working too. A disconnect means disconnected.

Errors ​

SymptomMeaning
400 unsupported_response_type on authorizeresponse_type wasn't code
400 "invalid client_id or redirect_uri"Unknown client, or redirect_uri not registered
400 "realm_id is required" on authorizerealm_id missing from the authorize URL or consent form
400 "invalid realm_id" on authorizeThe realm doesn't exist, or belongs to a different organization than the client — same error either way
401 invalid_client on /tokensBasic auth header wrong (client ID/secret)
400 invalid_grant on /tokensCode expired/consumed, redirect_uri mismatch, or a refresh token reused past its ~5-minute grace window (or after revocation)

API-level faults use QBO's Fault shape — see Error codes.

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.