OAuth apps (client credentials)
Alongside static API keys, Permissio supports OAuth 2.0 client-credentials — the right fit when you're building a platform or reselling on top of Permissio and want per-app credentials and short-lived, scoped tokens instead of a single long-lived shared key.
The flow is standard RFC 6749 client credentials:
- Register an app → you get a
client_idand aclient_secret. - Exchange those at
POST /oauth/tokenfor a short-lived access token. - Call the API with
Authorization: Bearer <access_token>— exactly like an API key.
1. Register an app
In the dashboard go to Settings → Developers → OAuth apps and create an app.
Choose its environment (live / sandbox) and the scopes it may request (the
same scope catalog as API keys — envelopes:read, envelopes:write,
templates:read, templates:write, webhooks:read, webhooks:write,
evidence:read).
You'll be shown the client_secret once — store it securely. If it leaks,
rotate it (a new secret is issued and the old one stops working immediately).
2. Exchange for an access token
Send the credentials to the token endpoint — either as form fields or via HTTP
Basic auth. Optionally narrow the token to a subset of the app's scopes with
scope (space-delimited); omit it to receive all the app's scopes.
curl https://api.permissio.us/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=oacl_1a2b3c4d5e6f7a8b" \
-d "client_secret=csec_…" \
-d "scope=envelopes:read envelopes:write"
{
"access_token": "at_9f8e7d6c5b4a…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "envelopes:read envelopes:write"
}
Access tokens are valid for 1 hour — request a fresh one when it expires (cache and reuse it until then rather than minting one per request).
Errors follow RFC 6749: bad credentials return 401 { "error": "invalid_client" },
an ungranted scope returns 400 { "error": "invalid_scope" }, and any grant
other than client_credentials returns 400 { "error": "unsupported_grant_type" }.
3. Call the API
Use the access token as a Bearer token on any /v1 endpoint. Its scopes are
enforced identically to an API key's:
curl https://api.permissio.us/v1/me \
-H "Authorization: Bearer at_9f8e7d6c5b4a…"
// Fetch a token, then point the SDK at it (tokens are Bearer tokens):
const res = await fetch("https://api.permissio.us/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "client_credentials",
client_id: process.env.PERMISSIO_CLIENT_ID!,
client_secret: process.env.PERMISSIO_CLIENT_SECRET!,
}),
});
const { access_token } = await res.json();
import { Permissio } from "permissio-sdk";
const client = new Permissio({ apiKey: access_token }); // Bearer token
API keys vs OAuth apps
| API key | OAuth app | |
|---|---|---|
| Credential | one long-lived sk_… key | client_id + client_secret → short-lived token |
| Best for | your own backend | platforms / reselling, per-app isolation |
| Rotation | rotate the key | rotate the secret; tokens expire hourly |
| Scopes | fixed on the key | chosen per token request (subset of the app's grant) |
Authorization-code + a consent screen (acting on behalf of another tenant's end-customers) is on the roadmap. Client-credentials — an app acting as its own tenant — is available today.