API & webhooks

REST + signed 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.

OpenAPI 3.1 Swagger UI served by the production backend at /docs
OpenAPI 3.1 spec served live by the FastAPI backend · captured from a provisioned tenant

01 · Authentication

Bearer token. Tenant-bound. Shown once.

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

Resource-scoped, tenant-scoped.

SurfacePath prefixRole 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/systemspublic
Health / metrics/status · /metricspublic

03 · Webhook signing

HMAC-SHA256 · 5-min replay window.

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

Stable contract per event type.

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.startedworkflow execution begun
workflow.step.completedone step closed; includes step id, capability called, outcome
workflow.completedterminal state reached
workflow.failedworkflow halted; includes error envelope
approval.requestedaction requires human authorization; includes tier + context
approval.grantedauthority graph approved the action
approval.deniedapprover rejected; reason logged
audit.entry.writtenfired for every audit log append (firehose; opt-in subscription)
anomaly.detectedanomaly detection raised a flag; includes score + axis
document.classifieddocument type assigned + routed
crm.contact.stalehealth scoring crossed staleness threshold

05 · Error contract

Predictable, parseable, stable.

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

Token bucket per route, per tenant.

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

Additive forever. Breaking is a new version.

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.