LedgerBee Developer
  • Getting started
  • Conventions
  • Products
  • API Reference
Subscriptions
Products & Pricing
Billing documents
WebhooksMCP (AI agents)
Customer Portal
    Portal SSO
    Embedded Checkout
Customer Portal

Portal SSO

Portal SSO lets your end-customers sign in to the LedgerBee customer portal through your own login, and lets your backend provision net-new customers on demand. Your operator configures SSO (OIDC, back-channel handoff, claim mapping) under Portal → Single sign-on and hands you a Partner SSO API key for the Portal SSO endpoints in the API Reference.

The Partner SSO key and its scopes

Partner calls authenticate with the x-api-key header — see Authentication. The tenant is resolved server-side from the key; never put a tenant id in a request body.

ScopeAuthorizes
portal-provisionPOST /v1/portal-sso/provision
portal-sso-mintPOST /v1/portal-sso/handoff/mint, POST /v1/portal-sso/sessions/revoke, and the same-call handoff mint inside provision

These scopes cannot be assigned through the generic API-key or OAuth-client paths (API_KEY_SCOPE_NOT_ASSIGNABLE); the operator mints the key via Generate SSO key on the SSO tab, and the plaintext is shown once.

The tenant must also hold the CustomerPortal license. Without it, every Portal SSO endpoint returns 403 with code: "LICENSE.REQUIRED" and details.licenseKey: "CustomerPortal", regardless of the key's scopes — see Errors.

Provision a customer

POST /v1/portal-sso/provision (scope portal-provision) upserts a customer and grants a credential-less portal user an active membership in one idempotent call, so a net-new end-customer can arrive via SSO. No invitation email is sent.

Identify the customer with exactly one of:

  • customerId — an existing customer, never mutated.
  • customer — an idempotent upsert keyed on customer.customerNumber within your tenant: an existing match is reused as-is; otherwise a customer is created. Required when creating: customerType, customerNumber, name, countryCode, vatZone (lowercase: domestic, eu, abroad, domestic_without_vat), and customerGroupId (list your tenant's groups under the Customers tag in the API Reference).
TerminalCode
curl -X POST 'https://api.ledgerbee.com/api/v1/portal-sso/provision' \ -H 'x-api-key: <your-partner-sso-key>' \ -H 'Content-Type: application/json' \ -d '{ "customer": { "customerType": "BUSINESS", "customerNumber": "ACME-001", "name": "Acme A/S", "countryCode": "DK", "vatZone": "domestic", "customerGroupId": "550e8400-e29b-41d4-a716-446655440099" }, "email": "user@example.com" }'

The top-level email (required) is the portal-user login. It is distinct from customer.email, the customer's contact email, which falls back to it when omitted. The response carries customerId, customerCreated, portalUserId, membershipId, and role. The organization's first member is force-assigned OWNER; re-provisioning reactivates a previously-removed membership.

mintHandoff: true additionally returns handoffRef / handoffExpiresAt for a single round-trip "Go to portal". It requires the portal-sso-mint scope, handoff enabled on the tenant, and sub.

Mint a handoff reference

The back-channel handoff signs a user in without a front-channel JWT: you authenticate the user, mint a single-use opaque ref (the identity stays server-side), and redirect the browser to the portal to redeem it.

POST /v1/portal-sso/handoff/mint (scope portal-sso-mint) responds with { ref, expiresAt }:

TerminalCode
curl -X POST 'https://api.ledgerbee.com/api/v1/portal-sso/handoff/mint' \ -H 'x-api-key: <your-partner-sso-key>' \ -H 'Content-Type: application/json' \ -d '{ "email": "user@example.com", "sub": "your-stable-user-id", "memberships": [{ "customerId": "your-customer-ref", "role": "USER" }] }'
  • email, sub (your stable user id), and memberships are required.
  • At least one membership is required. The portal user, organization, and membership are created at redeem from what you assert; no prior provision call is needed. Each customerId must be a real customer of your tenant.
  • role is one of OWNER, ADMIN, BILLING_ADMIN, USER.
  • When asserting multiple memberships, mark exactly one with "primary": true — the organization the user is signed into. With a single membership the flag is optional.

Redeem the handoff

Return the user's browser to the portal-API host — not api.ledgerbee.com where you minted, and not the portal SPA host (<slug>.portal.…), which serves the static app and ignores the ref:

Code
GET https://<slug>.portal-api.ledgerbee.com/api/auth/sso/handoff/redeem?ref=<ref>&returnTo=<relative-path>

The redeem resolves or creates the portal user, sets the session cookie (1-hour maxAge), and redirects to the portal SPA plus returnTo (same-origin relative paths only). The ref has a 60-second TTL and is consumed atomically. Any failure (missing, expired, or replayed ref; no resolvable membership) redirects to the tenant login screen with ?ssoError=1 rather than a raw error body. Mint a fresh ref.

Revoke sessions (offboarding)

POST /v1/portal-sso/sessions/revoke (scope portal-sso-mint, not portal-provision) with { "sub": "your-stable-user-id" } returns { revoked: <count> }. It immediately invalidates the user's live portal sessions but does not prevent re-login — deprovision the user at your IdP for that. An unknown sub returns 404; revoked: 0 means no active sessions.

OIDC login

OIDC login is the front-channel alternative: the portal redirects the browser to your OpenID Provider (OP), validates the returned ID token, and mints the session. You host the OP and emit the claims the operator configured.

  • Register exactly the redirect URI shown on the operator's SSO tab (https://<slug>.portal-api.<zone>/api/auth/sso/oidc/callback); it is derived from the portal slug and not editable.
  • The ID token must pass: exact iss match, aud equal to the configured client id, unexpired, JWS signature verifiable via your published JWKS, and the nonce minted at start. email falls back to preferred_username; the login fails if both are absent.
  • PKCE S256 is always sent — accept it even if your OP does not advertise code_challenge_methods_supported.
  • LedgerBee always requests openid email profile; operator-configured scopes are additive. If your IdP gates role/customer claims behind a custom scope (Okta's groups, an Entra API scope), the operator must add it — or the membership mapping comes back empty.

Claim mapping

Validated claims map to memberships of { customerId, role } — customerId is your asserted customer id, not a LedgerBee-internal id:

  • Single-org — orgClaimName (customer id) + roleClaimName (role), both string-typed.
  • Multi-org — membershipsClaimName: a JSON array of { customerId, role }. A non-empty array supersedes the single-org pair.
  • An unmapped role or a customerId not in the tenant fails the login closed. With no usable claims, the login succeeds and memberships are untouched. Asserted memberships reconcile on each login — dropped ones are deactivated, not deleted.

JWKS — verifying LedgerBee's client assertions

Under private_key_jwt client auth, your OP fetches LedgerBee's public signing keys from a public, version-neutral endpoint (no /v1 segment):

Code
GET https://api.ledgerbee.com/api/.well-known/portal-sso/<slug>/jwks.json

It returns an RFC 7517 JWK Set keyed by kid, containing the tenant's current signing key plus the previous key during a rotation grace window. An unknown slug returns an empty key set, never a 404. Responses are cached for 5 minutes, so re-fetch on an unknown kid. client_secret_basic is the default and the safe minimum — confirm private_key_jwt with your operator before relying on it.

Errors

You observeCauseFix
403 INSUFFICIENT_PERMISSIONSKey lacks the required scope (reason mint_handoff_requires_scope when mintHandoff: true without portal-sso-mint)Have the operator regenerate the SSO key with the needed capability
403 LICENSE.REQUIREDThe tenant doesn't hold the CustomerPortal license (details.licenseKey: "CustomerPortal")Have your LedgerBee administrator activate the customer-portal module
401 UNAUTHORIZEDInvalid / missing key, or IP not allow-listedSend a valid x-api-key; check allow-listing with the operator
403 "handoff is not enabled"Mint called while the tenant's back-channel toggle is offOperator enables handoff on the SSO tab
400 on mintUnknown role, a customerId outside your tenant, no memberships, or several without exactly one primaryFix the asserted membership set
Browser lands on login with ?ssoError=1Redeem of a missing / expired / used ref, or an OIDC start/callback failureMint a fresh ref; for OIDC, restart the login
Last modified on June 14, 2026
MCP (AI agents)Overview
On this page
  • The Partner SSO key and its scopes
  • Provision a customer
  • Mint a handoff reference
  • Redeem the handoff
  • Revoke sessions (offboarding)
  • OIDC login
    • Claim mapping
    • JWKS — verifying LedgerBee's client assertions
  • Errors