The Callario API, as it actually is.
A small, scoped REST API for tickets and contacts, and signed webhooks for what happens in your workspace. This page is written from the code: if an endpoint isn’t listed here, it isn’t public yet.
Overview
Every request goes to https://api.callario.com/papi/v1 over HTTPS, sends and receives JSON, and is authenticated with an API key. The API uses the same services as the Callario console, so the same validation, workspace isolation and audit trail apply to an API request as to a click.
| Endpoint | Scope | What it does |
|---|---|---|
GET /tickets | tickets:read | List tickets, filtered and paged |
GET /tickets/:id | tickets:read | One ticket with its thread |
POST /tickets | tickets:write | Create a ticket |
GET /contacts | contacts:read | List customer records |
Authentication and scopes
A workspace owner or admin creates keys in Settings → Integrations. A key looks like ck_<prefix>_<secret> and is shown once, when it’s created: Callario stores only a hash of the secret, so a lost key is replaced, not recovered. Revoking a key stops it immediately; to rotate, create the new key, switch over, then revoke the old one. Each key records when it was last used.
Send the key in either header. A key carries one or more scopes, and each endpoint needs exactly one of them.
curl https://api.callario.com/papi/v1/tickets \ -H "Authorization: Bearer ck_9f2c41e07a3b5d10_<secret>"# or, equivalentlycurl https://api.callario.com/papi/v1/tickets \ -H "X-Api-Key: ck_9f2c41e07a3b5d10_<secret>"| Scope | Grants |
|---|---|
tickets:read | GET /tickets and GET /tickets/:id |
tickets:write | POST /tickets |
contacts:read | GET /contacts |
List tickets
Returns a page of tickets, newest activity first unless you sort otherwise. All parameters are optional query-string values.
| Parameter | Values |
|---|---|
status | open · pending · on_hold · solved · closed · spam |
priority | low · normal · high · urgent |
type | question · incident · problem · task |
view | all · unassigned · open · pending · solved · urgent · breached · at_risk · open_or_pending |
q | Free-text search, up to 255 characters |
assigneeId · tagId · teamId · requesterId · organizationId | UUIDs that narrow the list |
sort | updated (default) · newest · oldest · priority · sla_due · waiting |
page · pageSize | From 1; pageSize 1–100, default 25 |
curl "https://api.callario.com/papi/v1/tickets?status=open&priority=urgent&sort=sla_due&pageSize=10" \ -H "Authorization: Bearer $CALLARIO_API_KEY"{ "data": [ { "id": "3c0b8a6e-2f4d-4e7a-9a51-6d2f0c1b7e42", "number": 1042, "subject": "Can't sign in after changing my email", "status": "open", "priority": "urgent", "type": "question", "channel": "web", "requester": { "id": "8d1e…", "name": "Maya Rivera", "email": "maya@northwind.example" }, "assignee": null, "team": null, "tags": [], "messageCount": 3, "createdAt": "2026-10-04T09:12:44.000Z", "updatedAt": "2026-10-04T09:20:03.000Z" } ], "total": 1, "page": 1, "pageSize": 10}Get a ticket
Returns one ticket with its messages, events, links, participants, side conversations, custom-field values and SLA clocks. The id must be a UUID — any other value is a 400 — and an unknown id is a 404 with the code TICKET_NOT_FOUND. The ticket’s version increases with each change.
curl https://api.callario.com/papi/v1/tickets/3c0b8a6e-2f4d-4e7a-9a51-6d2f0c1b7e42 \ -H "Authorization: Bearer $CALLARIO_API_KEY"{ "ticket": { "id": "3c0b8a6e-2f4d-4e7a-9a51-6d2f0c1b7e42", "number": 1042, "subject": "Can't sign in after changing my email", "status": "open", "description": null, "version": 4, "solvedAt": null // … the list fields above, plus parent, children, mergedInto, closedAt }, "messages": [ /* the thread, oldest first */ ], "events": [ /* status changes, assignments, … */ ], "links": [], "participants": [], "sideConversations": [], "customFields": [], "sla": null}Create a ticket
Creates a ticket and answers 201 with it. Tickets created this way are marked origin: "api", so your team can tell them apart from ones a customer or a teammate opened. Name the customer by email, or by the id of an existing customer record.
| Field | Notes |
|---|---|
subject | Required, 1–255 characters |
description | Optional, up to 20,000 characters |
priority | low · normal (default) · high · urgent |
type | question (default) · incident · problem · task |
requesterEmail · requesterName | Who the ticket is for, by email address |
requesterId | An existing customer record’s id; wins over requesterEmail when both are sent |
assigneeId | A teammate’s id, or null |
tagIds | An array of tag ids |
customFields | Values keyed by custom-field id, validated like the console |
curl -X POST https://api.callario.com/papi/v1/tickets \ -H "Authorization: Bearer $CALLARIO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": "Order arrived damaged", "description": "Filed from our returns portal.", "priority": "high", "type": "incident", "requesterEmail": "omar@northwind.example", "requesterName": "Omar Haddad" }'HTTP/1.1 201 Created{ "id": "b7f3…", "number": 1043, "subject": "Order arrived damaged", "status": "open", "priority": "high", "type": "incident", "origin": "api", "version": 0 // … the remaining ticket fields}List contacts
Returns a page of customer records from your CRM. Custom-field values are not included — some may hold personal data — and the API cannot create or edit contacts yet.
| Parameter | Values |
|---|---|
q | Free-text search |
segment · organizationId | UUIDs that narrow the list |
lifecycleStatus | The customer’s lifecycle stage, such as lead or active |
vip · hasOrganization · hasEmail · identified · hasOpenTickets | true or false |
tag | A tag name |
seenWithinDays | 1–3650 |
sort | recent (default) · created · name · tickets · last_seen |
page · pageSize | From 1; pageSize 1–100, default 25 |
curl "https://api.callario.com/papi/v1/contacts?q=northwind&hasOpenTickets=true&sort=last_seen" \ -H "Authorization: Bearer $CALLARIO_API_KEY"{ "data": [ { "id": "8d1e…", "displayName": "Maya Rivera", "primaryEmail": "maya@northwind.example", "primaryPhone": null, "avatarUrl": null, "vip": false, "lifecycleStatus": "active", "organization": { "id": "…", "name": "Northwind" }, "tags": ["beta"], "ticketCount": 4, "openTicketCount": 1, "lastSeenAt": "2026-10-04T09:11:58.000Z", "createdAt": "2026-08-21T14:03:10.000Z", "updatedAt": "2026-10-04T09:12:44.000Z" } ], "total": 1, "page": 1, "pageSize": 25}Errors
Every error is a application/problem+json document (RFC 9457) with a stable code to branch on, a human-readable title and detail, and a traceId to quote when you ask us about it. Validation errors add an errors array naming each field and what was wrong with it.
| Status | code | When |
|---|---|---|
| 401 | API_KEY_REQUIRED | No key in either header |
| 401 | API_KEY_INVALID | The key is unknown, malformed or revoked |
| 403 | INSUFFICIENT_SCOPE | The key lacks the endpoint’s scope |
| 404 | TICKET_NOT_FOUND | No such ticket in this workspace |
| 422 | VALIDATION_FAILED | A query or body field is invalid |
| 429 | RATE_LIMITED | Too many requests — see below |
HTTP/1.1 403 ForbiddenContent-Type: application/problem+json{ "type": "about:blank", "title": "Insufficient API-key scope", "status": 403, "detail": "This key lacks the \"tickets:write\" scope.", "code": "INSUFFICIENT_SCOPE", "instance": "/papi/v1/tickets", "traceId": "c41f…"}Rate limits
The public API allows 600 requests per 60-second window per client IP address, counted across all four endpoints and on top of key authentication. Past that, requests answer 429 with the code RATE_LIMITED and a detail that says how many seconds remain in the window. Back off until then; the window resets on its own.
Webhooks
Add an endpoint in Settings → Integrations and choose its events: exact names, a prefix such as customer.*, or * for everything. Endpoint URLs must use HTTPS and resolve to a public address. Callario generates a signing secret (or takes yours) and shows it once; you can rotate it at any time, and deliveries still waiting to be retried are signed with the new one.
Each delivery is a POST with a JSON envelope — id, type, tenantId, data and occurredAt — and these headers:
| Header | Meaning |
|---|---|
x-callario-signature | t = the send time in Unix seconds; v1 = HMAC-SHA256 of “t.body” with your secret. Ignore v0, a legacy body-only signature. |
x-callario-timestamp | The same t, for convenience — trust the signed one |
x-callario-event-id | The event’s id, the same on every retry and replay — your de-duplication key |
x-callario-delivery-id | This delivery, as shown in the delivery log |
POST /webhooks/callario HTTP/1.1Content-Type: application/jsonUser-Agent: Callario-Webhooks/1X-Callario-Signature: t=1759571564,v1=5d1f…a07c,v0=91be…X-Callario-Timestamp: 1759571564X-Callario-Event-Id: 6a0f2d1c-…X-Callario-Delivery-Id: e93b7c55-…{ "id": "6a0f2d1c-…", "type": "conversation.ticket.status_changed", "tenantId": "…", "data": { "ticketId": "3c0b8a6e-…", "from": "open", "to": "solved" }, "occurredAt": "2026-10-04T09:32:44.120Z"}Verify a signature
Recompute the HMAC over the timestamp, a full stop and the raw request body, compare it with v1 in constant time, and reject anything more than five minutes old. Because the timestamp is inside the signature, a captured delivery can’t be replayed later with a new one.
import crypto from 'node:crypto';import express from 'express';const app = express();const SECRET = process.env.CALLARIO_WEBHOOK_SECRET;const TOLERANCE_SECONDS = 300;// Verify against the RAW body: re-serialising parsed JSON changes the bytes.app.post('/webhooks/callario', express.raw({ type: 'application/json' }), (req, res) => { const header = req.get('x-callario-signature') ?? ''; const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('='))); const t = Number(parts.t); const raw = req.body.toString('utf8'); const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${raw}`).digest(); const given = Buffer.from(parts.v1 ?? '', 'hex'); const fresh = Math.abs(Date.now() / 1000 - t) <= TOLERANCE_SECONDS; if (!fresh || given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) { return res.status(400).send('invalid signature'); } const event = JSON.parse(raw); // Deliveries are at-least-once: skip an event.id you have already processed. res.sendStatus(200);});Retries and replay
Answer with any 2xx within 8 seconds and the delivery is done. Anything else — another status, a timeout, a refused connection — is retried with exponential backoff: about 1, 2, 4, 8 and 16 minutes after each failed attempt, up to six attempts in all. After the sixth it is marked dead.
| Attempt | If it fails, the next one is |
|---|---|
| 1 | about 1 minute later |
| 2 | about 2 minutes later |
| 3 | about 4 minutes later |
| 4 | about 8 minutes later |
| 5 | about 16 minutes later |
| 6 | none — the delivery is dead |
The delivery log for each endpoint shows every delivery with its status, HTTP code, duration and the start of your server’s response. From there an admin can replay any delivery — failed, dead or even delivered — and it is sent again, freshly signed. Delivery is at-least-once, so de-duplicate on the event id.
Event catalogue (46)
The names below are checked against what the code emits. Two high-volume events are only sent to endpoints that name them — * and customer.* do not include them.
Tickets12
- conversation.ticket.createdA ticket was created.
- conversation.ticket.updatedTicket fields changed.
- conversation.ticket.status_changedThe ticket moved between statuses.
- conversation.ticket.assignedThe ticket was assigned or unassigned.
- conversation.ticket.team_changedThe ticket moved between teams — which changes who can read it.
- conversation.ticket.taggedThe ticket’s tags changed.
- conversation.ticket.mergedThe ticket was merged into another.
- conversation.ticket.splitMessages were split into a new ticket.
- conversation.ticket.sla_policy_changedA rule bound the ticket to a different SLA policy.
- ticket.sla.approachingAn SLA target is nearing its deadline.
- ticket.sla.breachedAn SLA target passed its deadline unmet.
- ticket.unsnoozedA snoozed ticket returned to the queue.
Conversations9
- conversation.message.appendedA message was added to a thread.
- conversation.customer.repliedThe customer replied publicly.
- conversation.reopenedA resolved conversation was reopened.
- conversation.chat.startedA new AI chat started.
- chat.delivery_failedA chat reply did not go out through its WhatsApp, Slack or other channel.
- channel.email.quarantinedAn inbound email that could not prove which ticket it belongs to is held for review.
- conversation.chat.intake_completedDeprecated: a chat’s conversation workflow ended — how it ended, the path taken and the visitor’s answers.
- portal.deflection.recordedA portal visitor was shown, or clicked, a suggested article before filing.
- csat.respondedA customer answered a satisfaction survey (score, comment, sentiment).
AI2
- ai.resolution.proposedThe AI agent proposed that a conversation is resolved.
- ai.resolution.verifiedA proposed resolution was confirmed and held through its reopen window.
Customers17
- customer.contact.createdA customer record was created.
- customer.contact.updatedA customer record was updated.
- customer.contact.mergedTwo customer records were merged.
- customer.contact.unmergedA merge was reverted.
- customer.contact.org_associatedA customer was linked to a company.
- customer.consent.withdrawnA customer withdrew consent (from a teammate, the widget or an unsubscribe link).
- customer.contact.erasedA customer was erased (GDPR).
- customer.contact.restrictedProcessing was restricted for a customer (GDPR).
- customer.dsar.export_completedA data-subject export finished.
- customer.organization.createdA company was created.
- customer.organization.updatedA company was updated.
- customer.organization.deletedA company was deleted.
- customer.segment.createdA segment was created.
- customer.segment.updatedA segment was updated.
- customer.segment.refreshedA dynamic segment was re-evaluated and customers joined or left it.by name only
- customer.contact.seenA customer came back after more than an hour away (at most once an hour each).by name only
- customer.bulk_job.completedA bulk customer job (such as an import) finished.
Workflows & rules5
- workflow.run.failedA workflow run failed — with its step, error and workflow id.
- workflow.approval.requestedA workflow step is waiting on a person’s decision.
- workflow.approval.expiredNobody decided in time; the run was cancelled and the gated step never ran.
- automation.webhook.calledA rule called an outbound webhook.
- notification.requestedA rule asked for a notification to be sent.
Workspace1
- identity.user.provisionedA user account was provisioned.
Context endpoint: your data, for the AI agent
The other direction. Register one HTTPS endpoint of your own in Settings → Integrations, and the AI agent can ask it for a record — an order, a shipment, a return or an account — but only for a customer whose identity your website has verified. Requests are signed exactly like webhooks, time out after 5 seconds, and read at most 32 KB of your response. The lookup is read-only: the agent can tell a customer where their order is, not change it.
POST https://your-company.example/callario/contextContent-Type: application/jsonUser-Agent: Callario-Context/1X-Callario-Signature: t=1759571702,v1=0c9e…{ "type": "order", "ref": "A-10023", "contactRef": "email:maya@northwind.example" }“Send test” in the console makes three fixed requests, including a badly signed one and a stale one, so an endpoint that skips verification fails the test instead of passing it.
Not in the API yet
So nobody builds against something that isn’t there: the public API does not yet update tickets, post replies, read conversations or chats, create or edit contacts, or return reports. If one of these matters to you, tell us — it shapes what we open next. The product side is on API & Webhooks.
Design-partner programme · now onboarding
Building on Callario? Let’s talk.
Design partners connect their own systems with the founder alongside, and get a direct line for anything this page doesn’t answer.