Connected Apps
A connected app is an OAuth 2.1 client LedgerBee registers for your product, so it can act on a company's data with that company's consent. Register once, then run the authorization-code flow for every company that connects.
The Connected Apps playground runs this flow in your browser, before or after registration, and composes the registration email for the setup you tried. A temporary client connects a company; to connect an advisor firm, use the credentials of an app LedgerBee registered for advisor firms.
Registration
Registration is handled by LedgerBee, not self-service. Email development@ledgerbee.com with:
- your app's display name
- one or more https redirect URIs
- the scopes your integration needs
- a logo URL (optional)
You get back a client_id (lb_app_client_…) and a client_secret
(lb_app_secret_…). The secret is shown once and cannot be retrieved, so store
it before you close the message.
Email the same address to change any of this later, or to rotate the secret. A rotation keeps the previous secret working for 24 hours, so you can roll credentials without downtime.
Authorize
Send the company's user to:
Code
Wrapped for readability; every parameter belongs on one query string, and the
space between scope names must be encoded as %20.
| Parameter | Required | Notes |
|---|---|---|
response_type | Yes | Must be code. |
client_id | Yes | Your lb_app_client_… id. |
redirect_uri | Yes | Must exactly match one of your registered https URIs. |
state | Recommended | Echoed back on the redirect. Generate an unguessable value per request and reject a callback whose state doesn't match. |
code_challenge | Yes | PKCE, RFC 7636. |
code_challenge_method | Yes | Must be S256; plain is refused. |
scope | No | Space-separated, and a subset of your registered scopes. Omit to request all of them. |
A signed-out user is sent to login and returned here afterward.
An app LedgerBee registered for advisor firms as well as companies may request
scopes of both families in one request
(scope=customers-read%20advisor-clients-read). The user approving chooses
whether to connect a company or their advisor firm, and the grant holds only
that family's scopes. A scope outside your registered set is refused, as
below. Of the registered scopes, those of a family your app is not registered
to connect for are dropped; when none remain, the redirect carries
error=invalid_scope. See Consent for a firm.
Consent
The user picks the company to connect if they belong to more than one, then approves or denies the requested scopes as a fixed list. Approval grants every requested scope of the kind the user connects (a company's scopes for a company, a firm's for a firm); there is no partial grant beyond that, unless LedgerBee registered your app to let the user untick scopes. A company holds one live grant per app, so reconnecting refreshes it rather than adding a second.
When the request names advisor firm scopes as well and the user can connect either a company or a firm, the screen first asks What do you want to connect?, with the choices A company and Your advisor firm. Choosing the firm lists the firms the user administers; only a firm admin can approve a connection for a firm.
Approval redirects to your redirect_uri with code and state; denial
redirects with error=access_denied.
Token exchange
Code
Code
access_tokenis a JWT valid for 1 hour.refresh_tokenis valid for 30 days, single-use, and rotates on every exchange.scopereports what was actually granted. Read it back rather than assuming your request was granted verbatim.holdernames who the connection belongs to.kindiscompany, oradvisorwhen an advisor firm connected your app;idis that company's or firm's id andnameits current name. It is in every exchange and every refresh response, so a renamed company shows its new name on the next refresh. Read it from the response: the access token is opaque to your app, and the claims inside it are not a contract.
Authenticate with client_secret_post as above, or client_secret_basic, an
Authorization: Basic header carrying client_id:client_secret. The examples
read the secret from an environment variable; curl still expands that into its
argument list, so on a shared machine use your own HTTP client instead.
This endpoint and /oauth/revoke take a form-urlencoded body
(RFC 6749 /
RFC 7009), so they are
documented here rather than in the API Reference, which covers the JSON
REST endpoints.
Refresh
Code
The 30 days are counted per token: each replacement starts its own window, so refreshing regularly keeps you connected indefinitely while a leaked token still dies 30 days after issue. A connection ends when the tenant revokes it or you go 30 days without refreshing.
Serialize refresh calls per grant across your workers. Within 30 seconds of a rotation, the same client presenting the rotated-out token again receives the response that rotation produced, so a concurrent duplicate or a retry after a lost response lands on the one live token. After that window, presenting an already-used refresh token is treated as a compromised token family and revokes the whole grant.
Using the access token
Code
See the API Reference for the available endpoints.
Scopes
The scope you request must be a subset of the scopes registered for your app.
Three are never grantable to a connected app: checkout, portal-sso-mint,
and portal-provision, which are API-key-only provisioning credentials. Legacy
vendors-read / vendors-write are accepted at /authorize and normalized to
suppliers-read / suppliers-write.
Advisor firm scopes (advisor-clients-read, advisor-authority-extracts-write)
are granted only to a firm: a company consent never carries them, and an API
key cannot hold them. See Advisor firms.
Revocation
Users disconnect your app from their Connected apps page at any time. Your app can also revoke a token directly:
Code
The response is 200 with an empty body whether the token was valid, already
revoked, or unknown. Only a client-authentication failure returns 401.
Errors
| Condition | What you observe | Fix |
|---|---|---|
Unknown or revoked client_id, or unregistered redirect_uri | Error page, no redirect | Check both against your registration |
response_type isn't code | error=unsupported_response_type | Send response_type=code |
Missing PKCE, or a method other than S256 | error=invalid_request | Send PKCE S256 |
| Scope outside your registered set, or an unknown name | error=invalid_scope | Request only registered scopes, or email us to add one |
| Every requested scope is registered for your app but belongs to a family it is not registered to connect for (company or advisor firm) | error=invalid_scope, error_description starting This app is not registered to connect for the kind of holder these scopes belong to | Request the family your app is registered for, or email us to add the other |
| User declines | error=access_denied | Nothing to fix |
Bad, expired or replayed code, PKCE mismatch, redirect_uri mismatch | 400 invalid_grant | Restart the flow |
Wrong client_id / client_secret | 401 invalid_client | Check your credentials |
| Refresh token used more than 30 seconds ago, unknown, or grant revoked | 400 invalid_grant; a token reused after the window revokes the grant | Reconnect |
| Token missing a required scope | 403, error="insufficient_scope" | Reconnect with the broader scope |
| A company's token on an advisor firm endpoint, or a firm's token on a company endpoint | 403, code: "PUBLIC_API_HOLDER_MISMATCH", details: { required, actual } | Call the endpoints of the kind you connected; see Choose your integration |
| Tenant disconnected your app | 401, error="invalid_token", details.reason: "grant_revoked" | Reconnect |
| The firm admin who approved a firm connection no longer holds the seat, or the firm was deactivated | 401, error="invalid_token", details.reason grant_revoked or consenting_user_inactive; refresh answers 400 invalid_grant | Reconnect with a current firm admin |
Discovery
Code
Returns the OAuth 2.1 metadata: authorization_endpoint, token_endpoint,
revocation_endpoint, jwks_uri, supported grant types and scopes. The
API Reference carries the full schema, including the JWKS and
protected-resource variants.