Docs/Technical API contract

Technical API Contract: Auth, RBAC, and Billing

Contract version 1.0. This is the implementation source of truth for third-party teams integrating with Doorman SDK and APIs.

Scope and version

This contract defines exactly how a third-party app must integrate: authentication, callback processing, tenant bootstrap, subscription checkout, entitlement gating, usage metering, and RBAC enforcement.

  • OIDC issuer baseline: https://doorman.f1cs-dev.it/oauth2.
  • Control API baseline: https://doorman.f1cs-dev.it.
  • Checked OpenAPI contract: services/control-api/contracts/control-api.openapi.json.
  • SDK package baseline: @doorman/react-sdk plus oidc-client-ts .

Non-negotiable guardrails

  1. MUST treat Doorman as the identity and entitlement authority.
  2. MUST unlock paid features only when getSubscriptionStatus(...).is_active === true .
  3. MUST call handleDoormanOidcCallback on callback route.
  4. MUST prefer existing-tenant path: resolveTenantForIdentity -> ensureBillingCustomer before bootstrap.
  5. MUST keep x-doorman-api-key off browser bundles in production (server/agent only).
  6. MUST enforce RBAC from the access token claim for every protected API action in your resource server.
  7. MUST send usage with stable idempotency keys to prevent overcounting.
  8. MUST fail closed on auth/callback/token errors (no partial login state).

Required configuration

Minimum runtime env for SPA/server integration:

OIDC_ISSUER=https://doorman.f1cs-dev.it/oauth2
OIDC_CLIENT_ID=<oauth_client_id>
OIDC_REDIRECT_URI=<app_origin>/callback
OIDC_POST_LOGOUT_REDIRECT_URI=<app_origin>/
OIDC_SCOPE=openid profile email offline_access

KRATOS_PUBLIC_URL=http://localhost:4433
CONTROL_API_URL=https://doorman.f1cs-dev.it

DOORMAN_APP_ID=<app_id>
# Server integrations only; never NEXT_PUBLIC_/VITE_:
DOORMAN_API_KEY=<scoped_dmn_...>

SSO provider callback registered in Google/Microsoft/GitHub: https://doorman.f1cs-dev.it/api/oauth/sso/callback .

SDK surface to use

  • UI-first auth path: DoormanAuthPanel for register/login/direct SSO/domain SSO.
  • Low-level auth path: createDoormanCredentialAuthClient , startDoormanLogin , handleDoormanOidcCallback .
  • SSO routing/admin: createDoormanSsoClient .
  • Billing/runtime: createDoormanBillingClient .
  • Authorization: getDoormanClaimsFromUser , createDoormanPolicyContext , can , requirePolicy , DoormanProvider , RequirePolicy , createDoormanServerContext , and requireServerPolicy .
import {
  DoormanAuthPanel,
  buildDoormanOidcClientSettings,
  createDoormanBillingClient,
  createDoormanServerContext,
  requireServerPolicy,
  RequirePolicy,
  handleDoormanOidcCallback
} from "@doorman/react-sdk";

Auth sequence contract

  1. User starts login/register/SSO from app UI.
  2. App uses SDK auth flow and redirects to Doorman/Hydra authorize. App MUST request the app audience:audience=urn:doorman:app:${APP_ID}.
  3. Provider or credential auth completes, then user returns to app callback URL with code/state.
  4. App callback route calls handleDoormanOidcCallback(...) .
  5. App fetches authenticated user and reads sub claim as identity_id for downstream billing APIs.
const result = await handleDoormanOidcCallback(userManager, {
  pathname: window.location.pathname,
  search: window.location.search,
  callbackPath: "/callback",
  clearUrlPath: "/"
});
if (result.handled && result.errorMessage) throw new Error(result.errorMessage);

Billing sequence contract

  1. Resolve tenant by identity: resolveTenantForIdentity(identity_id=sub) .
  2. If no tenant: bootstrap once with bootstrapTenant(identity_id, tenant_name, email) .
  3. Ensure customer mapping for effective tenant: ensureBillingCustomer(tenant_id) .
  4. Start checkout: createCheckoutSession(tenant_id, plan_id, success_url, cancel_url) .
  5. Redirect user to returned checkout_url.
  6. On success callback, poll getSubscriptionStatus(subscription_id) until active or timeout.
  7. Send usage with meter id via ingestUsageEvent after activation.

SDK to API endpoint matrix

SDK methodHTTPEndpointAuth modeNotes
createDoormanSsoClient.resolveSsoProviderByEmailGET/api/sso/resolve-provider?email=...Browser sessionEnterprise domain-discovery SSO only.
createDoormanCredentialAuthClient.registerWithPasswordGET+POST/self-service/registration/browser and /self-service/registrationKratos browser sessionIn-app credential registration path.
createDoormanCredentialAuthClient.loginWithPasswordGET+POST/self-service/login/browser?refresh=true and /self-service/loginKratos browser sessionRefresh=true allows already-authenticated browser sessions.
startDoormanLoginGETHydra /oauth2/auth via oidc-client-tsBrowser redirectSupports provider-first hints via extraQueryParams. MUST request audience=urn:doorman:app:${APP_ID} for stable aud enforcement.
handleDoormanOidcCallbackGET callbackApp callback route (for example /callback)Browser redirect callbackMust be called exactly once per callback URL.
getDoormanClaimsFromUserlocalOIDC user access_token/id_token/profileValidated JWT claimsDeny-safe claim parser for app id, tenants, roles, permissions, entitlements.
can / requirePolicylocalSDK policy evaluatorParsed Doorman claimsEvaluates generic rules only; app owns product-specific permission meaning.
createDoormanServerContext / requireServerPolicylocalResource server action/API handlerBearer JWT or explicit claimsUse for protected mutations. React gates are not security boundaries.
direct HTTP (app admin)GET+POST/api/apps/:appId/oauth/clientsx-doorman-api-key (server only)Optional: manage OIDC clients programmatically (non-interactive rollout, service agents).
direct HTTP (app admin)GET+POST/api/apps/:appId/service-accountsx-doorman-api-key (server only)Optional: manage service accounts via API key instead of dashboard session cookie.
billing.resolveTenantForIdentityPOST/api/apps/:appId/billing/resolve-tenantx-doorman-api-keyNo Stripe side effects.
billing.bootstrapTenantPOST/api/apps/:appId/billing/bootstrap-tenantx-doorman-api-keyFirst-time identity bootstrap only.
billing.ensureBillingCustomerPOST/api/apps/:appId/billing/ensure-customerx-doorman-api-keyExisting tenant path before checkout.
billing.createCheckoutSessionPOST/api/apps/:appId/billing/checkout-sessionsx-doorman-api-keyCreates/updates subscription and returns checkout_url.
billing.getSubscriptionStatusGET/api/apps/:appId/billing/subscriptions/:subscriptionId/statusx-doorman-api-keyGate entitlements on is_active=true only.
billing.ingestUsageEventPOST/api/apps/:appId/events/ingestx-doorman-api-keyIdempotency key strongly recommended.
billing.createAgentPOST/api/apps/:appId/service-accountsx-doorman-api-keyApp-managed service agent lifecycle.
billing.listAgentsGET/api/apps/:appId/service-accountsx-doorman-api-keyUse for agent inventory and limits.

Full OpenAPI reference:https://doorman.f1cs-dev.it/docs/api/

RBAC token contract

Resource servers must enforce tenant-scoped permissions from the Doorman access-token claim. Consumer apps define permission names and resource rules; Doorman only supplies generic claims and evaluator mechanics.

Audience contract: access tokens MUST include your app audience in the standard aud claim. Required value:urn:doorman:app:${APP_ID}If aud is empty, ensure your authorize request includesaudience=urn:doorman:app:${APP_ID}

{
  "doorman": {
    "v": 1,
    "app_id": "APP_ID",
    "tenants": [
      {
        "tenant_id": "TENANT_ID",
        "roles": ["member", "admin"],
        "permissions": ["resources:read", "resources:write"]
      }
    ]
  }
}
  1. Verify JWT signature and standard claims (iss, aud, exp).
  2. Verify aud contains urn:doorman:app:${APP_ID}.
  3. Verify doorman.app_id matches your app id.
  4. Find tenant block for the requested tenant id.
  5. Require permission presence before executing action.
  6. Deny by default if claim missing or malformed.
const context = createDoormanServerContext({
  authorizationHeader: request.headers.get("authorization"),
  appId: "APP_ID",
  activeTenantId: tenantId
});

requireServerPolicy(context, {
  type: "permission",
  permission: "resources:write"
});

Client helpers such as DoormanProvider and RequirePolicy only hide or show UI. Server-side enforcement is mandatory for protected actions.

Service agent contract

Agent-like entities must be created as Doorman billing agents/service accounts and tied to tenant + role_ids.

  • Create: createAgent(name, tenant_id, role_ids?) .
  • Rotate credentials: rotateAgentSecret(agent_id) .
  • Disable or revoke: deactivateAgent and revokeAgent .
  • Enforce caps from plan capacity metrics: service_accounts and member_seats .

Error handling contract

All billing API failures surface as DoormanApiError with status , code , and raw details .

try {
  await billing.createCheckoutSession({ tenant_id, plan_id, success_url, cancel_url });
} catch (error) {
  if (error instanceof DoormanApiError) {
    // Handle by error.code, not message text.
    console.error(error.status, error.code, error.details);
  }
}
  • Treat these as operator-mapping errors: missing_provider_plan_base_component_mapping , missing_provider_customer_mapping .
  • Treat these as limits/business errors: tenant_app_member_cap_exceeded , service_account_cap_exceeded .
  • Treat auth callback stale PKCE as retryable and restart login from app root.

Conformance tests

A third-party implementation is compliant only if all tests pass:

  1. Password register/login and Google SSO both end with authenticated app session and no callback loop.
  2. Callback route is idempotent (reload callback URL does not break auth).
  3. Checkout creates pending subscription, then flips to active, and app unlocks only after active.
  4. Usage ingestion produces Doorman usage/event records and Stripe export after worker sync.
  5. Protected API endpoints reject missing/invalid permission claims.
  6. Agent creation obeys plan cap and returns deterministic cap-exceeded errors when limit reached.
Handoff rule: do not start production implementation until this contract is implemented exactly and verified against Doorman test env.