StudioApiKey table (studio_api_keys). The router lives in the partner module (modules/partner/api-keys.routes.ts) because the partner lane is the main consumer.
Mount point and auth
The router starts with
router.use(requireRole(StudioRole.OWNER, StudioRole.HEAD_COACH)), so every endpoint needs one of those roles. Other roles get FORBIDDEN (403) insufficient role.
In modules/index.ts this mount is registered one line before /web/studios, so it wins over the Studios router for these paths.
How keys work
- Format.
generatePartnerSecret()returns the prefixpf_live_followed by 40 hex characters (20 random bytes). - Storage. Only the SHA-256 hash is stored, in
keyHash(unique). The plaintext exists only in the create response. It cannot be read again. - Prefix. The first 15 characters of the secret are stored in
prefixso the settings page can show which key is which. - Scopes.
StudioApiKey.scopesis a string array with database default["panel"]. This router does not set or return it. - Revocation. Revoking sets
revokedAt. The row stays so the list keeps its history.
Where a key is accepted
requirePartnerKey in modules/partner/partner-auth.ts authenticates a request from the Authorization: Bearer YOUR_API_KEY header. It rejects a missing header or a value that does not start with pf_live_, then looks the hash up. A key is refused when it is unknown, revoked, or its studio has deletedAt set. On success the request gets req.partner = { studioId, apiKeyId }.
lastUsedAt is stamped at most once every 5 minutes per key (LAST_USED_THROTTLE_MS). The write is fire-and-forget and never fails the request. Treat it as a coarse usage signal, not an access log.
The WhatsApp agent also creates rows in this table through its own repository, so a studio’s list can contain a key the coach did not create by hand. Revoking that row disconnects the agent for the studio.
Endpoints
GET /v1/web/studios/current/api-keys
Lists every key of the studio, revoked ones included, newest first.
Auth: web lane, roles OWNER or HEAD_COACH.
Response: data.keys is an array. The hash and scopes are never returned.
string
cuid.
string
The label given at creation.
string
First 15 characters of the secret.
string | null
Last throttled usage stamp.
string
ISO timestamp.
string | null
Set once the key is revoked.
FORBIDDEN (403), UNAUTHORIZED (401).
POST /v1/web/studios/current/api-keys
Creates a key and returns its secret once. Responds with status 201.
Auth: web lane, roles OWNER or HEAD_COACH.
string
required
Trimmed, 1 to 100 characters. Names do not have to be unique.
object
id, name, prefix, createdAt.string
The full plaintext key. Show it to the user now. It is not stored and no later call returns it.
VALIDATION (422) for a missing, empty or too long name. FORBIDDEN (403). CONFLICT (409) resource already exists is possible in theory if a generated hash collides with the unique keyHash index, through the shared Prisma error mapping.
POST /v1/web/studios/current/api-keys/:id/revoke
Revokes a key. The service first checks the key belongs to the studio, then runs updateMany with revokedAt: null in the filter, so revoking an already revoked key succeeds and keeps the original revokedAt.
Auth: web lane, roles OWNER or HEAD_COACH.
string
required
Key id.
AutomationHook rows reference StudioApiKey (see Automation hooks). This service does not touch them on revoke.
Errors: NOT_FOUND (404) api key not found, FORBIDDEN (403).
There is no delete endpoint and no way to rename a key.