Authentication
DIDHub APIs use two authentication mechanisms depending on how you're calling.
| Mechanism | Use it for | Where it goes |
|---|---|---|
| Session cookie | The DIDHub dashboard SPA (browser sessions for human users) | didhub_sid cookie, set by /v1/auth/login |
| Bearer API key | Server-to-server calls from your backend to DIDHub | Authorization: Bearer didhub_live_… header |
For most integrations you want a Bearer API key. The session cookie is intended for the official dashboard at https://dashboard.didhub.io.
API keys
Sign in to the dashboard and go to Settings → API keys → Create key.
Each key has:
- Name — what you'll see it as in the list (free-text label, e.g.
Production webhook signer) - Scopes — currently all keys are scope
*(full access); granular scopes are on the roadmap - Expiry — optional; DIDHub will email a reminder 14 days before the date
- Prefix display — the visible portion (
didhub_live_UQcs…) so you can identify a key without seeing the secret
The raw key is shown once at creation time. Store it immediately — there's no way to retrieve it later. If you lose it, revoke it and create a new one.
Authorization: Bearer didhub_live_UQcsmrT6I5FS18Z6xwgHYjVYq37X43g6WatYHVrL3k8Treat API keys like passwords
WARNING
- ✅ Store in a secrets manager (1Password, Vault, AWS/GCP Secrets Manager)
- ✅ Rotate periodically — set an
expires_atwhen creating a key - ✅ Use a separate key per application or environment so revocation is targeted
- ❌ Never commit a key to git
- ❌ Never paste a key into a chat / ticket / screenshot
- ❌ Never put a key in a URL query string (it ends up in server logs)
Beta
Programmatic API keys are currently in beta. The issuance + revocation endpoints work today, and we're staging the request-authentication middleware for general availability. Contact support@didhub.io to enable API keys for your account.
Revoking a compromised key
Either:
- Dashboard: Settings → API keys → click the key → Revoke. Effect is immediate.
- API:
DELETE /v1/api-keys/{id}with a still-active key or session cookie.
OAuth 2.0 — authorizing a third-party app
If you're building an application that DIDHub users will sign into with their own DIDHub account — a Zapier integration, a desktop client, a multi-tenant SaaS — use OAuth, not API keys. DIDHub acts as the OAuth 2.0 Authorization Server; your app is the client.
The flow is Authorization Code with PKCE (RFC 6749 + 7636). PKCE is mandatory — there is no implicit-grant path, no code_challenge_method=plain, no skipping code_challenge. This applies to confidential clients too.
1. Register your app
Sign in to the dashboard and go to Settings → OAuth apps → Register OAuth app. You'll provide:
- Name — shown to users on the consent screen.
- Description / homepage URL / logo URL — also shown on the consent screen. Optional but recommended; users are far more likely to authorize an app they recognize.
- Redirect URIs — one per line. Must match exactly at
/oauth2/authorize.https://only, excepthttp://localhost/http://127.0.0.1(any port) for local development. Custom schemes likecom.example.app://oauthare allowed for native apps. - Scopes — the maximum set of scopes the app is allowed to request. Users may further narrow this on the consent screen.
On submit you receive a client_id and a client_secret. The secret is shown once — store it immediately in your secrets backend. If you lose it, click Rotate secret to mint a new one (the previous secret is rejected immediately).
2. Send the user to /oauth2/authorize
Generate a PKCE pair (code_verifier is a 43–128 character random string; code_challenge is BASE64URL(SHA256(code_verifier))), then redirect the user's browser to:
https://api.didhub.io/oauth2/authorize?
response_type=code
&client_id=didhub_oauth_…
&redirect_uri=https://app.example.com/oauth/callback
&scope=numbers:read%20cdrs:read
&state=<random-csrf-token>
&code_challenge=<base64url-sha256-of-verifier>
&code_challenge_method=S256state is a random string you generate and verify on the callback — protects against CSRF, per RFC 6749 §10.12.
The user lands on DIDHub's consent screen at dashboard.didhub.io/oauth-consent. If they're not logged in, they sign in first. They click Authorize, and DIDHub redirects back to your redirect_uri:
https://app.example.com/oauth/callback?code=<one-shot-code>&state=<your-state>If they click Cancel, you receive ?error=access_denied&error_description=…&state=….
3. Exchange the code for tokens
Server-side, POST to /oauth2/token with Content-Type: application/x-www-form-urlencoded:
POST /oauth2/token HTTP/1.1
Host: api.didhub.io
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<one-shot-code-from-callback>
&redirect_uri=https://app.example.com/oauth/callback
&client_id=didhub_oauth_…
&client_secret=didhub_oauths_…
&code_verifier=<the-original-pkce-verifier>Client credentials can also go in an Authorization: Basic header (base64(client_id:client_secret)); see RFC 6749 §2.3.1.
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiJ9...",
"refresh_token": "didhub_oauthr_…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "numbers:read cdrs:read"
}- The access token is a JWT, 1 hour lifetime by default. Send it as
Authorization: Bearer eyJ…to any/v1/*endpoint that the granted scopes cover. It is stateless —/v1/*verifies it without a DB hit (other than checking the refresh-row hasn't been revoked, which is cached). - The refresh token is opaque. Store it securely server-side. Use it to mint a new access token before the current one expires.
Codes are single-use. Replaying a code revokes the access + refresh tokens it originally minted (RFC 6749 §10.5) — your integration is then broken until the user re-authorizes.
4. Refresh before the token expires
POST /oauth2/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=didhub_oauthr_…
&client_id=didhub_oauth_…
&client_secret=didhub_oauths_…Returns a fresh access_token and a new refresh_token — DIDHub rotates refresh tokens on every use (RFC 6749 §10.4 best practice). Save the new refresh token and discard the old one. If you ever present the old (rotated) refresh token again, DIDHub detects the replay and revokes the entire token chain for that grant; the user has to re-authorize.
You can narrow scope on refresh (&scope=numbers:read) but you cannot widen it — widening requires a fresh /oauth2/authorize.
5. Revoke a token
POST /oauth2/revoke HTTP/1.1
Content-Type: application/x-www-form-urlencoded
token=didhub_oauthr_…
&client_id=didhub_oauth_…
&client_secret=didhub_oauths_…Per RFC 7009, this endpoint always returns 200 (so attackers can't probe for valid tokens by status code). Only refresh tokens can be revoked here — access tokens are stateless JWTs that expire naturally within ~1 hour.
Users can also revoke from their dashboard at Settings → OAuth apps → Connected applications → Disconnect. That revokes the consent grant + all live refresh tokens for that user/app pair.
Discovery (.well-known)
If your OAuth library auto-configures from a discovery URL, point it at:
https://api.didhub.io/.well-known/oauth-authorization-serverThat returns the RFC 8414 metadata: endpoint URLs, supported grant/response types, supported scopes, supported PKCE methods.
OAuth scopes
| Scope | What it grants |
|---|---|
account:read | Read your DIDHub account (name, email, country, settings) |
account:write | Update your DIDHub account |
numbers:read | List phone numbers, status, routing |
numbers:write | Order, route, attach compliance, release numbers |
sip_trunks:read / sip_trunks:write | Read / manage SIP trunks + gateways |
routing:read / routing:write | Read / manage call-routing profiles |
cdrs:read | List Call Detail Records |
billing:read | Read balance, transactions, saved payment methods |
compliance:read / compliance:write | Read / manage end-users, documents, bundles |
:write scopes implicitly grant :read for the same resource — granting numbers:write is enough to call GET /v1/numbers.
Intentionally not OAuth-grantable: billing:write (moving money — too sensitive for OAuth; use a session cookie or hand-issued API key) and oauth:apps (managing OAuth apps via OAuth would be a privilege-escalation loop).
When to use OAuth vs API keys
| You're building… | Use |
|---|---|
| Your own backend talking to your own DIDHub account | API key |
| An integration where each end-user logs in with their own DIDHub account | OAuth |
| A multi-tenant SaaS that resells DIDHub to its customers | OAuth, one app per integration |
| A CLI / dashboard for your team | API key (or session cookie if it's a browser tool) |
API keys never expire on their own, can't be granularly scoped per-user, and don't give the user a "Disconnect this app" UI. They're the right tool when you are the customer; OAuth is the right tool when your users are the customer.
Errors
A missing or invalid token returns 401 Unauthorized with the standard error envelope:
{
"ok": false,
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key"
}
}A valid token without the required scope returns 403 Forbidden:
{
"ok": false,
"error": {
"code": "forbidden",
"message": "API key lacks scope: numbers:write"
}
}Validation errors return 422 Unprocessable Entity with a fields object showing per-field problems:
{
"ok": false,
"error": {
"code": "invalid_input",
"message": "Invalid input",
"fields": { "country": "Required, 2-letter ISO code" }
}
}See the interactive API explorer for per-endpoint details.