Authentication guide — Muster public API
1. TL;DR
- Every authenticated call sends
Authorization: Bearer <jwt>. - The JWT is issued by Safe Signals at
integration-registration time. Contact
integrations@safesignals.ioto register. - The JWT identifies your integration, not a specific department. Per-department authorization is a separate step the customer chief performs in their admin console.
- Sandbox JWTs verify with the same substrate but authorize
against fake departments at
api-sandbox.safesignals.io.
2. JWT anatomy
Muster license JWTs are RS256-signed. Header:
{"alg":"RS256","typ":"JWT"}
Payload (excerpt — see proto/muster/v1/ for the authoritative field list):
{
"iss": "licensing.safesignals.io",
"aud": "api.safesignals.io",
"sub": "integration:eso-records-v1.2",
"iat": 1735689600,
"exp": 1893456000,
"jti": "jwt-uuid-here",
"tier": "tier2",
"features": ["api_read_v1"],
"department": {"id":"integration_root","name":"ESO Records","country":"US"}
}
Verify the signature against the public keys returned by
GET /v1/trust/keys (see below).
3. The two-step onboarding
Step 1 — Muster-issued integration credential (once per integrator)
Contact integrations@safesignals.io. Include:
- Legal integrator name (e.g., "ESO Solutions LLC").
- Integration display name (e.g., "ESO Records Integration v1.2").
- Producer capabilities you'll write to Muster
(e.g.,
ingest.incident_created,ingest.unit_checked_in). - Consumer capabilities you'll read from Muster
(e.g.,
read.incident_events,read.neris_projection). - Contact email for incident notifications.
Safe Signals issues you one integration-scoped JWT. It expires after 1 year and is renewed manually — Safe Signals will contact your integration email 30 days before expiry to coordinate the rotation.
Step 2 — Chief-issued per-department authorization
For each customer department where you want to consume data, the department's chief goes to admin console → Integrations → finds your integration in the marketplace → clicks "Authorize" → confirms which capabilities to grant.
Muster stores an authorized_integrations doc at
departments/{deptId}/authorized_integrations/{integrationId}.
Your JWT's Layer 1 list/get calls only resolve for
authorized departments. Chief can revoke at any time.
How the gate works. On every request whose URL targets a
department other than the one your JWT is scoped to (e.g.,
your integrator JWT is scoped to integrator-root and you're
reading /v1/departments/dept-a/…), the server:
- Resolves your JWT's
subclaim to your registered integration id via the integrations registry. - Reads
departments/{deptId}/authorized_integrations/{integrationId}. - Missing → 403 with the message
Integration "{id}" is not authorized for department "{dept}". The customer chief must click "Authorize" on this integration in their admin console → Integrations tab. revoked_atfield set → 403 with the revocation timestamp.- Present and not revoked → the request proceeds to the normal Layer 1 read path.
Gate lookups are cached with a 1-minute TTL, so new authorizations take effect within a minute of the chief clicking "Authorize". Revocations take effect on the same TTL.
Operator surface. Chiefs manage per-department
authorizations from the admin console's Integrations page —
"Authorized integrations" panel. Under the hood this hits
three endpoints on admin.safesignals.io:
GET /v1/admin/authorized-integrations?department_id=…— list every integration currently authorized (or revoked) for the department.POST /v1/admin/authorized-integrations— grant. Body:{"department_id": "…", "integration_id": "…"}. Idempotent: re-authorizing an active integration is a no-op; re-authorizing a revoked one clears the revocation.DELETE /v1/admin/authorized-integrations/{integrationId}— revoke. Body:{"department_id": "…"}. Preserves the originalauthorized_atfor audit; setsrevoked_atandrevoked_by_uid. Re-revoking is a no-op.
All three require chief / admin role in the target
department (cross-department escalation is refused). Every
grant + revoke is audit-logged (action: integrationAuthorized / integrationRevoked).
Customer-scoped JWTs bypass the gate. Muster's own mobile
app, admin console, and firefighter portal use JWTs scoped
directly to a specific customer department (department.id
matches the URL). Those requests never invoke the gate — the
same-dept check is the entire enforcement.
Rule 25 is preserved end-to-end: each department decides which integrator sees their data.
4. Auth failure modes
| Status | auth_rejection_code |
What it means |
|---|---|---|
| 401 | missingBearer |
No / malformed Authorization header |
| 401 | invalidJwt |
JWT signature or structural validation failed |
| 401 | wrongAudience |
aud claim isn't api.safesignals.io |
| 401 | revoked |
JTI is on the revocation blocklist |
| 401 | featureNotAvailable |
JWT valid but tier lacks api_read_v1 |
| 403 | (no auth_rejection_code; response body's reason) |
Department not authorized for your integration |
5. Verifying event signatures
Every event has an Ed25519 signature from the originating device. To independently verify an event you retrieved from Layer 1 or Layer 3:
GET /v1/trust/keys— returns Muster's current + historical Ed25519 signing keys (which Muster uses to sign its own artifacts — archives, license JWTs, etc.).- Per-event device signatures are exposed as
payload_json.signaturewhen the client requests it via?include_signatures=1(Layer 1 read). Compare against the event'sorigin_devicepublic key (surface in the admin console under Devices → Public Keys — accessible via Layer 1 only where the customer has authorized this).
6. Rotating your JWT
Contact integrations@safesignals.io when:
- Your integration's private key is suspected compromised.
- Your JWT expires (Safe Signals sends a renewal email 30 days before expiry).
- You want to declare additional producer / consumer capabilities and need a new capability set.
Rotation is manual — no self-service in Phase 7. When your new JWT lands, discard the old one in your codebase. Safe Signals does NOT retire the old JWT immediately unless you request revocation; overlap avoids race conditions on long-lived callers.
7. Muster key rotation
Muster's issuer key rotates on a documented schedule (roughly
annually or on suspected compromise). Older JWTs remain
verifiable via the historical public keys from
GET /v1/trust/keys. Your caller code should:
- Fetch
/v1/trust/keyson startup. - Cache the response for up to 1 hour.
- On JWT verification failure, refetch (the key may have rotated since your last fetch).
8. Sandbox vs. production
| Sandbox | Production | |
|---|---|---|
| Base URL | api-sandbox.safesignals.io |
api.safesignals.io |
| Data | Fake departments, fake incidents | Real customer data |
| JWTs | Separately-issued sandbox JWT | Production JWT |
| Rate limits | Higher (integration testing tolerated) | Per your tier |
| SLA | Best-effort | See status.safesignals.io/api |
| Signing keys | Sandbox-signed | Production-signed |
Sandbox JWTs never work against production and vice versa — they use different signing keys.