API & webhooks
OpenAPI 3.1 spec served from the backend itself — single source of truth. Webhooks are HMAC-signed with a 5-minute replay window. Every endpoint is tenant-scoped and role-guarded.
01 · Authentication
Every API key is bound to a tenant (firm_id) and a role (owner / admin / operator / viewer). The raw token is displayed exactly once at issuance. We store only its SHA-256 hash; we cannot recover a lost key — rotate it from the admin console.
GET /api/v1/workflows/acme-legal/list Authorization: Bearer sk_live_… X-Request-Id: 7f3e1a… # optional client-supplied; echoed in response
Cross-tenant access returns 403 forbidden unconditionally — the API key's bound firm_id must match the URL.
02 · Endpoint surfaces
| Surface | Path prefix | Role gate |
|---|---|---|
| Workflows | /api/v1/workflows/{firm_id}/ | operator+ (write) |
| Document Intelligence | /api/v1/document-intelligence/{firm_id}/ | operator+ |
| RAG knowledge base | /api/v1/rag/{firm_id}/ | viewer+ |
| CRM | /api/v1/crm/{firm_id}/ | operator+ (write) |
| Communication | /api/v1/communication/{firm_id}/ | operator+ |
| Scheduling | /api/v1/scheduling/{firm_id}/ | operator+ |
| Compliance officer | /api/v1/compliance/{firm_id}/ | viewer+ |
| Audit explorer | /api/v1/audit/{firm_id}/ | admin+ |
| Approval Inbox | /api/v1/approvals/{firm_id}/ | varies by action tier |
| Webhook subscriptions | /api/v1/webhooks/{firm_id}/ | admin+ |
| System catalog (public) | /api/v1/systems | public |
| Health / metrics | /status · /metrics | public |
03 · Webhook signing
Outbound webhooks include two headers a receiver must verify before processing the body:
X-Sonoda-Timestamp: 1716718230 # unix seconds X-Sonoda-Signature: v1=a7c3… # hex HMAC-SHA256 X-Sonoda-Event: workflow.completed
The signature is computed as:
signed_payload = f"{timestamp}.{request_body}" signature = HMAC_SHA256(secret, signed_payload).hex()
Reject the request if the timestamp is more than 300 seconds in the past (replay protection) or if the computed signature doesn't match in constant time.
04 · Event payload schemas
Every emitted event carries an envelope plus a typed body. Envelope shape:
{
"event_id": "evt_…", # unique, idempotent retry key
"event_type": "workflow.completed",
"emitted_at": "2026-05-26T14:23:07.421Z",
"hlc": "017f9a3b-000c-7e21-9f02", # Hybrid Logical Clock
"firm_id": "acme-legal",
"actor": { "type": "agent", "id": "workflow_engine" },
"data": { /* event-type-specific */ }
}
Subscribed event types (incomplete list):
| workflow.started | workflow execution begun |
| workflow.step.completed | one step closed; includes step id, capability called, outcome |
| workflow.completed | terminal state reached |
| workflow.failed | workflow halted; includes error envelope |
| approval.requested | action requires human authorization; includes tier + context |
| approval.granted | authority graph approved the action |
| approval.denied | approver rejected; reason logged |
| audit.entry.written | fired for every audit log append (firehose; opt-in subscription) |
| anomaly.detected | anomaly detection raised a flag; includes score + axis |
| document.classified | document type assigned + routed |
| crm.contact.stale | health scoring crossed staleness threshold |
05 · Error contract
Every error response has the same shape regardless of which router raised it:
{
"error": {
"code": "workflow.invalid_transition",
"message": "Cannot move from 'failed' to 'running'",
"hint": "Restart the workflow via POST /workflows/{firm}/{id}/restart",
"request_id": "req_7f3e1a…",
"docs_url": "https://sonodadynamics.com/api.html#errors"
}
}
Codes are namespaced (auth.*, workflow.*, rate_limit.*, tenant.*) and are stable across versions — we add new ones, we don't rename existing ones.
06 · Rate limits
Every response includes the standard rate-limit headers:
X-RateLimit-Limit: 120 X-RateLimit-Remaining: 117 X-RateLimit-Reset: 1716718260 # unix seconds
On 429, the body includes a retry_after seconds value. Default limits are configurable per tier; enterprise SOWs raise limits explicitly.
07 · Versioning & deprecation
We add fields freely. We never rename or remove a field within a major version. Breaking changes ship as /api/v2/... running side-by-side with /api/v1/... for at least 12 months. Deprecations are announced via a Deprecation header on responses with a sunset date.
SDK clients (Python, TypeScript) auto-generated from the OpenAPI spec are available on engagement.
Email engineering@sonodadynamics.com for early access.