API Keys
Create, list, and revoke API keys programmatically, and narrow a key with scopes.
Create a Key
POST /api/v1/keys
Authorization: Bearer chirpie_sk_YOUR_KEY
Content-Type: application/jsonRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Key name, 100 characters or less. Empty, whitespace or omitted means "Default" |
scopes | string[] | No | What the new key may do. Omit it for a key that can do exactly what the key making the request can do. An empty array is refused. See Scopes |
Allowed values for scopes: posts:read, posts:write, accounts:read, accounts:write,
analytics:read, comments:read, comments:write, media:write, keys:write.
This endpoint needs keys:write, and so do List and Revoke. There is deliberately no
keys:read: the key list is the inventory of your account's credentials, and a key meant
only to post has no business enumerating its siblings.
A key can never grant a scope it does not itself hold. Minting with a narrow key and
asking for something outside it returns 403 insufficient_scope naming the scope, so a
restricted key cannot quietly widen itself. Leaving scopes out is not a way around that:
the new key inherits the caller's own scopes, so a scoped key mints a copy of itself and
only a full-access key (or the dashboard) mints a key that can grant anything.
Response 201 Created
{
"data": {
"key": "chirpie_sk_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789ab",
"prefix": "chirpie_sk_aBcDeFgH",
"name": "Production",
"scopes": ["posts:write", "media:write"],
"expires_at": "2026-06-21T00:00:00.000Z",
"message": "Save this key securely. It will not be shown again."
}
}| Field | Description |
|---|---|
key | The full key. Returned once, here, and never again |
prefix | The leading characters of the key, used to identify it in listings |
scopes | The scopes the key holds, or null for full access |
expires_at | Keys expire 90 days after creation. Create a replacement before then |
The full key is returned only once in this response. Store it immediately. Chirpie stores a SHA-256 hash, so the raw key cannot be retrieved later.
Limits
Maximum 25 active keys per account. Creating a 26th returns 400 bad_request:
API key limit reached (25). Revoke unused keys before creating new ones.
List Keys
GET /api/v1/keys
Authorization: Bearer chirpie_sk_YOUR_KEYResponse 200 OK
{
"data": [
{
"id": "key-uuid-here",
"name": "Production",
"key_prefix": "chirpie_sk_aBcDeFgH",
"scopes": ["posts:write", "media:write"],
"last_used_at": "2026-03-23T10:00:00.000Z",
"expires_at": "2026-05-30T00:00:00.000Z",
"created_at": "2026-03-01T00:00:00.000Z"
}
]
}Only key prefixes are returned, never the full key. Revoked keys are not listed.
scopes is on every key in the listing. It is null on a key with full access, which is
what every key minted before scopes existed carries, and what a key created without scopes
by the dashboard or by another full-access key carries. A key created without scopes by a
narrowed key carries a copy of that key's list instead.
Revoke a Key
DELETE /api/v1/keys?id=KEY_UUID
Authorization: Bearer chirpie_sk_YOUR_KEYQuery Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string (UUID) | Yes | The key ID to revoke |
Response 200 OK
{
"data": {
"id": "key-uuid-here",
"revoked": true
}
}Revoked keys stop working immediately. This action cannot be undone.
Error Responses
| Status | Code | Condition |
|---|---|---|
400 | bad_request | A scopes value that is not a scope, an empty scopes array, a name over 100 characters, or the 25-key limit |
401 | unauthorized | Invalid, expired or revoked API key |
403 | insufficient_scope | The key does not hold keys:write, or is trying to grant a scope it does not itself hold. The message names the scope |
404 | not_found | On revoke: no key with that id on your account |
From the CLI, SDK and MCP
chirpie keys create --name "Publishing bot" --scope posts:write --scope media:write
chirpie keys # prints each key's scopes, or "all" for full accessawait chirpie.createKey({ name: "Publishing bot", scopes: ["posts:write", "media:write"] });
await chirpie.createKey("Everything"); // a bare name still works, and copies this key's scopesMCP: chirpie_create_key takes name and an optional scopes array.