API keys

A scoped secret that lets an external system call the Carbon API on your behalf, with its own permissions.

An is a secret string that authenticates programmatic calls to Carbon. Where a person signs in and Carbon reads their permissions from a session, a script sends its key on every request and Carbon reads the key's own permissions instead. A key belongs to one company and carries an explicit set of scopes, so you can hand a partner or a job exactly the access it needs and nothing more.

Create and manage keys under Settings → API Keys. Every key you see is listed by Name, a masked Key preview (only the last five characters, e.g. crbn_•••abcde), its Scopes, Rate Limit, who created it, and when it expires.

Creating a key

Click New API Key and give it a Name (unique within your company). Optionally set Expires At — a future date after which the key stops working; leave it blank and the key never expires. Then grant scopes with the permission matrix (covered below) and save.

Carbon shows the full key exactly once, in a dialog headed "You can only see this key once. Store it safely." Copy it right then. Carbon never stores the raw key: it keeps only a SHA-256 hash for lookup and the five-character preview for display. There is no way to reveal a key again later, so if you lose it, delete the key and create a new one.

HEADS UP

The key is a bearer secret

Anyone holding the key can act as it, within its scopes. Treat it like a password: store it in a secret manager, never commit it to source control, and rotate it (delete and recreate) if it leaks.

The full format is crbn_ followed by a random token. The same dialog also hands you a ready-to-paste MCP command that wires the key into an AI assistant — see The MCP endpoint below.

How a key authenticates a request

Send the key as a bearer token on every request. The hosted Data API lives at rest.carbon.ms:

http
GET /salesOrder?select=id,salesOrderId,status
Host: rest.carbon.ms
Authorization: Bearer crbn_your_key_here

Carbon hashes the incoming key, looks up the matching record, and checks two things before it lets the request through:

  • Expiry. If the key has an expiresAt in the past, the request is rejected.
  • Rate limit. The request is counted against the key's window; if the window is full, the request is rejected.

There is no separate login step and no token to refresh. The header is the whole handshake.

NOTE

Bearer at the edge, carbon-key underneath

rest.carbon.ms takes the key as Authorization: Bearer crbn_… and forwards it internally as the carbon-key header that the database reads. If you run Carbon yourself and call PostgREST directly, send carbon-key: crbn_… against /rest/v1/<table> instead. The MCP endpoint that fronts the Carbon API takes the same Bearer token either way.

Permission scoping

A key does not inherit the permissions of the person who created it. It acts with exactly the scopes stored on the key, and only for the company it belongs to. A brand-new key with no scopes granted can authenticate but can read and write nothing.

You grant scopes in the permission matrix when you create or edit the key. Each cell is a module_action pair — the same shape Carbon uses for a user's permissions. So a key scoped to view sales and create inventory can read salesOrder rows and insert inventory records, but a call that needs, say, purchasing_update is refused with "API key lacks required permissions" unless you granted that exact scope.

FieldType
The set of permissions the key carries, e.g. sales_view, inventory_create. Empty means no access. Edit them anytime from the key's form.
A key is bound to the single company it was created under. It can never reach another company's data, even one you also belong to.

Because scopes are checked on every request against the key's own record, tightening a key's scopes (or deleting the key) takes effect immediately — there is no cached session to wait out.

Rate limiting

Every key allows 60 requests per minute. Carbon counts each authenticated request against the current window and rejects anything over the limit with "Rate limit exceeded" until the window rolls over.

The limit is platform-controlled, not something you set: the key's form has no rate-limit field, and insertApiKey/updateApiKey strip the columns from whatever is submitted (apps/erp/app/modules/settings/settings.service.ts:1282). The Rate Limit column in the key list reports the limit in force; it is not an input.

The limit is counted per key, not per company, so one integration burning its allowance does not throttle another.

A rejected request comes back 429 with X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and Retry-After (packages/auth/src/services/auth.server.ts:255). Read Retry-After and wait it out rather than hammering through the rejection.

What the key unlocks

One key unlocks two surfaces. Reach for the Carbon API first; drop to the Data API only when you need raw table access.

The Carbon API

The Carbon API is Carbon's service layer — the safe way to read and write, running the same validation, recalculation, and permission checks the app itself uses. Reach it over plain HTTP at POST /api/v1/{module}/{operation} — the key goes in an Authorization: Bearer header, and the full operation catalogue is described by /api/v1/openapi.json. The same surface is also exposed over MCP: the creation dialog hands you a ready-to-paste command that registers the key with an AI assistant, which then operates strictly within the key's scopes. See Connect over MCP.

The Data API

The same key also unlocks the Data API — direct REST access to every table and view, hosted at rest.carbon.ms (the full endpoint catalogue is generated in the reference). The same row-level security that governs the app governs these calls: the key is scoped to its company and permissions by the database itself, not just the application layer, so it can only ever touch data its scopes allow.

HEADS UP

You are outside the service layer

The Data API writes straight to tables, so Carbon does not recalculate the derived values — totals, statuses, ledger entries — that it maintains when you write through the Carbon API. It's the escape hatch: reach for the Carbon API first, and use the Data API when it doesn't cover what you need and you know exactly what the table touches.

The full endpoint catalogue, with request and response shapes per resource, lives in the generated Data API reference.

Keys versus webhooks

API keys and webhooks are two halves of an integration, pulling in opposite directions:

  • An API key lets you call Carbon — poll for data, create records, run reports on demand.
  • A webhook lets Carbon call you — an HTTP callback the moment a subscribed record changes.

They are independent: a webhook is not authenticated with an API key (Carbon does not sign webhook payloads at all), and an API key does not subscribe to anything. The common pattern is to use both together: let a webhook tell you that a record changed, then use your API key to fetch the full, current record from the REST API.