API keys
API keys let your customers call your API from scripts, integrations and other applications. Instead of sharing a person's password or a deployment-wide administrator credential, issue a key with just the access that integration needs.
For example, a reporting tool could read documents in one workspace without editing them or accessing another team's documents.
API key management is part of SaaSLayer Permissions. You can use it with SaaSLayer Users, Auth0, Clerk or your own sign-in system. If you keep an existing provider, your backend verifies the session and maps the person to a Permissions user before managing their keys. See keep your existing sign-in.
There are two parts to the integration:
- Your customer creates and manages a key in your app's settings. You can build that screen over the API or use the API key widget, which is Preview.
- Your backend accepts that key when a script or integration calls your API. Follow Accept API keys in your own API below.
Before you start
You need a Permissions deployment and a user or service-account principal to own the key. The owner needs the memberships and permissions the key will use.
The examples use a document service registered in your app's permissions catalog.
Replace files, document and ws-acme-prod with your own service, resource type
and workspace. The Quickstart introduces users, roles and
workspace assignments; hierarchy and namespaces
explains the catalog.
Calls to the management API need an authorized SaaSLayer credential. Creating a
key through POST /api-keys, for example, requires keys:api-key:create and access
to the intended owner and scope. Creating a key does not grant new permissions:
the requested access must be within both the owner's and the issuing caller's
authority.
Create a key
This server-side JavaScript example creates a read-only key for Alice's reporting
integration. permissionsBase is your Permissions API base URL, and
managementKey is the backend credential authorized to create this key.
const response = await fetch(`${permissionsBase}/api-keys`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': managementKey,
},
body: JSON.stringify({
userId: 'user-alice',
name: 'Reporting integration',
scope: { type: 'WORKSPACE', id: 'ws-acme-prod' },
permissions: {
allows: [
{ action: 'read', resource: 'document', service: 'files' },
],
denies: [],
},
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
}),
})
if (!response.ok) throw new Error('API key creation failed')
const { data } = await response.json()
const keyId = data.id
const apiKey = data.attributes.apiKeyThe response is 201 Created. keyId identifies the key for management;
apiKey is the full secret the integration presents. The secret is returned
once, when created. Store it securely or show it once to the person creating it.
Reading or listing the key later does not recover the secret.
If a user requests a key through your app, derive userId from their verified
session. Do not let the browser choose another person as the owner. Keep
managementKey on the server, separate from the key you give the integration.
Choose what the key can do
| Limit | What it controls |
|---|---|
permissions.allows | Which actions and resources the key can use, within a service. |
permissions.denies | Explicit restrictions that override a matching allow. |
scope | Which workspace, or set of workspaces in an organization, the key can reach. |
expiresAt | When the key stops being usable. |
Always send an explicit permission set. A key without one does not gain access just because its owner has a role.
For a key limited to one workspace, use:
{ "type": "WORKSPACE", "id": "ws-acme-prod" }For selected workspaces within one organization, use:
{ "type": "ORG", "id": "org-acme", "workspaces": ["ws-acme-prod", "ws-acme-dev"] }These are values for the key's scope field. A permission-check request instead
names its target workspace as a string, such as "scope": "ws-acme-prod".
Accept API keys in your own API
When an integration presents a key to your API, your backend asks Permissions whether that key is valid and allowed to perform the requested action.
The request flow is:
- Your customer creates a key in your app and gives it to their integration.
- The integration sends that key to your API, for example in
X-API-Key. - Your backend asks SaaSLayer to verify the incoming key and check the specific action, resource and workspace. It uses its own server-side verifier credential to make that call.
- Your backend serves the request only when the decision allows it.
The widget handles key management. Your API performs this check on each protected request, including requests that never pass through your frontend.
Call the following from your backend. The X-API-Key header authenticates your
backend; apiKey in the body is the incoming integration key being checked.
POST /api-keys/verify
X-API-Key: <backend-verifier-key>
Content-Type: application/json
{
"apiKey": "<incoming-integration-key>",
"action": "read",
"resource": "document/doc-123",
"product": "acme",
"service": "files",
"scope": "ws-acme-prod"
}Use the product and service registered for the resource in your catalog. Derive the resource and its workspace from your own application data. For resources identified by an XRN, the XRN carries that identity and scope.
Read data.attributes.valid and data.attributes.permissionCheck.allowed from
the response. Allow the operation only when both are true. A valid key may
still be denied access to that action, document or workspace.
An invalid key can return HTTP 200 with valid: false. A successful HTTP
request alone is not an access decision. Treat errors and missing decisions as
denied access.
Sending only apiKey checks validity; it does not check access to a resource.
Check the key's own permissions rather than checking its owner's roles alone,
which could allow more access than the key was issued to have.
Understand the response
This excerpt shows the relevant fields for an allowed request:
{
"data": {
"type": "api-key-verification",
"id": "<key-id>",
"attributes": {
"valid": true,
"record": {
"userId": "user-alice",
"principalType": "USER",
"scope": { "type": "WORKSPACE", "id": "ws-acme-prod" },
"permissions": {
"allows": [{ "action": "read", "resource": "document", "service": "files" }],
"denies": []
}
},
"permissionCheck": { "allowed": true, "effect": "allow" }
}
}
}validsays whether the key is accepted as a credential.recordidentifies its owner, workspace limits and permission set. Use this context for your app's auditing and identity handling.permissionCheck.allowedis the decision for the action and resource you requested. Use this decision to enforce access, rather than interpreting the permission strings yourself.
A read-only key can return valid: true and permissionCheck.allowed: false
for an update. That means the key works, but the requested operation is forbidden.
Example backend check
This framework-independent helper protects a document read. document is loaded
from your own database; its id and workspaceId come from that record. The
permissionsBase and verifierKey values belong to your backend configuration.
async function authorizeDocumentRead(request, document) {
const incomingKey = request.headers.get('X-API-Key')
if (!incomingKey) return { allowed: false, status: 401 }
try {
const response = await fetch(`${permissionsBase}/api-keys/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': verifierKey,
},
body: JSON.stringify({
apiKey: incomingKey,
action: 'read',
resource: `document/${document.id}`,
product: 'acme',
service: 'files',
scope: document.workspaceId,
}),
})
if (!response.ok) return { allowed: false, status: 503 }
const { data } = await response.json()
if (data?.attributes?.valid !== true) {
return { allowed: false, status: 401 }
}
if (data.attributes.permissionCheck?.allowed !== true) {
return { allowed: false, status: 403 }
}
return { allowed: true, status: 200 }
} catch {
return { allowed: false, status: 503 }
}
}Use the result in your route before returning the document:
const access = await authorizeDocumentRead(request, document)
if (!access.allowed) {
return new Response('Access denied', { status: access.status })
}
return Response.json(document)Here your API returns 401 for a missing or invalid customer key, 403 for a
valid key that cannot perform the action, and 503 when verification is
unavailable. All three stop the protected operation.
Give your backend verification-only access
A deployment administrator can issue a verifier key for your backend's dedicated service
account with POST /service-principals/{id}/verifier-keys. The request includes
name and optionally expiresAt; the response returns the secret once as
data.attributes.apiKey.
The administrator needs keys:api-key:issue-verifier. The resulting key has only
the verification permission, so your backend can verify incoming keys without
receiving general key-management authority. Keep it server-side.
If the service account already exists, it must be owned by the issuing administrator and dedicated to verification, without other roles.
List and manage keys
| Operation | Endpoint |
|---|---|
| List a user's keys | GET /api-keys?userId=<user-id> |
| Read a key's metadata | GET /api-keys/{keyId} |
| Update its name, permissions, workspace limits or expiry | PUT /api-keys/{keyId} |
| Rotate its secret | POST /api-keys/{keyId}/rotate |
| Revoke the key | DELETE /api-keys/{keyId} |
These operations require the corresponding API key permission and authority over the key. Owning a workspace does not automatically mean you can manage every key in it. Updates to permissions and workspace limits must remain within the owner's and issuing caller's authority.
For a caller already authenticated to SaaSLayer, GET /me/api-keys,
POST /me/api-keys and DELETE /me/api-keys/{keyId} provide self-service access to
their own keys. A create request through /me omits userId and principal:
the owner is always the verified caller, and the key is limited by their access.
Rotate or revoke a key
Rotation returns a new key ID and secret and revokes the previous key. The
replacement keeps the previous key's configuration, including its expiry.
POST /api-keys/{keyId}/rotate does not provide an overlap window: coordinate
the credential change with the integration using it.
For an application-controlled overlap, create a replacement using an independent management credential, switch the integration to it, verify that it works, then revoke the old key. A replacement created through the old key depends on that key remaining valid; use an independent credential for this handover.
DELETE /api-keys/{keyId} returns 204 No Content when the key is revoked. When
removing someone's access, review and revoke their existing keys too. If your
application or deployment caches verification results, account for that cache
when planning how quickly revocation takes effect.
Use the audit log to review key-management activity. Avoid logging full key secrets in your own application.