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.
| Surface | Used by | Header | Credential |
|---|---|---|---|
| API key | External integrations / backend servers | Authorization: Bearer <raw key> | htst_… (test) or hlive_… (live) |
| Dashboard session | The holdyn-business-frontend (Client #1) | Authorization: Bearer <session JWT> | Obtained from /auth/login or /auth/signup |
| Portal session | Participant browsers | X-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.
curl https://api.holdyn.io/api/v1/b2b/transactions \
-H "Authorization: Bearer htst_abc…"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.
{
"sub": "6432f7…", // B2bAccount id
"email": "ops@acme.inc",
"kind": "dashboard_session", // REQUIRED — exact value
"iat": 1745530200,
"exp": 1746135000 // iat + 7 days
}{
"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 class | TTL | Renewal |
|---|---|---|
| API key | Until revoked | None — re-issue if compromised |
| Dashboard session | 7 days | Re-login; no silent refresh |
| Portal session | 30 minutes | Re-exchange the access token at /portal/session |
| Access token | Customer-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:
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.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | missing_api_key | Authorization: Bearer header absent |
| 401 | invalid_api_key | Key revoked, deleted, or never existed |
| 403 | insufficient_scope | API key lacks the required scope |
| 401 | invalid_dashboard_session | Dashboard JWT invalid or wrong kind |
| 401 | invalid_portal_session | Portal JWT invalid or wrong kind |
| 401 | expired_portal_session | Portal JWT past exp (30 min TTL) |