Account Management API
Create, freeze, rotate and revoke API keys, and read credits, usage and auto top-up settings, without a browser.
Everything the console does to an organization's API keys and billing can also be done over HTTP, at api.b1m.ai. Mint a key from CI, freeze a leaked one from a script, or pull usage into your own billing system.
Two APIs, two credentials
This is the part worth reading twice. Beam has two HTTP APIs and they do not share a credential.
| Transfer API | Management API | |
|---|---|---|
| Host | beamcore.b1m.ai | api.b1m.ai |
| Does | Runs transfers | Administers keys and billing |
| Credential | An API key, b1m_… | A service account credential, bm_sa_… |
| Header | X-Api-Key | Authorization: Bearer |
A transfer key cannot manage keys, deliberately. A transfer key is handed to whatever runs a job — CI, a container, a colleague's laptop. If it could also mint keys, one leaked credential could issue successors that outlive revoking the original, and revoking it would not end the compromise. So the two capabilities are held by different credentials, and a service account is created and revoked from the console, separately from the keys it administers.
A service account credential also cannot run transfers. Each does one job.
Getting a credential
In the console, go to Organization → Service accounts and create one. A service account is a machine identity: it has a name, a role, and optionally a project, and it holds one or more credentials.
Creating a credential shows the bm_sa_… secret once. It is stored hashed and cannot be shown again — if you lose it, issue another.
You need the Manage service accounts permission to create one. If you do not have it, an organization owner or admin does.
What a credential is allowed to do
A credential's permissions come from its service account's role, plus any permissions granted to that service account directly:
| Role | Can read keys and billing | Can create and rotate keys | Can revoke keys | Can change billing |
|---|---|---|---|---|
| Owner, Admin | Yes | Yes | Yes | Yes |
| Developer | Yes | Yes | No | No |
| Billing | Yes | Yes | No | Yes |
| Viewer | Yes | No | No | No |
| Custom | Only what is granted directly |
Give a service account the least that its job needs. A deployment pipeline that only rotates keys does not need billing access.
Disabling the service account disables every credential it holds, at once. That is the fastest way to cut off an integration.
Authentication
Send the credential as a bearer token:
curl https://api.b1m.ai/v1/keys \
-H "Authorization: Bearer bm_sa_your_credential"X-Api-Key: bm_sa_… is accepted as well, if that is easier for your client.
Every route answers for exactly one organization — the one its credential belongs to. There is no organization parameter, and a credential cannot read or change another organization.
Managing API keys
| Method | Path | Permission |
|---|---|---|
GET | /v1/keys | Read API keys |
POST | /v1/keys | Create API keys |
GET | /v1/keys/:id | Read API keys |
PATCH | /v1/keys/:id | Create API keys |
POST | /v1/keys/:id/rotate | Create API keys |
DELETE | /v1/keys/:id | Manage service accounts |
These manage transfer keys (b1m_…). Service account credentials are not listed here and are managed from the console.
Create a key
curl -X POST https://api.b1m.ai/v1/keys \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL" \
-H "Content-Type: application/json" \
-d '{"name": "production ingest", "creditLimit": 5000}'| Field | |
|---|---|
name | Required. |
expiresAt | ISO date or timestamp. A bare date expires at the end of that day, UTC. Omit for a key that does not expire. |
creditLimit | Credits this key may spend, on top of the organization's balance. Omit for no per-key cap. |
projectId | Scope the key to a project, so its spend is attributed there. |
monthlyBudgetCredits, budgetWarningThresholds, budgetBlockOnExceed | Budget controls. Sending any of these also requires Manage billing. |
The response carries rawKey — the only time the secret is returned. Store it before you do anything else.
{
"key": {
"id": "cmud1y0pl0001qr018c338s2g",
"name": "production ingest",
"prefix": "b1m_p6e-1LXE",
"status": "ACTIVE",
"creditLimit": 5000,
"rawKey": "b1m_p6e-1LXE…"
}
}Your organization must have passed KYC before it can create keys, and a restricted organization cannot create them at all.
Freeze a key
Freezing stops a key working while keeping it, its budget and its usage history. Use it when you suspect a key is compromised but are not ready to throw it away.
curl -X PATCH https://api.b1m.ai/v1/keys/$KEY_ID \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL" \
-H "Content-Type: application/json" \
-d '{"status": "DISABLED"}'status accepts ACTIVE, DISABLED and REVOKED. Freezing is reversible; revoking is not. The same call changes name, creditLimit, expiresAt, projectId and the budget fields.
A frozen key stops authenticating transfers immediately, including any secret still inside a rotation grace period.
Move a key between projects
A key's scope is not fixed at creation. PATCH it with a different projectId to move it, or with null to return it to organization scope:
# scope it to a project
curl -X PATCH https://api.b1m.ai/v1/keys/$KEY_ID \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL" \
-H "Content-Type: application/json" \
-d '{"projectId": "cmuel5roi000zop016h40hpi3"}'
# back to organization-wide
-d '{"projectId": null}'Moving a key changes where its future spend is attributed. Usage already recorded stays under the project it was spent in, so past periods do not move with the key.
An organization-wide credential can move a key anywhere in its organization, including back to organization scope. A project-scoped credential can only move keys between the projects it holds — sending a key to a project it cannot reach, or to organization scope, would put that key beyond the caller on the very next request. Either returns 403 project_out_of_scope, and the response names the projects that are allowed:
{
"error": "project_out_of_scope",
"message": "This credential can only move keys between the projects it is scoped to",
"allowed": ["cmuel5roi000zop016h40hpi3"]
}Omitting projectId entirely leaves the scope alone — it is only read when present.
Delete a key
curl -X DELETE https://api.b1m.ai/v1/keys/$KEY_ID \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL"The key is removed and every secret it ever had stops working. Its past spend stays in your usage history — deleting a key does not rewrite what it cost you.
Rotating a key
Rotation issues a new secret for an existing key while keeping its identity, project, budget and usage history. The old secret keeps working for a grace period, so a deployment can pick up the new secret at its own pace instead of losing access the moment you rotate.
curl -X POST https://api.b1m.ai/v1/keys/$KEY_ID/rotate \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL" \
-H "Content-Type: application/json" \
-d '{"gracePeriodDays": 7}'gracePeriodDays is one of 0, 1, 3, 7 or 14, and defaults to 7.
{
"key": { "prefix": "b1m_pQOAb7YF", "rawKey": "b1m_pQOAb7YF…" },
"rotation": {
"oldPrefix": "b1m_p1y5El5i",
"newPrefix": "b1m_pQOAb7YF",
"gracePeriodDays": 7,
"oldMaterialExpiresAt": "2026-09-29T19:16:10.000Z"
}
}Both secrets authenticate until oldMaterialExpiresAt. Deploy the new one, confirm it works, and let the old one lapse.
Choose 0 when the old secret is compromised: it is revoked on the spot, and anything still using it fails immediately. That is the point — a grace period is for planned rotation, not for a leak.
Rotation recomputes the key's permissions rather than copying them, so a permission changed since the key was issued takes effect when it rotates. Only an ACTIVE key can be rotated.
Projects
A project groups keys so their spend is attributed together and can be capped together. Because a key can be created straight into one, the API can make and remove projects too.
| Method | Path | Permission |
|---|---|---|
GET | /v1/projects | Read transfers |
POST | /v1/projects | Create transfers, at organization scope |
DELETE | /v1/projects/:id | Create transfers, at organization scope |
curl -X POST https://api.b1m.ai/v1/projects \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL" \
-H "Content-Type: application/json" \
-d '{"name": "Data pipeline", "description": "Nightly ingest"}'| Field | |
|---|---|
name | Required, at least 2 characters. |
description | Optional. |
memberIds | Organization members to add. Ignored for anyone outside the organization. |
The slug is derived from the name and made unique within the organization, so a second Data pipeline becomes data-pipeline-2.
Creating and deleting both require the permission at organization scope. A credential confined to one project cannot mint another or delete the boundary it was given — see What a credential is allowed to do.
Delete a project
curl -X DELETE https://api.b1m.ai/v1/projects/$PROJECT_ID \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL"A project must be empty first. While any API key or service account still belongs to it, the call returns 409 project_not_empty and names what is in the way:
{
"error": "project_not_empty",
"message": "This project still has 2 API keys and 1 service account. Move or delete them first."
}This is deliberate. A project-scoped key or credential is scoped by pointing at the project; delete the project underneath it and that scope falls away, quietly promoting it to organization-wide reach. Move them to another project, or delete them, and the promotion never happens.
Deleting a project removes its members and its budget. Usage already recorded stays in your billing history.
Credits and usage
| Method | Path | Permission |
|---|---|---|
GET | /v1/credits | Read billing |
GET | /v1/usage | Read billing |
/v1/credits returns the organization's balance:
{
"organizationId": "org_…",
"organizationName": "Acme",
"credits": 4820,
"restrictionStatus": "NONE"
}/v1/usage answers where credits went, broken down by key, by project and by action:
curl "https://api.b1m.ai/v1/usage?dateFrom=2026-09-01&dateTo=2026-09-30" \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL"| Parameter | |
|---|---|
days | Days to look back. Defaults to 30, capped at 365. |
dateFrom, dateTo | An explicit window, which takes precedence over days. A bare date covers that whole day, UTC, so a calendar month means that month. |
keyId | Restrict to one key. |
The response carries totalCreditsUsed, totalRequests, byKey, byProject, byAction, dailyUsage, and the 50 most recent usage transactions. All three breakdowns are derived from the same period, so they always agree with the total.
Each breakdown is a snapshot of what was true when the credits were spent. Moving a key between projects does not rewrite earlier periods.
Auto top-up
Auto top-up buys a credit pack automatically when the balance falls to a threshold.
| Method | Path | Permission |
|---|---|---|
GET | /v1/billing/auto-topup | Read billing |
PATCH | /v1/billing/auto-topup | Manage billing |
GET | /v1/billing/packs | Any valid credential |
curl -X PATCH https://api.b1m.ai/v1/billing/auto-topup \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL" \
-H "Content-Type: application/json" \
-d '{"enabled": true, "thresholdCredits": 500, "packId": "pack_50"}'thresholdCredits is between 1 and 10,000. packId comes from /v1/billing/packs.
Adding a card is the one thing this API cannot do. Card details go to Stripe through the console, never through Beam. Enabling auto top-up without a saved payment method answers 409 payment_method_required; add one in Billing first, then enable it here. Once a card is saved, everything else about auto top-up is configurable programmatically.
A failed charge disables further attempts and records the reason, so a dead card does not get retried indefinitely. Writing to this endpoint clears that state, which is how you resume after fixing the card.
Budget alerts
A monthly budget is only useful if something tells you when it is running out. Set a budget and its warning thresholds on a key — monthlyBudgetCredits, budgetWarningThresholds, budgetBlockOnExceed — and Beam records an alert the first time each threshold is crossed in a month.
| Method | Path | Permission |
|---|---|---|
GET | /v1/alerts | Read billing |
curl https://api.b1m.ai/v1/alerts \
-H "Authorization: Bearer $BEAM_MANAGEMENT_CREDENTIAL"| Query | |
|---|---|
since | ISO timestamp. Only alerts raised after it — poll with the last value you saw. |
unacknowledged | true to drop alerts already dismissed in the console. |
limit | 1–200, default 50. |
The response also carries organizationId — the organization the credential speaks for. A client holding one credential on behalf of several viewers must check it against whoever is looking, rather than assuming the alerts belong to them.
{
"alerts": [
{
"id": "cmuf…",
"targetType": "api_key",
"threshold": 80,
"usageCredits": 812,
"budgetCredits": 1000,
"percentUsed": 81,
"month": "2026-09-01T00:00:00.000Z",
"createdAt": "2026-09-23T14:22:04.000Z",
"acknowledgedAt": null,
"apiKey": { "id": "cmue…", "name": "production ingest", "prefix": "b1m_p6e-1LXE" },
"project": null
}
]
}Each threshold raises one alert per target per month. Crossing 80% does not alert again on the next transfer, so polling this endpoint gives you a list of crossings rather than a stream that repeats.
Spending the budget always raises a threshold: 100 alert, whatever warning thresholds are configured. Without it, a key that crossed 95% earlier in the month and then ran out produced nothing new — exhaustion, the one state worth acting on, was the only silent one.
A refused charge still raises its alert. With budgetBlockOnExceed set, usage beyond the budget is rejected — and the crossing is recorded anyway, so a blocked key explains itself here rather than only through a 403 at the call site. The thresholds recorded are the ones the key has actually reached: a key at 3 of 4 credits that is refused a 2-credit charge reports 75%, not 125%.
beam-cli signs in as a person rather than as a service account, so the same alerts are also served at GET /api/alerts for a device-flow session, taking an X-Organization-Id header and the same query parameters. A member needs billing:read, which every role carries.
Where a budget stands right now
Alerts say what already happened. Every key returned by /v1/keys also carries a budget object saying where it stands:
{
"budget": {
"budgetCredits": 1000,
"usageCredits": 812,
"percentUsed": 81,
"thresholds": [50, 80, 95],
"thresholdsReached": [50, 80],
"blockOnExceed": true,
"state": "warning"
}
}state | |
|---|---|
none | No budget set. Nothing to report. |
ok | Under every threshold. |
warning | At or past a threshold, still under budget. |
exceeded | At or past budget, and usage continues. |
blocked | At or past budget, and further usage is refused. |
percentUsed is clamped to 100, so a progress bar built on it never overflows.
blocked is enforced, not advisory. With budgetBlockOnExceed set, usage beyond the budget is rejected with 403. That check runs when usage is recorded, so it stops the next charge rather than one already in flight — bytes that have already moved are still billed.
Errors
Every error returns a JSON body with a stable error code and a human-readable message. Match on the code.
| Status | Code | |
|---|---|---|
| 401 | unauthorized | Missing, unknown, or not a service account credential. |
| 401 | credential_inactive | The credential or its service account is disabled, revoked or expired. |
| 403 | insufficient_scope | Valid credential, but it does not hold the required permission. The response names it. |
| 403 | kyc_required | The organization must pass KYC before creating keys. |
| 403 | organization_blocked | The organization is restricted. |
| 403 | project_out_of_scope | A project-scoped credential tried to move a key outside its projects. |
| 403 | monthly budget exhausted | A budget with budgetBlockOnExceed is spent. Raise it, or clear the block. |
| 404 | not_found | No such key or project in this organization. |
| 400 | name_required, invalid_credit_limit, invalid_expiration | Bad field on create or update. |
| 400 | invalid_name | A project name must be at least 2 characters. |
| 400 | invalid_status | status must be ACTIVE, DISABLED or REVOKED. |
| 400 | key_not_active | Only an active key can be rotated. |
| 400 | invalid_grace_period | Must be 0, 1, 3, 7 or 14. |
| 400 | invalid_threshold, invalid_pack | Bad auto top-up setting. |
| 409 | payment_method_required | Enable auto top-up only after a card is saved. |
| 409 | project_not_empty | Keys or service accounts still belong to the project. |
An unknown credential and a transfer key presented to this API both return the same 401 unauthorized. Telling a transfer key that it is merely the wrong kind of credential would confirm it is a valid one.
Related
- Developer Quickstart — create your first key and send a transfer.
- Billing & Payments — how payments become credits, and the console's own billing screens.
- API Reference — the transfer API at
beamcore.b1m.ai.