API Reference

This document describes every REST API endpoint exposed by the Llamenos server. All endpoints are prefixed with /api. Requests and responses use JSON unless otherwise noted. All timestamps are ISO 8601 strings.

The API is the same whether the backend runs on Cloudflare Workers (with Durable Objects) or self-hosted (Node.js + PostgreSQL). The six Durable Objects — Identity, Settings, Records, ShiftManager, CallRouter, and Conversation — map to logical API domains described below.

Authentication

Llamenos supports two authentication mechanisms. All authenticated endpoints require one of these.

Schnorr signature auth (primary)

Every authenticated request carries a self-signed BIP-340 Schnorr token bound to the HTTP method and path.

Header format:

Authorization: Bearer {"pubkey":"<64_hex>","timestamp":<ms>,"token":"<128_hex>"}

Token construction:

  1. Build the message: llamenos:auth:<pubkey>:<timestamp_ms>:<METHOD>:<path>
  2. Hash with SHA-256
  3. Sign the hash with BIP-340 Schnorr using your secp256k1 secret key
  4. Encode as inline JSON with pubkey, timestamp, and token (hex signature) fields

Validation rules:

  • Token freshness: |now() - timestamp| <= 300,000 ms (5-minute window)
  • Signature is verified against the reconstructed message hash
  • The pubkey is looked up in the identity store to resolve the user record

Session token auth (WebAuthn)

After a WebAuthn authentication ceremony, the server issues a random 256-bit session token valid for 8 hours.

Authorization: Session <token_hex>

The server checks Session auth first. If the header starts with Session , Schnorr auth is not attempted, and vice versa.


Public endpoints

These endpoints require no authentication.

Health check

GET /api/health

Response:

{ "status": "ok" }

Configuration

GET /api/config

Returns public hub configuration, enabled channels, and server identity.

Response:

{
  "hotlineName": "Hotline",
  "hotlineNumber": "+1234567890",
  "channels": {
    "voice": true, "sms": false, "whatsapp": false,
    "signal": false, "rcs": false, "reports": true
  },
  "setupCompleted": true,
  "demoMode": false,
  "demoResetSchedule": null,
  "needsBootstrap": false,
  "hubs": [{ "id": "...", "name": "...", "slug": "..." }],
  "defaultHubId": "...",
  "serverNostrPubkey": "hex_64",
  "nostrRelayUrl": "wss://..."
}

Build verification

GET /api/config/verify

Returns build metadata for reproducible build verification.

Response:

{
  "version": "1.0.0",
  "commit": "abc1234",
  "buildTime": "2024-01-01T00:00:00Z",
  "verificationUrl": "https://github.com/...",
  "trustAnchor": "GitHub Release checksums + SLSA provenance"
}

IVR audio

GET /api/ivr-audio/:promptType/:language

Returns audio files fetched by telephony providers during calls.

  • promptType: [a-z_-]+
  • language: [a-z]{2,5}(-[A-Z]{2})?
  • Response: audio/wav binary

Messaging preferences

Token-validated public endpoints for subscriber preference management.

GET  /api/messaging/preferences?token=<hmac_token>
PATCH /api/messaging/preferences?token=<hmac_token>

PATCH body:

{ "status": "active", "language": "es" }

Authentication endpoints

Login

POST /api/auth/login

Body:

{ "pubkey": "hex64", "timestamp": 1709318400000, "token": "hex128" }

Response:

{ "ok": true, "roles": ["role-super-admin"] }

Rate limited: 10 attempts per IP. Returns 401 on invalid credentials.

Bootstrap (first admin)

POST /api/auth/bootstrap

Registers the first admin account. Fails with 403 if an admin already exists.

Body: Same as login. Response: Same as login. Rate limited: 5 attempts per IP.

Get current user

GET /api/auth/me

Auth: Required

Response:

{
  "pubkey": "hex64",
  "roles": ["role-super-admin"],
  "permissions": ["*"],
  "primaryRole": { "id": "role-super-admin", "name": "Super Admin", "slug": "super-admin" },
  "name": "Admin",
  "transcriptionEnabled": true,
  "spokenLanguages": ["en", "es"],
  "uiLanguage": "en",
  "profileCompleted": true,
  "onBreak": false,
  "callPreference": "phone",
  "webauthnRequired": false,
  "webauthnRegistered": true,
  "adminPubkey": "hex64",
  "adminDecryptionPubkey": "hex64"
}

Logout

POST /api/auth/me/logout

Auth: Required. If using Session auth, the token is revoked server-side.

Update profile

PATCH /api/auth/me/profile

Auth: Required

Body:

{
  "name": "string",
  "phone": "+1234567890",
  "spokenLanguages": ["en", "es"],
  "uiLanguage": "en",
  "profileCompleted": true,
  "callPreference": "phone"
}

All fields are optional. callPreference accepts "phone", "browser", or "both".

Update availability

PATCH /api/auth/me/availability

Auth: Required

Body:

{ "onBreak": true }

Update transcription preference

PATCH /api/auth/me/transcription

Auth: Required

Body:

{ "enabled": false }

Returns 403 if opt-out is not allowed by admin settings.


WebAuthn

Login flow

POST /api/webauthn/login/options

Auth: None. Returns publicKeyCredentialRequestOptions with a challengeId.

POST /api/webauthn/login/verify

Auth: None

Body:

{ "assertion": {}, "challengeId": "uuid" }

Response:

{ "token": "hex64", "pubkey": "hex64" }

Registration flow

POST /api/webauthn/register/options

Auth: Required

Body:

{ "label": "My Phone" }
POST /api/webauthn/register/verify

Auth: Required

Body:

{ "attestation": {}, "label": "My Phone", "challengeId": "uuid" }

Credential management

GET /api/webauthn/credentials

Auth: Required. Returns all registered credentials.

DELETE /api/webauthn/credentials/:credId

Auth: Required. Removes a credential.


Invites

Public

GET /api/invites/validate/:code

Rate limited: 5 attempts per IP.

Response:

{ "valid": true, "name": "...", "expiresAt": "..." }
POST /api/invites/redeem

Body:

{ "code": "...", "pubkey": "hex64", "timestamp": 1709318400000, "token": "hex128" }

Rate limited: 5 attempts per IP.

Authenticated

GET /api/invites

Permission: invites:read

POST /api/invites

Permission: invites:create

Body:

{ "name": "Jane Doe", "phone": "+1234567890", "roleIds": ["role-volunteer"] }
DELETE /api/invites/:code

Permission: invites:revoke


Volunteers

All volunteer endpoints require volunteers:read as a baseline permission.

GET /api/volunteers

Permission: volunteers:read

POST /api/volunteers

Permission: volunteers:create

Body:

{ "name": "string", "phone": "string", "roleIds": ["string"], "pubkey": "string" }
PATCH /api/volunteers/:targetPubkey

Permission: volunteers:update

Body: Partial volunteer fields (name, phone, roles, active, etc.)

DELETE /api/volunteers/:targetPubkey

Permission: volunteers:delete


Shifts

GET /api/shifts/my-status

Auth: Required (any role). Returns the current user’s shift status.

GET /api/shifts

Permission: shifts:read

POST /api/shifts

Permission: shifts:create

Body:

{
  "name": "Morning Shift",
  "startTime": "09:00",
  "endTime": "17:00",
  "days": [1, 2, 3, 4, 5],
  "volunteerPubkeys": ["hex64", "hex64"]
}
PATCH /api/shifts/:id

Permission: shifts:update

DELETE /api/shifts/:id

Permission: shifts:delete

Fallback ring group

GET /api/shifts/fallback

Permission: shifts:manage-fallback

PUT /api/shifts/fallback

Permission: shifts:manage-fallback

Body:

{ "fallbackPubkeys": ["hex64", "hex64"] }

Hub-scoped: All shift endpoints are also available at /api/hubs/:hubId/shifts/*.


Notes

All note endpoints require notes:read-own as a baseline. Clients must encrypt notes before sending (see the protocol specification for ECIES envelope format).

GET /api/notes?callId=...&page=1&limit=50

Permission: notes:read-own (own only) or notes:read-all (all notes)

Response:

{ "notes": [], "total": 0 }
POST /api/notes

Permission: notes:create

Body:

{
  "callId": "uuid",
  "encryptedContent": "hex",
  "authorEnvelope": { "wrappedKey": "hex", "ephemeralPubkey": "hex" },
  "adminEnvelopes": [{ "pubkey": "hex64", "wrappedKey": "hex", "ephemeralPubkey": "hex" }]
}
PATCH /api/notes/:id

Permission: notes:update-own

Body: Same shape as POST (with updated encrypted content and envelopes).

Hub-scoped: /api/hubs/:hubId/notes/*


Calls

GET /api/calls/active

Permission: calls:read-active (caller info redacted) or calls:read-active-full

GET /api/calls/today-count

Permission: calls:read-active

GET /api/calls/presence

Permission: calls:read-presence. Returns volunteer online/busy status.

GET /api/calls/history?page=1&limit=50&search=&dateFrom=&dateTo=

Permission: calls:read-history

POST /api/calls/:callId/answer

Permission: calls:answer. Returns 409 if the call was already answered.

POST /api/calls/:callId/hangup

Permission: calls:answer. Returns 403 if not your call.

POST /api/calls/:callId/spam

Permission: calls:answer. Flags the call as spam.

GET /api/calls/:callId/recording

Permission: calls:read-recording or answering volunteer.

Response: audio/wav binary with Cache-Control: private, no-store.

GET /api/calls/debug

Permission: calls:debug. Returns internal call state for troubleshooting.

Hub-scoped: /api/hubs/:hubId/calls/*


Conversations

GET /api/conversations?status=&channel=&page=1&limit=50

Permission: conversations:read-all or conversations:read-assigned (own + waiting)

Response:

{
  "conversations": [],
  "total": 0,
  "assignedCount": 0,
  "waitingCount": 0,
  "claimableChannels": ["sms", "whatsapp"]
}
GET /api/conversations/stats

Auth: Required

Response:

{ "total": 0, "active": 0, "waiting": 0, "closed": 0 }
GET /api/conversations/load

Permission: conversations:read-all. Returns per-volunteer conversation counts.

GET /api/conversations/:id

Auth: Required (access-checked per conversation).

GET /api/conversations/:id/messages?page=1&limit=50

Auth: Required (access-checked). Returns encrypted messages.

POST /api/conversations/:id/messages

Permission: conversations:send or conversations:send-any

Body:

{
  "encryptedContent": "hex",
  "readerEnvelopes": [{ "pubkey": "hex64", "wrappedKey": "hex", "ephemeralPubkey": "hex" }],
  "plaintextForSending": "Hello"
}

The plaintextForSending field is used for external channels (SMS, WhatsApp, Signal). The server sends the message via the channel adapter and then discards the plaintext.

PATCH /api/conversations/:id

Permission: conversations:update or assigned volunteer

Body:

{ "status": "closed", "assignedTo": "hex64" }
POST /api/conversations/:id/claim

Permission: conversations:claim + channel-specific (e.g., conversations:claim-sms)

Hub-scoped: /api/hubs/:hubId/conversations/*


Reports

Reports are a specialized type of conversation with metadata.type = "report".

GET /api/reports?status=&category=&page=1&limit=50

Permission: reports:read-all, reports:read-assigned, or reports:read-own

POST /api/reports

Permission: reports:create

Body:

{
  "title": "Report title",
  "category": "safety",
  "encryptedContent": "hex",
  "readerEnvelopes": [{ "pubkey": "hex64", "wrappedKey": "hex", "ephemeralPubkey": "hex" }]
}
GET /api/reports/:id

Permission: reports:read-all, reports:read-assigned, or own report

GET /api/reports/:id/messages?page=1&limit=100

Auth: Required (access-checked)

POST /api/reports/:id/messages

Permission: reports:send-message, reports:send-message-own, or assigned

Body:

{
  "encryptedContent": "hex",
  "readerEnvelopes": [],
  "attachmentIds": ["uuid"]
}
POST /api/reports/:id/assign

Permission: reports:assign

Body:

{ "assignedTo": "hex64" }
PATCH /api/reports/:id

Permission: reports:update

GET /api/reports/categories

Auth: Required

GET /api/reports/:id/files

Auth: Required (access-checked)

Hub-scoped: /api/hubs/:hubId/reports/*


Bans

POST /api/bans

Permission: bans:report

Body:

{ "phone": "+1234567890", "reason": "Spam caller" }
GET /api/bans

Permission: bans:read

POST /api/bans/bulk

Permission: bans:bulk-create

Body:

{ "phones": ["+1234567890", "+0987654321"], "reason": "Imported ban list" }
DELETE /api/bans/:phone

Permission: bans:delete

The :phone parameter is URL-encoded E.164 (e.g., %2B12125551234).

Hub-scoped: /api/hubs/:hubId/bans/*


Settings

Telephony provider

GET /api/settings/telephony-provider

Permission: settings:manage-telephony

PATCH /api/settings/telephony-provider

Permission: settings:manage-telephony

Body: TelephonyProviderConfig (provider type + credentials)

POST /api/settings/telephony-provider/test

Permission: settings:manage-telephony

Tests provider credentials without saving.

Messaging

GET /api/settings/messaging

Permission: settings:manage-messaging

PATCH /api/settings/messaging

Permission: settings:manage-messaging

Spam mitigation

GET /api/settings/spam

Permission: settings:manage-spam

PATCH /api/settings/spam

Permission: settings:manage-spam

Call settings

GET /api/settings/call

Permission: settings:manage

PATCH /api/settings/call

Permission: settings:manage

IVR languages

GET /api/settings/ivr-languages

Permission: settings:manage-ivr

PATCH /api/settings/ivr-languages

Permission: settings:manage-ivr

Body:

{ "enabledLanguages": ["en", "es", "zh"] }

IVR audio

GET /api/settings/ivr-audio

Permission: settings:manage-ivr

PUT /api/settings/ivr-audio/:promptType/:language

Permission: settings:manage-ivr Content-Type: application/octet-stream (raw audio bytes)

DELETE /api/settings/ivr-audio/:promptType/:language

Permission: settings:manage-ivr

Transcription

GET /api/settings/transcription

Auth: Required (any role)

Response:

{ "globalEnabled": true, "allowVolunteerOptOut": false }
PATCH /api/settings/transcription

Permission: settings:manage-transcription

Custom fields

GET /api/settings/custom-fields

Auth: Required (returns fields filtered by role)

PUT /api/settings/custom-fields

Permission: settings:manage-fields

Body:

{ "fields": [{ "id": "uuid", "name": "severity", "label": "Severity Rating", "type": "select", "required": true, "options": ["low", "medium", "high"], "visibleToVolunteers": true, "editableByVolunteers": true, "context": "call-notes", "order": 0 }] }

WebAuthn settings

GET /api/settings/webauthn

Permission: settings:manage

PATCH /api/settings/webauthn

Permission: settings:manage

Body:

{ "requireForAdmins": true, "requireForVolunteers": false }

Roles (PBAC)

GET /api/settings/roles

Auth: Required

POST /api/settings/roles

Permission: system:manage-roles

Body:

{
  "name": "Supervisor",
  "slug": "supervisor",
  "permissions": ["notes:read-all", "calls:read-history"],
  "description": "Can read all notes and call history"
}
PATCH /api/settings/roles/:id

Permission: system:manage-roles

DELETE /api/settings/roles/:id

Permission: system:manage-roles

Permissions catalog

GET /api/settings/permissions

Permission: system:manage-roles

Returns all available permissions organized by domain.

Setup state

GET /api/settings/setup

Permission: settings:manage

PATCH /api/settings/setup

Permission: settings:manage


Files

Upload flow

Chunked upload for encrypted file attachments.

POST /api/uploads/init

Permission: files:upload

Body:

{
  "totalSize": 1048576,
  "totalChunks": 4,
  "conversationId": "uuid",
  "recipientEnvelopes": [],
  "encryptedMetadata": [{ "pubkey": "hex64", "encryptedContent": "hex", "ephemeralPubkey": "hex" }]
}

Response:

{ "uploadId": "uuid", "totalChunks": 4 }
PUT /api/uploads/:id/chunks/:chunkIndex

Permission: files:upload Content-Type: application/octet-stream (raw encrypted chunk bytes)

Response:

{ "chunkIndex": 0, "completedChunks": 1, "totalChunks": 4 }
POST /api/uploads/:id/complete

Permission: files:upload

Response:

{ "fileId": "uuid", "status": "complete" }

Returns 400 if not all chunks have been uploaded.

GET /api/uploads/:id/status

Permission: files:upload

Download

GET /api/files/:id/content

Permission: files:download-own (if recipient) or files:download-all

Response: application/octet-stream (encrypted file bytes)

GET /api/files/:id/envelopes

Permission: files:download-own or files:download-all

Non-admin users receive only their own envelope.

GET /api/files/:id/metadata

Permission: files:download-own or files:download-all

POST /api/files/:id/share

Permission: files:share

Re-encrypts the file key for a new recipient.


Blasts (message broadcasting)

Subscribers

GET /api/blasts/subscribers?page=&limit=&tag=&status=

Auth: Required

DELETE /api/blasts/subscribers/:id

Auth: Required

GET /api/blasts/subscribers/stats

Auth: Required

POST /api/blasts/subscribers/import

Auth: Required

Body:

{ "subscribers": [{ "phone": "+1234567890", "tags": ["alerts"] }] }

Blasts

GET /api/blasts

Auth: Required

POST /api/blasts

Auth: Required

Body:

{
  "name": "Emergency alert",
  "content": { "sms": "Alert text", "whatsapp": "Alert text" },
  "targetChannels": ["sms", "whatsapp"],
  "targetTags": ["alerts"],
  "targetLanguages": ["en", "es"]
}
GET /api/blasts/:id

Auth: Required

PATCH /api/blasts/:id

Auth: Required

DELETE /api/blasts/:id

Auth: Required

POST /api/blasts/:id/send

Auth: Required. Sends the blast immediately.

POST /api/blasts/:id/schedule

Auth: Required

Body:

{ "scheduledAt": "2026-03-01T12:00:00Z" }
POST /api/blasts/:id/cancel

Auth: Required. Cancels a scheduled blast.

Blast settings

GET /api/blasts/settings

Auth: Required

PATCH /api/blasts/settings

Auth: Required

Hub-scoped: /api/hubs/:hubId/blasts/*


Hubs

Multi-tenant hub management.

GET /api/hubs

Auth: Required (filtered by membership; super admin sees all)

POST /api/hubs

Permission: system:manage-hubs

Body:

{ "name": "NYC Hub", "slug": "nyc", "description": "New York City operations", "phoneNumber": "+1234567890" }
GET /api/hubs/:hubId

Auth: Required (membership checked)

PATCH /api/hubs/:hubId

Permission: system:manage-hubs

Hub members

POST /api/hubs/:hubId/members

Permission: volunteers:manage-roles

Body:

{ "pubkey": "hex64", "roleIds": ["role-volunteer"] }
DELETE /api/hubs/:hubId/members/:pubkey

Permission: volunteers:manage-roles

Hub key management

GET /api/hubs/:hubId/key

Auth: Required (hub member). Returns only the requesting user’s ECIES-wrapped hub key envelope.

PUT /api/hubs/:hubId/key

Permission: system:manage-hubs

Body:

{ "envelopes": [{ "pubkey": "hex64", "wrappedKey": "hex", "ephemeralPubkey": "hex" }] }

Setup wizard

GET /api/setup/state

Auth: Required

PATCH /api/setup/state

Permission: settings:manage

POST /api/setup/complete

Permission: settings:manage

Body:

{ "demoMode": false }

Also creates a default hub if none exists.

Channel tests

POST /api/setup/test/signal

Permission: settings:manage-messaging

Body:

{ "bridgeUrl": "http://signal-cli:8080", "bridgeApiKey": "secret" }
POST /api/setup/test/whatsapp

Permission: settings:manage-messaging

Body:

{ "phoneNumberId": "123456", "accessToken": "EAAx..." }

Audit log

GET /api/audit?page=1&limit=50&actorPubkey=&eventType=&dateFrom=&dateTo=&search=

Permission: audit:read

Response:

{
  "entries": [{
    "id": "uuid",
    "event": "note.created",
    "actorPubkey": "hex64",
    "details": {},
    "createdAt": "2026-01-01T00:00:00Z",
    "previousEntryHash": "hex64",
    "entryHash": "hex64"
  }],
  "total": 100
}

The audit log uses a SHA-256 hash chain (previousEntryHash + entryHash) for tamper detection.

Hub-scoped: /api/hubs/:hubId/audit/*


WebRTC

GET /api/telephony/webrtc-token

Auth: Required

Returns a provider-specific WebRTC token for in-browser call answering.

Response:

{ "token": "string", "provider": "twilio", "identity": "hex64" }

Returns 400 if call preference is set to phone only.

GET /api/telephony/webrtc-status

Auth: Required

Response:

{ "available": true, "provider": "twilio" }

Device provisioning

For linking new devices to an existing account via ephemeral ECDH key exchange.

POST /api/provision/rooms

Auth: None (new device has no auth)

Body:

{ "ephemeralPubkey": "hex66" }

Response:

{ "roomId": "uuid", "token": "random_string" }
GET /api/provision/rooms/:id?token=<token>

Auth: None

Response:

{
  "status": "waiting",
  "encryptedNsec": "hex",
  "primaryPubkey": "hex64",
  "ephemeralPubkey": "hex66"
}

Status transitions: waiting -> ready -> consumed. Rooms expire after ~5 minutes.

POST /api/provision/rooms/:id/payload

Auth: Required (primary device must be authenticated)

Body:

{
  "token": "string",
  "encryptedNsec": "hex",
  "primaryPubkey": "hex64"
}

Push notifications (mobile)

POST /api/devices/register

Auth: Required

Body:

{
  "platform": "ios",
  "pushToken": "apns_device_token",
  "voipToken": "ios_voip_push_token",
  "wakeKeyEnvelope": { "wrappedKey": "hex", "ephemeralPubkey": "hex" }
}

Response:

{ "deviceId": "uuid" }

Push notifications use a two-tier encryption scheme: a wake key (no PIN required) for notification metadata, and the identity key (PIN required) for sensitive content.


Telephony webhooks

These endpoints are called by telephony providers, not by clients. Each request is validated by the provider’s webhook signature.

POST /api/telephony/incoming
POST /api/telephony/language-selected
POST /api/telephony/captcha
POST /api/telephony/volunteer-answer
POST /api/telephony/call-status
POST /api/telephony/wait-music          (also GET)
POST /api/telephony/queue-exit
POST /api/telephony/voicemail-complete
POST /api/telephony/call-recording
POST /api/telephony/voicemail-recording

Hub routing is via the ?hub=<hubId> query parameter.


Messaging webhooks

Called by messaging providers. Each adapter validates its own webhook signature.

GET  /api/messaging/whatsapp/webhook    (Meta webhook verification)
GET  /api/messaging/rcs/webhook         (Google RBM webhook verification)
POST /api/messaging/:channel/webhook?hub=<hubId>

Supported channels: sms, whatsapp, signal, rcs.


Hub-scoped routes

All of the following routes are also available with a /api/hubs/:hubId/ prefix, which scopes them to a specific hub:

  • /api/hubs/:hubId/shifts/*
  • /api/hubs/:hubId/bans/*
  • /api/hubs/:hubId/notes/*
  • /api/hubs/:hubId/calls/*
  • /api/hubs/:hubId/audit/*
  • /api/hubs/:hubId/conversations/*
  • /api/hubs/:hubId/reports/*
  • /api/hubs/:hubId/blasts/*

When using hub-scoped routes, the hubContext middleware resolves hub-specific permissions for the user.


Error responses

All error responses follow this format:

{ "error": "Human-readable error message" }

Common HTTP status codes:

CodeMeaning
400Bad request (malformed body, missing fields, validation failure)
401Unauthorized (missing or invalid auth token)
403Forbidden (valid auth but insufficient permissions)
404Not found
409Conflict (e.g., call already answered, resource already exists)
429Too many requests (rate limited)
500Internal server error

Permission reference

Permissions follow the domain:action format. Users are assigned roles, and each role bundles a set of permissions. Effective permissions are the union of all assigned roles.

Wildcard * grants all permissions. Domain wildcard domain:* grants all actions in that domain.

DomainPermissions
callsanswer, read-active, read-active-full, read-history, read-presence, read-recording, debug
notescreate, read-own, read-all, read-assigned, update-own
reportscreate, read-own, read-all, read-assigned, assign, update, send-message-own, send-message
conversationsread-assigned, read-all, claim, claim-sms, claim-whatsapp, claim-signal, claim-rcs, claim-web, claim-any, send, send-any, update
volunteersread, create, update, delete, manage-roles
shiftsread-own, read, create, update, delete, manage-fallback
bansreport, read, create, bulk-create, delete
invitesread, create, revoke
settingsread, manage, manage-telephony, manage-messaging, manage-spam, manage-ivr, manage-fields, manage-transcription
auditread
blastsread, send, manage, schedule
filesupload, download-own, download-all, share
systemmanage-roles, manage-hubs, manage-instance

Default roles

RoleSlugKey permissions
Super Adminrole-super-admin* (all permissions)
Hub Adminrole-hub-adminvolunteers:*, shifts:*, settings:*, audit:read, bans:*, invites:*, notes:read-all, reports:*, conversations:*, calls:*, blasts:*, files:*
Reviewerrole-reviewernotes:read-assigned, reports:read-assigned, reports:assign, reports:update, conversations:read-assigned, conversations:send, files:download-own, files:upload
Volunteerrole-volunteercalls:answer, calls:read-active, notes:create, notes:read-own, notes:update-own, conversations:claim, conversations:send, conversations:read-assigned, bans:report, files:upload, files:download-own
Reporterrole-reporterreports:create, reports:read-own, reports:send-message-own, files:upload, files:download-own

Development / test endpoints

Available only in development environments.

POST /api/test-reset            (full reset, requires X-Test-Secret header)
POST /api/test-reset-no-admin   (reset without admin)
POST /api/test-reset-records    (light reset, preserves identity/settings)