401 and 403 responses look like, and how to get sandbox credentials. It is written for the engineer making the first call.
How requests authenticate
Send the token in theAuthorization header on every request:
POST /v1/oauth/tokenis the token endpoint. It carries no bearer token; the client id and secret travel as HTTP Basic credentials, or as body fields.POST /v1/webhooks/{platform}is called by a destination platform, not by you. It carries no bearer token either. The delivery authenticates by itsX-Signature-HMAC-SHA256-{n}header: a Base64-encoded HMAC-SHA256 of the raw request body, computed with one of the active signing keys. The numeric suffix carries no meaning; during key rotation a delivery may carry several such headers, and any one that matches an active key verifies it. Comparison is timing-safe, and a delivery that cannot be verified is rejected with400, not queued. composerID’s own outbound webhooks use the same convention.
Credentials
- Personal sandbox key
- OAuth 2.0 client credentials
A personal key is a bearer token that carries every scope. It is the quickest route to a first request and is meant for exploring the API by hand. The reference sandbox prints one at startup (
demo_...) unless COMPOSER_API_KEYS supplies your own as a comma-separated list. Keys are compared in constant time.Scopes
Every route declares the scope it needs. A token without that scope receives403 insufficient_scope, never a silent success. A personal key carries all six scopes; a client credentials token carries the scopes the client was granted, narrowed further by the scope it asked for.
Four routes need no scope. The
GET /v1 index, which any authenticated principal may call. POST /v1/oauth/token, which authenticates the client itself. POST /v1/webhooks/\{platform\}, which authenticates by signature. And POST /v1/sandbox/keys, which carries no credential at all: it is how a caller gets their first one, so it is rate-limited per address and answers 201 with Cache-Control: no-store.
Token lifetime and rotation
A token expires3600 seconds after it is issued (expires_in). Request a new one when a call returns 401; the token endpoint response is never cached. Tokens are self-contained rather than stored. Each one is a base64url JSON payload carrying the subject, its scopes, its tenant and an expiry, a dot, and an HMAC-SHA256 signature over that payload under the issuer’s secret. Nothing is written down: a token is valid when its signature verifies and its expiry is in the future. That is what lets the hosted sandbox run on serverless instances that share no memory, and it is why an operator must set COMPOSER_TOKEN_SECRET to the same value everywhere. Without it each instance generates its own secret, and a token issued by one is rejected by the next.
Rotate a client by issuing it a new client secret, without a redeploy. Personal keys are issued per person, so rotation, revocation and the audit trail trace to a named individual rather than to a shared secret.
Errors
Token endpoint errors follow RFC 6749 section 5.2, with
error and error_description fields.
Getting sandbox credentials
There is no account to create and no sign-in. The documentation is open, and the sandbox issues its own credentials. The hosted sandbox answers athttps://sandbox.composer.id/v1. One unauthenticated call mints a tenant with everything needed to publish:
expires_at. Mint another set whenever you need one, at up to five sets an hour per address. Each tenant sees only the intents it minted, and an inbound webhook is routed to the tenant that minted the intent it names.
Running the sandbox locally is the other way in: python3 -m service.server prints a personal key, a demo client id and secret, and a webhook signing key at startup. See the quickstart.
This is the reference sandbox: destinations are mock tenants, and its
GET /v1 index reports whether its state is durable. It answers at https://sandbox.composer.id/v1 today; a dedicated sandbox.composer.id host is planned and not live yet. The production service behind api.composer.id is in build and will not expose POST /sandbox/keys.Next steps
Quickstart
Start the sandbox and make the first call.
API reference
Every
/v1 route and the scope it requires.Manuscript API
A separate API with per-customer keys issued per programme.