Backend
API keys
pleate_backend.apikeys lets users create personal keys for scripts, CI and integrations. A key looks like pl_<prefix>_<secret>: 8 characters of prefix, stored and indexed so the key can be found and shown, and 32 random bytes of secret. Only the SHA-256 of the whole key is stored; the key itself is returned once, by the create call.
Personal API keys
Create a key, copy it while it is shown, revoke it. useApiKeys maps the API onto ApiKeyList.
ada@plate.devUsing a key
curl https://plate.app/api/catalog/products \
-H "Authorization: Bearer pl_k3J9xQ2m_8s7d6f5g4h3j2k1l0z9x8c7v6b5n4m3q2w1e0r9t8y7u6"APIKeyAuthentication runs before the session check on every endpoint. It finds the key by prefix, compares the hash in constant time and sets request.auth to the APIKey. Requests with a key skip CSRF, since they carry no cookie. last_used_at and last_used_ip are written at most once a minute.
readkeys may only use GET, HEAD and OPTIONS.writeimpliesread.- Keys can expire after 1 to 3650 days, or never.
- A user holds at most 25 active keys.
- Keys of deactivated users stop working at once.
From the TypeScript client, on a server or in a script:
import { createPleateClient } from "pleate-client";
const plate = createPleateClient({
baseUrl: "https://plate.app/api",
headers: { Authorization: `Bearer ${process.env.PLATE_API_KEY}` },
csrf: false,
});
const page = await plate.list("/catalog/products", { search: "oat", page_size: 100 });Endpoints
Mounted at /api/api-keys. Managing keys needs a session: a request made with an API key gets api_key_not_allowed, so a leaked key cannot mint more keys. The same goes for every account security endpoint: password and email change, session revocation, account update and deletion, the data export, 2FA setup and removal, and linking or unlinking social accounts. Changing or resetting the password revokes every key.
| Method | Path | Purpose |
|---|---|---|
| GET | /api-keys | My keys. Page list, ?search= name or prefix, ?ordering=created_at,name,last_used_at,expires_at. |
| POST | /api-keys | {name, scopes?: ["read"] | ["read", "write"], expires_in_days?} → 201 with key, the only time it is returned. |
| GET | /api-keys/<id> | One key without key. |
| PATCH | /api-keys/<id> | {name} |
| DELETE | /api-keys/<id> | Revokes (sets revoked_at). 204. |
{
"id": "6720c9a1b2c3d4e5f6a7b8c9",
"name": "CI deploys",
"prefix": "k3J9xQ2m",
"display": "pl_k3J9xQ2m_...",
"scopes": ["read", "write"],
"status": "active",
"expires_at": "2027-01-24T10:00:00Z",
"last_used_at": null,
"last_used_ip": null,
"created_at": "2026-10-26T10:00:00Z",
"revoked_at": null,
"key": "pl_k3J9xQ2m_8s7d6f5g4h3j2k1l0z9x8c7v6b5n4m3q2w1e0r9t8y7u6"
}status is active, expired or revoked.
Helpers
from pleate_backend.apikeys.services import create_key, find_key
from pleate_backend.apikeys.permissions import DenyAPIKey
key, raw = create_key(user, "Nightly export", scopes=["read"], expires_in_days=90)
class BillingPortalView(APIView):
permission_classes = [IsAuthenticated, DenyAPIKey] # session onlyError codes
| Code | Status | When |
|---|---|---|
| invalid_api_key | 401 | Unknown, revoked, or belonging to a deactivated user. |
| api_key_expired | 401 | Past expires_at. |
| insufficient_scope | 403 | A read key on POST, PUT, PATCH or DELETE. |
| api_key_not_allowed | 403 | A session only endpoint called with a key. |
| api_key_limit | 400 | 25 active keys already. |