A studio API key lets an external tool call the backend on behalf of one studio. Keys are issued from the coach web settings and stored in the 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 prefix pf_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 prefix so the settings page can show which key is which.
  • Scopes. StudioApiKey.scopes is 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.
Errors: 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.
Response:
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.
There is no limit on the number of keys per studio in this service. Errors: 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.
Response:
Side effects: the key stops authenticating on the next request. 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.