Send your key as a bearer token:
Authorization: Bearer bc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Getting a key
Dashboard → Settings → Developers → Create key.
The key is shown once, at creation, and never again. We store a SHA-256 hash of it, not the key, so there is no "reveal" button and no support request that can recover one. If you lose it, rotate it.
Give each key a name that says where it runs — up to 60 characters. When something needs turning off at short notice, the name is what tells you which one. You can give it an expiry of up to 365 days, which is what you want for a key you are handing to somebody else.
How many active keys a workspace may hold depends on its organisation's plan — see Plans and rate limits; never more than 20. The refusal names the number that applied. Revoked and expired ones do not count. The limit is there so that "revoke everything and start again" stays a bounded amount of work at the moment you least want an unbounded one.
Everything on that screen is also an API call, so rotating every key on a schedule is a cron job rather than somebody remembering.
List the keys this workspace holds — answers 200
Metadata only, including revoked and expired rows: the row is the record that a credential was issued here. Never a key itself, which is not stored.
curl https://api.boltclaw.ai/v1/keys \
-H "Authorization: Bearer bc_live_EXAMPLExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
const res = await fetch("https://api.boltclaw.ai/v1/keys", {
headers: {
Authorization: `Bearer ${process.env.BOLTCLAW_KEY}`,
},
});
// 200
const data = await res.json();
Scopes
A key does only what it was created to do. You pick that at creation, and you
can change it afterwards without changing the key itself — see
PATCH /v1/keys/{id}.
| Scope | What it allows |
|---|---|
pages:read |
Read pages, links and statistics |
pages:write |
Create pages; edit links, theme and settings |
pages:delete |
Delete a page permanently |
inquiries:read |
Read conversations and messages |
inquiries:write |
Reply, mark read, close, re-open and file conversations |
social:read |
Read connected social accounts, posts, the social inbox, automations and usage |
social:write |
Schedule, publish and cancel posts, upload media, reply to and hide comments, send DMs, manage automations |
leads:read |
Read captured leads — other people's email addresses and phone numbers |
conversions:write |
Report signups, sales and refunds for your referral programs |
referrals:read |
Read your referral programs, their partners, commissions, payouts and statistics |
referrals:write |
Create and edit referral programs, approve or reject partners, adjust commissions, mark payouts paid, record rewards that are not cash as given, and create the program's codes in your Stripe |
referrals:embed |
Show your own users a referral panel: mint panel sessions and create, update and read the referrers behind them, and read the friend's offer and who invited a customer. Nothing else. Never carried by an AI assistant |
referrals:payouts |
For your own payout system: send a payout batch, read where a requested payout goes, and report what happened to it. Beside referrals:write, it also lets a key forfeit money held for a missing tax form. Never carried by an AI assistant |
keys:read |
List API keys and what each may do. Never a key itself |
keys:write |
Create, re-scope, rotate and revoke API keys |
webhooks:read |
List webhook endpoints, the event catalog and a delivery log |
webhooks:write |
Register, change, pause and delete webhook endpoints; rotate a signing secret; redeliver; send a test event |
logs:read |
Read the workspace's activity log and the log of what AI assistants did |
workspace:read |
Reserved. GET /v1/ping and GET /v1/workspace need no scope at all |
Keys an AI assistant holds
A key approved through the device sign-in is an agent key, and three things are true of it that are not true of a key you created in the dashboard.
It can never carry keys:read, keys:write, webhooks:read or
webhooks:write. Not "those are left out of the form": the database refuses
them, on the way in and on every request afterwards, whatever anybody ticks.
Something acting on its own should not be able to issue credentials or point
your data at a new address, and that is a property of the key rather than a
policy somebody has to remember. referrals:embed is refused the same way: a
panel session is minted by your own server for a person who is signed in to your
product, never by an assistant. So is referrals:payouts: an assistant never
reads where a promoter's money goes or reports a payout's result, and it sends a
payout batch only with a person's approval.
An organisation can turn every one of them off in one click, under
Settings, AI agents, without hunting down and revoking each key, and turn them
back on without anybody signing in again. While it is off, those keys answer
403 AGENT_ACCESS_DISABLED; a key you created in the dashboard is untouched.
Everything one does is on a log, GET /v1/logs/agent-calls, whatever the
outcome.
What an assistant must ask about
Some calls stop and ask a person first. The API answers 428 with a link,
nothing happens, and the assistant retries the identical request with the
approval id once somebody has decided. The mechanics are
on the Agents page.
Which calls, and whether you can decide to stop being asked:
| Action | Operation | Can an organisation waive it? |
|---|---|---|
page.delete |
DELETE /v1/pages/{slug} |
Yes |
page.unpublish |
PATCH /v1/pages/{slug} with published: false |
Yes |
link.delete |
DELETE /v1/links/{id} |
Yes |
conversation.reply |
POST /v1/inquiries/{id}/messages |
Yes |
post.publish |
POST /v1/posts, unless draft: true |
Yes |
post.delete |
DELETE /v1/posts/{id} |
Yes |
inbox.reply |
POST /v1/inbox/{id}/reply |
Yes |
inbox.hide |
POST /v1/inbox/{id}/hide |
Yes |
automation.enable |
POST /v1/automations, unless enabled: false; PATCH /v1/automations/{id} with enabled: true |
Yes |
automation.delete |
DELETE /v1/automations/{id} |
Yes |
lead.delete |
DELETE /v1/leads/{id} |
Yes |
referral.payout.paid |
POST /v1/referrals/payouts/{id}/paid |
No |
referral.commission.adjust |
POST /v1/referrals/commissions/{id}/adjust |
No |
referral.partner.approve |
POST /v1/referrals/partners/{id}/approve |
No |
referral.commission.bonus |
POST /v1/referrals/partners/{id}/bonuses |
No |
referral.reward.delete |
DELETE /v1/referrals/rewards/{id} |
No |
referral.group.delete |
DELETE /v1/referrals/groups/{id} |
No |
referral.referrer.erase |
DELETE /v1/referrals/referrers/{id} |
No |
referral.grant.fulfil |
POST /v1/referrals/grants/{id}/fulfil; POST /v1/referrals/grants/{id}/send |
No |
referral.grant.fail |
POST /v1/referrals/grants/{id}/fail |
No |
referral.stripe.codes |
POST /v1/referrals/programs/{id}/stripe-codes |
No |
referral.testdata.clear |
DELETE /v1/referrals/programs/{id}/test-data |
No |
referral.flag.dismiss |
POST /v1/referrals/flags/{id}/dismiss; POST /v1/referrals/programs/{id}/flags/dismiss |
No |
referral.flag.confirm |
POST /v1/referrals/flags/{id}/confirm; POST /v1/referrals/programs/{id}/flags/confirm |
No |
referral.risk.update |
PATCH /v1/referrals/programs/{id}/risk-policy |
No |
referral.payout.send |
POST /v1/referrals/payout-batches/{id}/send |
No |
referral.payout.settings |
PATCH /v1/referrals/programs/{id}/payout-settings |
No |
referral.commission.forfeit |
POST /v1/referrals/partners/{id}/forfeit |
No |
referral.tax.settings |
PATCH /v1/referrals/programs/{id}/tax |
No |
The first group is your own content, and an organisation that trusts its assistant with it may turn those prompts off, or trust one connection for up to eight hours. The referral rows move somebody else's money or change somebody else's access, or what they will be paid, and are asked every time: turning off a convenience switch is not a decision that an assistant may pay people without asking.
A key never does more than the person who created it
This is the one rule worth reading twice.
A key's authority is the scopes stamped on it, intersected with the permissions the person who created it still holds in that workspace, worked out on every request. Nothing is cached and nothing is a snapshot. So:
- change that person's role, and every key they made narrows on its next request;
- remove them from the organisation, and their keys carry nothing and answer
403 INSUFFICIENT_SCOPE; - the key keeps working for everything they still may do.
GET /v1/ping reports the difference as lost_scopes, and
GET /v1/keys reports it per key. Normally both are empty. When
one is not, the fix is at your end: restore that person's permission, or have
somebody who holds it create the key again.
The same rule bounds what you can grant. A key you ask for carries at most what
you carry, and a key created by another key carries at most what that
key carries — so a chain of keys collapses to one person's live permissions
however long it is. Anything asked for and not granted comes back in dropped,
and the key is still created.
Eight presets are offered in the dashboard. Inbox (inquiries:read +
inquiries:write) is the default and is exactly what a self-hosted inbox
needs. Pages manages pages and links with no access to conversations.
Social (social:read + social:write) schedules posts, works the social
inbox and runs automations. Full is all three. A fifth, Referral
tracking, is conversions:write alone: the key your checkout server holds to
report conversions, which can read nothing. A sixth,
Referrals (referrals:read + referrals:write), runs your referral
programs from your own systems; it does not report conversions, so the two
jobs stay on two keys. A seventh, Referral panel (referrals:embed alone),
is the key the server that shows your users their referrals holds: it mints
panel sessions and creates referrers, and a leaked one cannot touch a program,
a partner or a payout. See Referral panel. An eighth,
Referral payouts (referrals:read + referrals:payouts), is the key your own
payout system holds: it sends payout batches, reads where each requested payout
goes and reports what happened. See Paying partners.
pages:delete is in none of them. It is the only irreversible operation in
this API — deleting a page also ends your access to every conversation on it —
and a key is a credential you are told to paste into a CI runner and a Zapier
step. It is a separate tick box, on purpose.
Neither is leads:read. A lead is somebody else's email address or phone
number, handed to your automation by a person who expected you — not every
tool you connect — to have it. A key pasted into a scheduling tool has no
business reading them, so this scope is only ever added on purpose.
A call outside a key's scopes answers 403 with code: "INSUFFICIENT_SCOPE", and the message names the scope that was missing.
Nothing is read or written first, so the refusal tells you nothing about
whether the page or thread exists — it is a statement about your key only.
Do not retry a 403; no amount of waiting adds a scope. It is the one error
here that means "the request was fine, this credential was not."
What a key outside its scopes gets — answers 403
A key without leads:read is refused before anything is read, so the answer says nothing about whether you have any leads.
curl https://api.boltclaw.ai/v1/leads \
-H "Authorization: Bearer bc_live_NARROWxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
const res = await fetch("https://api.boltclaw.ai/v1/leads", {
headers: {
Authorization: `Bearer ${process.env.BOLTCLAW_KEY}`,
},
});
// 403: branch on the "code", not the sentence
const data = await res.json();
Keys created before scopes existed hold the five scopes that existed then, so
nothing they could do before stopped working — pages:delete included. They
hold no referral scope and no keys: scope.
One scope moved. GET /v1/webhooks is now webhooks:read. It still
accepts inquiries:read, so a key issued before the move keeps working; that
acceptance will be withdrawn, so issue new keys with webhooks:read.
keys:* and webhooks:* cannot be held by an AI assistant's key. Whatever
is asked for, a key approved for an assistant carries neither. Issuing
credentials and pointing your data at a new address are things a person does,
or a system a person deployed; they are not things a credential acting on its
own should be able to do. Your own scripts and servers may hold both.
Storing a key
A key can do everything to your pages that its creator can. Treat it like a password:
- Put it in an environment variable or a secret manager. Never in a repository, a client-side bundle, a mobile app, or a browser — anything shipped to a user is readable by that user.
- Use a separate key per system, so one can be revoked without stopping the others.
- Give it an expiry if you can. A key that stops on its own is one you cannot forget about.
Rotating a key
Dashboard → Settings → Developers → the rotate button, or
POST /v1/keys/{id}/rotate.
Rotation issues a replacement with the same scopes and the same expiry, shows
it once, and retires the old one. grace_seconds keeps the old value working
for up to 24 hours, so the new one can reach every deployment before anything
breaks; zero stops it at once, which is what you want when the old value
leaked.
The replacement has a new id. If you store the id as well as the value,
store both again. rotated_from on the new key names the old one.
Revoking a key
Dashboard → Settings → Developers → the revoke button, or
DELETE /v1/keys/{id}.
Revocation is immediate. The next request with that key gets 401. The key's
row stays in your list, marked revoked, so a key that turns up in a log later
is still identifiable. Revoking a key that is already revoked or expired is
404.
Checking a key
GET /v1/ping answers with the scopes the key carries, and GET /v1/workspace
with the workspace it belongs to. Neither needs a scope, so any working key can
call them: use them while you set an integration up, and as a liveness probe.
{
"pong": true,
"key_kind": "secret",
"scopes": ["pages:read", "pages:write"],
"lost_scopes": []
}
{ "id": "6f1c2a9e-3b7d-4c51-9e08-2d4a7b6c1f30", "name": "Main" }
Every key belongs to one workspace, so no other call takes a workspace parameter, and this is the only response an ordinary key gets that names one. The two others are not answered to a key at all: the approval of a device sign-in, which goes only to the program that started it, and a route reserved for the hosted MCP server.