LedgerBee Developer
  • Getting started
  • Conventions
  • Products
  • Advisor firms
  • Configuration
  • API Reference
IntroductionGetting StartedChoose your integrationAuthenticationConnected AppsConnected Apps playgroundAPI KeysLicenses
Getting started

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
GET https://api.ledgerbee.com/auth/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/callback &state=RANDOM_STATE &code_challenge=CODE_CHALLENGE &code_challenge_method=S256 &scope=customers-read%20customers-write

Wrapped for readability; every parameter belongs on one query string, and the space between scope names must be encoded as %20.

ParameterRequiredNotes
response_typeYesMust be code.
client_idYesYour lb_app_client_… id.
redirect_uriYesMust exactly match one of your registered https URIs.
stateRecommendedEchoed back on the redirect. Generate an unguessable value per request and reject a callback whose state doesn't match.
code_challengeYesPKCE, RFC 7636.
code_challenge_methodYesMust be S256; plain is refused.
scopeNoSpace-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

TerminalCode
curl -X POST https://api.ledgerbee.com/api/v1/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTH_CODE" \ -d "code_verifier=CODE_VERIFIER" \ -d "redirect_uri=https://yourapp.com/callback" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=$LEDGERBEE_CLIENT_SECRET"
Code
{ "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "lb_app_refresh_...", "scope": "customers-read customers-write", "holder": { "kind": "company", "id": "0198F2C4-3B1A-7D2E-9C55-6A7B8C9D0E1F", "name": "Example ApS" } }
  • access_token is a JWT valid for 1 hour.
  • refresh_token is valid for 30 days, single-use, and rotates on every exchange.
  • scope reports what was actually granted. Read it back rather than assuming your request was granted verbatim.
  • holder names who the connection belongs to. kind is company, or advisor when an advisor firm connected your app; id is that company's or firm's id and name its 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

TerminalCode
curl -X POST https://api.ledgerbee.com/api/v1/auth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=YOUR_REFRESH_TOKEN" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=$LEDGERBEE_CLIENT_SECRET"

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

TerminalCode
curl https://api.ledgerbee.com/api/v1/customers \ -H "Authorization: Bearer ACCESS_TOKEN"

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:

TerminalCode
curl -X POST https://api.ledgerbee.com/api/v1/oauth/revoke \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=REFRESH_OR_ACCESS_TOKEN" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=$LEDGERBEE_CLIENT_SECRET"

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

ConditionWhat you observeFix
Unknown or revoked client_id, or unregistered redirect_uriError page, no redirectCheck both against your registration
response_type isn't codeerror=unsupported_response_typeSend response_type=code
Missing PKCE, or a method other than S256error=invalid_requestSend PKCE S256
Scope outside your registered set, or an unknown nameerror=invalid_scopeRequest 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 toRequest the family your app is registered for, or email us to add the other
User declineserror=access_deniedNothing to fix
Bad, expired or replayed code, PKCE mismatch, redirect_uri mismatch400 invalid_grantRestart the flow
Wrong client_id / client_secret401 invalid_clientCheck your credentials
Refresh token used more than 30 seconds ago, unknown, or grant revoked400 invalid_grant; a token reused after the window revokes the grantReconnect
Token missing a required scope403, 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 endpoint403, code: "PUBLIC_API_HOLDER_MISMATCH", details: { required, actual }Call the endpoints of the kind you connected; see Choose your integration
Tenant disconnected your app401, 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 deactivated401, error="invalid_token", details.reason grant_revoked or consenting_user_inactive; refresh answers 400 invalid_grantReconnect with a current firm admin

Discovery

Code
GET https://api.ledgerbee.com/api/.well-known/oauth-authorization-server

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.

Last modified on November 6, 2026
AuthenticationAPI Keys
On this page
  • Registration
  • Authorize
  • Consent
  • Token exchange
  • Refresh
  • Using the access token
  • Scopes
  • Revocation
  • Errors
  • Discovery
JSON