Ownware
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 acknowledgement, 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.

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
POST/api/complaints/{id}/statusMove it through the workflow
GET/api/deadlinesThe feed above
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 acknowledgement 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 acknowledgement, 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 acknowledgement deadlines.

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"
}

Own Your AI reads a list of servers in this shape:

{
  "mcpServers": [
    { "id": "complia", "name": "Complia", "url": "https://complia.example.com/mcp",
      "token": "apk_xxxx", "enabled": true }
  ]
}

**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"* — not an mcpServers array. Pasted as-is, the block above 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.

← Back to Complia · Manual · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →