API reference/Authentication

Authentication

Authenticate your requests

The Holdyn B2B API has three distinct authentication surfaces, each scoped to a different class of caller. A token issued for one surface cannot be used against another — cross-class use is rejected at the middleware layer with HTTP 401.

SurfaceUsed byHeaderCredential
API keyExternal integrations / backend serversAuthorization: Bearer <raw key>htst_… (test) or hlive_… (live)
Dashboard sessionThe holdyn-business-frontend (Client #1)Authorization: Bearer <session JWT>Obtained from /auth/login or /auth/signup
Portal sessionParticipant browsersX-Portal-Session: <session JWT>Obtained from /portal/session via X-Access-Token

API keys

API keys are generated with at least 256 bits of entropy and stored hashed at rest. The raw key is shown once at creation; subsequent list calls return only the prefix and last four characters.

Prefixes identify the environment: htst_ (test mode) or hlive_ (live mode). Scopes are enforced per endpoint — see each resource's reference.

Authenticated requestbash
curl https://api.holdyn.io/api/v1/b2b/transactions \
  -H "Authorization: Bearer htst_abc…"
Treat keys like production secrets. Raw keys are shown once. Store them in a secret manager and rotate on any suspicion of leakage — there is no recovery endpoint for lost keys.

Dashboard sessions

A dashboard session is a short-lived JWT minted by POST /auth/signup or POST /auth/login. Send it as an HTTP bearer token on every dashboard-gated call.

JWT structure

Both the dashboard session and the portal session carry a kind claim identifying the class of token. Middlewares reject any token whose kind does not match the class they protect — a dashboard JWT cannot authorize against the portal, and vice versa.

Dashboard session payloadjson
{
  "sub":   "6432f7…",            // B2bAccount id
  "email": "ops@acme.inc",
  "kind":  "dashboard_session",  // REQUIRED — exact value
  "iat":   1745530200,
  "exp":   1746135000             // iat + 7 days
}
Portal session payloadjson
{
  "sub":   "6432fa…",            // B2bAccessToken id
  "tid":   "6432fb…",            // B2bTransaction internal id
  "role":  "payer",              // "payer" | "beneficiary" | "viewer"
  "perms": ["view_transaction", "fund_transaction"],
  "kind":  "portal_session",     // REQUIRED — exact value
  "iat":   1745530200,
  "exp":   1745532000             // iat + 30 minutes
}

Algorithm is HS256. Signing secrets are separate per class (B2B_DASHBOARD_SESSION_SECRET and B2B_PORTAL_SESSION_SECRET) so even a token forged against one secret will not verify against the other — the kind check is belt-and-braces defense against secret-collision misconfiguration.

Token lifetimes

Token classTTLRenewal
API keyUntil revokedNone — re-issue if compromised
Dashboard session7 daysRe-login; no silent refresh
Portal session30 minutesRe-exchange the access token at /portal/session
Access tokenCustomer-controlled (default 7d, max 30d)Mint a new one via /transactions/:id/access-tokens

Portal session exchange

Participant browsers begin with an X-Access-Token header and exchange it for a portal session JWT:

Exchange access token → sessionbash
curl -X POST https://api.holdyn.io/api/v1/b2b/portal/session \
  -H "X-Access-Token: htpk_live_…"

# → { "session_token": "eyJ…", "expires_at": "…", "role": "…",
#     "permissions": [...], "transaction": { … } }

Subsequent portal requests send X-Portal-Session (or Authorization: Bearer) with the returned JWT.

Common auth errors

Every authentication failure returns a 401 with a structured error body. See Errors for the full code list.

HTTPCodeMeaning
401missing_api_keyAuthorization: Bearer header absent
401invalid_api_keyKey revoked, deleted, or never existed
403insufficient_scopeAPI key lacks the required scope
401invalid_dashboard_sessionDashboard JWT invalid or wrong kind
401invalid_portal_sessionPortal JWT invalid or wrong kind
401expired_portal_sessionPortal JWT past exp (30 min TTL)