Home›Complia›API
Complia · API

Complia API, as shipped in the download

Complia API (v2.0)

Which deadlines are running out

curl -H "Authorization: Bearer apk_xxxx" https://complia.example.com/api/deadlines
{
  "today": "2026-08-06",
  "ack_window_days": 29,
  "overdue": [
    {"id": 12, "reference": "CR-0044", "subject": "Late SAR response",
     "stored_status": "received", "status": "ack_overdue",
     "ack_due_at": "2026-02-03", "days_remaining": -184}
  ],
  "awaiting_acknowledgement": [
    {"id": 31, "subject": "Marketing emails after opt-out",
     "stored_status": "received", "status": "received",
     "ack_due_at": "2026-08-20", "days_remaining": 14}
  ],
  "counts": {"overdue": 1, "awaiting": 1}
}

Everything still awaiting acknowledgment, split into overdue and pending, with signed days remaining. Poll it from a morning digest or a case-management dashboard.

Status is derived, never stored

stored_status is what is in the database: received, acknowledged, investigating, resolved or rejected. status adds the one that matters and is computed on every read:

statusMeaning
ack_overdueNot yet acknowledged and the due date has passed
received / acknowledged / investigatingThe stored workflow state
resolved / rejectedClosed — the clock stops and never shows overdue

Because ack_overdue is never written to a column, it cannot be stale and there is no nightly job that could forget to set it. Filter on it directly:

curl -H "Authorization: Bearer apk_xxxx" \
  "https://complia.example.com/api/complaints?status=ack_overdue"

The due date itself is not late — a complaint due today still reads received.

Authentication

Create a key in Settings → API & webhooks. It is shown once.

Authorization: Bearer apk_<40 hex chars>

/api/* is CSRF-exempt by design: the key is the credential and browsers never send it implicitly.

Every write checks the key holder's role, exactly as the screen does: a viewer's key cannot log a complaint or move one on (403 forbidden).

Endpoints

MethodPathNotes
GET/api/pingVerify a key
GET/api/complaints?status= filters the derived status
POST/api/complaintsLog one that arrived by phone, letter or email
GET/api/complaints/{id}One complaint with its full append-only log and its related records (kind, ref, url, note, the last status read from Privara; 3.2.0)
POST/api/complaints/{id}/statusMove it through the workflow
GET/api/deadlinesThe feed above
GET/api/reportThe leadership report (3.2.0): ?period=12m (default), quarter, year, or custom&from=YYYY-MM-DD&to=YYYY-MM-DD; the same numbers as the Leadership report page
GET/api/openapi.jsonOpenAPI 3.0.3 — imports as a custom connector
GET/healthzUnauthenticated {"ok":true,"app":"complia","version":"2.0.0"}

Logging a complaint that did not come through the web form

curl -X POST https://complia.example.com/api/complaints \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"complainant_name": "R. Vance", "complainant_email": "r@example.test",
       "subject": "Marketing emails after opt-out",
       "details": "Unsubscribed on 12 July, still receiving daily emails.",
       "reference": "CR-0051"}'

Most complaints arrive by phone or letter. Logging them through the API starts **the same acknowledgment clock** the public form starts — the deadline comes from Complaints::ackDue() either way, so nothing sits outside the register with an untracked duty. The response includes the tracking_token so you can give the complainant the same self-service status link a web submission gets.

Moving it along

curl -X POST https://complia.example.com/api/complaints/31/status \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"status": "resolved", "outcome": "Suppression list corrected; confirmed by email 6 Aug."}'

The workflow is append-only and enforced by the product's own transition table:

  • received → acknowledged · investigating · resolved · rejected
  • acknowledged → investigating · resolved · rejected
  • investigating → resolved · rejected
  • resolved / rejected — terminal; a closed complaint cannot be reopened

Closing requires an outcome. resolved or rejected without an outcome is refused with 422 — a register that records a closure with no reasoning is not evidence of anything.

{"error":"validation","detail":"An outcome summary is required to resolve or reject a complaint."}

The first move out of received also stamps the acknowledgment, satisfying the duty and stopping the clock. Every response carries allowed_transitions, so a UI can render only the buttons that will actually work.

Webhooks

EventFires when
complaint.createdOne is logged (public form or API)
complaint.status_changedIt moves state
X-Complia-Event: complaint.created
X-Complia-Signature: sha256=<hmac_sha256(raw_body, secret)>
import hmac, hashlib
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers["X-Complia-Signature"]):
    abort(401)

One attempt per event with a 5-second timeout. The last 200 deliveries per endpoint are logged with their response code.

There is deliberately no complaint.ack_overdue event. Going overdue is a date passing, not something Complia observes — firing it would require a background job, and Complia runs none. Poll /api/deadlines or ?status=ack_overdue on your own schedule instead: same answer, computed fresh, and you control the cadence.

Errors

CodeMeaning
401 unauthorizedMissing, malformed, revoked or unknown key
404 not_foundNo such complaint
422 validationRefused by a guard — missing fields, an illegal transition, or closing without an outcome

MCP — the agent endpoint (new in 3.0)

POST /mcp
Authorization: Bearer apk_...          ← the SAME revocable key the REST API uses
ToolWrites?What it does
list_complaintsnoFilter by derived status (ack_overdue works) or by due state.
complaint_detailnoOne complaint with its full append-only log.
create_complaintYESLogs a complaint that arrived by phone/letter/email and starts its statutory clock.
advance_statusYESMoves it through the workflow.
deadlines_reportnoOverdue and approaching acknowledgment deadlines.
leadership_reportnoThe leadership report for a period: on time, late, not yet; median days; outcomes; month by month; open by owner (3.2.0).

Connect it to an assistant

Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.

Every client needs the same three facts, and nothing in the handshake is vendor-specific:

The addresshttps://complia.example.com/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP over streamable HTTP, stateless

Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:

claude mcp add --transport http complia https://complia.example.com/mcp \
  --header "Authorization: Bearer apk_xxxx"

ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):

{
  "type": "mcp",
  "server_label": "complia",
  "server_url": "https://complia.example.com/mcp",
  "authorization": "apk_xxxx",
  "require_approval": "never"
}

**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"*. Pasted as-is, a block written for another client will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.

A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:

curl -X POST https://complia.example.com/mcp \
  -H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Complia implements the protocol, not an integration with a particular vendor.

What the agent cannot do

advance_status calls advance_complaint_status() — the same function the register screen and the REST API call. There is exactly one implementation of the workflow, so an agent:

  • cannot close a complaint without an outcome — refused with the product's own sentence;
  • cannot reopen a closed one — resolved and rejected are terminal;
  • cannot invent a deadline — ack_due_at is computed once by Complaints::ackDue() when the complaint is logged, and ack_overdue is derived on read, never stored.

Roles apply exactly as in the browser: a viewer's key is refused both write tools. Every MCP write lands in the audit trail with via: mcp, naming the key's user.

Connect it to Zapier, Make or n8n

Complia talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.

Before you start

  • In Complia, open Settings → API & webhooks. Mint an API key (choose Read only if the other tool only reads), and add a webhook when you have its URL (below).
  • Use your install's full address, for example https://complia.yourcompany.com.

When something happens in Complia: send it to another tool

  1. In the other tool, create the trigger that gives you a URL to receive webhooks: - Zapier: the "Webhooks by Zapier" app, trigger Catch Hook (Webhooks by Zapier needs a paid Zapier plan). - Make: module Webhooks → Custom webhook. - n8n: the Webhook node.
  2. Copy that URL into Complia under Settings → API & webhooks, and choose the event or events below.
  3. Make the event happen once in Complia, so the other tool receives a sample and you can map its fields.
EventFires when
complaint.createdOne is logged (public form or API)
complaint.status_changedIt moves state
complaint.ack_overdueThere is deliberately no event. Going overdue is a date passing, not

Every delivery carries X-Complia-Signature, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data.

When something happens elsewhere: act in Complia

  • Zapier: "Webhooks by Zapier", action POST (or Custom Request).
  • Make: module HTTP → Make a request.
  • n8n: the HTTP Request node.

Send the request to your install's address plus the path from the Endpoints table above, with the header Authorization: Bearer <your API key> and a JSON body. A read-only key cannot write.

The endpoints that change something:

  • POST /api/complaints: Log one that arrived by phone, letter or email
  • POST /api/complaints/{id}/status: Move it through the workflow

Example recipes

  • complaint.created → a row in Google Sheets: webhook trigger on complaint.created (One is logged (public form or API)), then the Google Sheets "Create row" action with the fields you want.
  • complaint.status_changed → a message in Slack or Teams: webhook trigger on complaint.status_changed (It moves state), then the Slack or Microsoft Teams "Send message" action.
  • A new form entry or row elsewhere → Complia: the other tool's trigger (for example a new Google Sheets row or a form submission), then an HTTP action calling POST /api/complaints (Log one that arrived by phone, letter or email).

These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; Complia's own automated tests check its API.

← Back to Complia · Manual · Quickstart · Test run