BeaamAPI conventions
The response envelope, error shape, authentication, and the rules every capability follows.
The response envelope, error shape, authentication, and the rules every capability follows.
Changed 1 September 2026. An earlier version of this page described outbound webhook signing and a CloudEvents payload envelope that Beaam did not actually implement. Signing is now real and is documented below as built; the envelope claim was wrong and has been removed. If you wrote a client against the previous page, the two sections to re-read are Outbound webhooks and Versioning.
Beaam follows widely-adopted open standards where it can, so a client — monitoring tools, event platforms, or your own code — can integrate without bespoke work. Where this document references an external standard, that standard (not this document) is the source of truth.
Versioning
- HTTP endpoints are versioned in the path:
/api/v1/.... Breaking changes ship under a new major version. - Adding fields to a response or a webhook payload is not breaking. Consumers MUST ignore unknown fields.
- Deprecations are announced in the changelog.
Authentication
- API requests authenticate with a bearer token:
Authorization: Bearer <token>. - Create a key under Settings → API keys in the app. Keys can be revoked.
- Treat keys as secrets. Never embed one in client-side code.
Choosing the organization
- Without a header, a request acts in the organization the credential belongs to: an API key's own organization, or a signed-in session's active one.
X-Beaam-Org: <organization id>names it explicitly. The caller must be a member. A key bound to one organization cannot name a different one.- A refused organization returns HTTP 403 with
{ "ok": false, "code": "org_forbidden", "error": "<reason>" }. The reason is worded for the user, for example a membership that has been removed. - The mobile app sends this header on every call. Its organization is chosen on the phone and does not follow the web app's switcher.
One endpoint, one envelope
Every feature is a capability, and every capability is reachable identically from the API, the CLI, and MCP. The capability name is the path; the body is its input:
curl -X POST https://app.beaam.app/api/v1/list-stacks \
-H "Authorization: Bearer $BEAAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
Responses share one envelope, so a client written once works for every capability:
{ "ok": true, "data": { } }
{ "ok": false, "error": "Authentication required." }
On a failure error is a string, not an object — the message, ready to show
a user. An earlier version of this page documented it as { "message": … },
which no version of the API has ever returned; a client written from that page
read error.message as undefined and reported nothing. The HTTP status
carries the class of failure: 400 invalid input, 401 no or bad credential,
404 unknown capability (also what an operator-only capability returns to
everyone else, deliberately), 413 body too large, 429 rate limited
(retry-after is set), 500 the capability itself failed.
A refused operation is a 200, not an error
This is the one thing to get right, and the envelope above does not show it.
ok: false means the request failed — no credential, bad JSON, unknown
capability, a crash. It does not cover an operation Beaam understood and
declined: a Free-tier limit, a credential the provider rejected, an invitation
that expired, a person who is not a member. Those are answers, not faults, so
they come back as 200 with ok: true, and the outcome is inside data:
{ "ok": true, "data": { "status": "error",
"message": "Free includes one organization. Upgrade to Solo ($19/mo) to create more workspaces — your current organization is unaffected." } }
On a refusal status is "error" and message is written to be shown to a
person as-is. On success it is "ok" — or, for the connect-* operations,
"connected". About half the capabilities can refuse this way — every
connect-*, the billing and membership operations, and most writes. Each one is
marked Can refuse in-band in the API reference, with an example
of the refusal, and the OpenAPI download carries a refused example for it.
A status field on its own does not mean an operation can refuse: some report a
state instead (recorded, pending, ready, not_found). Only a status
that can be "error" is a refusal.
So a client MUST check three things, in order:
- the HTTP status — was the request itself accepted;
ok—falsemeans readerror(a string);data.status—"error"means readdata.messageand show it.
A client that stops after step 2 reports every tier refusal and every rejected provider credential as a success. That is the most common way to integrate this API wrongly, and until this section existed the published page gave no hint the case was there.
The machine-readable list of every operation is
/api/v1/manifest — fetch it to
generate a typed client rather than writing one by hand.
Outbound webhooks
A webhook notification channel receives a JSON POST when an alert fires.
The payload is Beaam's own flat shape, not an event-envelope standard:
{
"source": "beaam",
"event": "alert",
"service": "drop",
"trigger": "broken",
"duration_ms": 420000,
"duration": "7m",
"guidance": "Checkout has been failing for 7 minutes.",
"summary": "drop is broken",
"sent_at": "2026-09-01T02:14:07.113Z"
}
event is alert for a real alert and test for one sent from the
notifications page. correlated is present only when Beaam has attributed the
incident to a change.
Verifying a webhook came from Beaam
Every outbound webhook is signed with the
Standard Webhooks scheme. Each channel has
its own secret, shown on the notifications page in the app (or via the
get-webhook-signing-secret capability).
Three headers accompany every request:
| Header | Meaning |
|---|---|
webhook-id |
Unique id for this message. Treat a repeat as the same delivery. |
webhook-timestamp |
Unix seconds at send time. |
webhook-signature |
Space-separated list of v1,<base64 HMAC-SHA256>. |
To verify:
- Take the part of the secret after
whsec_and base64-decode it — those bytes are the HMAC key. - Compute
base64(HMAC_SHA256(key, "{webhook-id}.{webhook-timestamp}.{body}"))over the raw request body, before any JSON parsing or re-serialisation. - Constant-time compare it against each signature in
webhook-signature. - Reject if
webhook-timestampis more than 5 minutes from now. This is what makes a captured request unreplayable.
Any off-the-shelf Standard Webhooks library verifies Beaam's signatures without Beaam-specific configuration.
Rotation replaces the secret immediately. rotate-webhook-signing-secret
issues a new one and the previous one stops verifying at once — Beaam does not
send overlapping signatures during a rotation, so update your endpoint in the
same sitting.
Inbound webhooks
Webhooks Beaam receives — from Polar, Resend and Vonage — are verified against each provider's own scheme and fail closed: an unset secret or any mismatch is rejected rather than processed.
This page is generated from docs/api/conventions.md in the Beaam repository, so it stays in step with the implementation. Something unclear or wrong?Tell us — and see theAPI reference for the full list of operations.