Developers
Connect IgniChat to your own systems: read your data with an API key and receive events by webhook.
The technical documentation is in English.
Getting started
1. In IgniChat open Settings → Developer and create an API key. Choose only the scopes the integration needs. The key is shown once — store it somewhere safe.
2. Send it as a bearer token. Every request is scoped to the one workspace the key belongs to.
Use an ign_test_ key while you build: it works the same way but never touches live data.
curl https://YOUR_API_HOST/v1/workspaces/WORKSPACE_ID/contacts?limit=20 \
-H "Authorization: Bearer ign_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Authentication and scopes
A key is ign_live_… (real data) or ign_test_… (sandbox). It carries the scopes chosen when it was created; each operation in the reference states the scope it needs, and a key without it gets 403.
Version 1 is read-only. A write with an API key returns 403 with the code api_key.write_not_supported.
Revoke a key at any time in Settings → Developer; it stops working immediately.
Rate limits
Each key may make 300 requests per minute. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets).
When the limit is exceeded the API answers 429 with a Retry-After header. Wait that many seconds and retry; do not retry in a tight loop.
Pagination
List operations are cursor based. Pass limit (1–100); the response has items and nextCursor. Send nextCursor back as cursor to get the next page; a null nextCursor means the end. There is no offset, so a page never repeats or skips a row while data changes.
curl "https://YOUR_API_HOST/v1/workspaces/WORKSPACE_ID/contacts?limit=100&cursor=NEXT_CURSOR" \
-H "Authorization: Bearer ign_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Errors
Errors share one shape. code is stable and safe to branch on; message is for people and may change; requestId identifies the request when you contact support.
{
"error": {
"code": "api_key.write_not_supported",
"message": "API keys are read-only in version 1.",
"requestId": "req_01HX9Z3KQ7M2"
}
}Webhooks
A webhook sends an HTTP POST to your HTTPS URL when something happens in your workspace. Create one in Settings → Developer, choose the events, and copy the signing secret shown once.
Respond with any 2xx status within 10 seconds. Anything else, or no answer, counts as a failure and is retried up to 9 times with exponentially growing waits, starting at 5 minutes. A webhook that keeps failing is disabled automatically after 3 failed deliveries and the workspace's admins are notified.
The same event can be delivered more than once (retries, or your server answering after a timeout). Use the event's id to ignore a delivery you have already handled.
Always verify the signature before trusting a delivery, using the raw body exactly as received, and reject deliveries whose timestamp is more than 5 minutes old.
Headers sent with every delivery
| Header | Description |
|---|---|
| X-Ignichat-Signature | Hex HMAC-SHA256 of <timestamp>.<raw body> using the subscription's signing secret. |
| X-Ignichat-Timestamp | When the delivery was signed, in milliseconds since the Unix epoch. Reject deliveries older than five minutes. |
| X-Ignichat-Event | The event type, e.g. contact.created. |
Body
{
"id": "evt_01HX9Z3KQ7M2",
"type": "contact.created",
"occurredAt": "2026-10-09T10:15:30.000Z",
"payload": {
"contactId": "c_123"
}
}Verify a signature
const crypto = require("node:crypto");
// Use the RAW request body (the exact bytes received), not a re-serialised object.
function isValid(secret, timestamp, rawBody, signature) {
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Also reject stale deliveries: Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000
console.log(isValid(process.env.SECRET, process.env.TIMESTAMP, process.env.BODY, process.env.SIGNATURE) ? "valid" : "invalid");
Test your code with this example: the secret is not a real one.
- Secret:
- whsec_example_secret_0123456789
- Timestamp:
- 1760000000000
- Body:
- {"id":"evt_01HX9Z3KQ7M2","type":"contact.created","occurredAt":"2026-10-09T10:15:30.000Z","payload":{"contactId":"c_123"}}
- Expected signature:
- 9b4c3072d5762792f7f0880fe278ea8f6800d4bae8dc467a7a8de9c6b3c6c9e1
Events
| Event | Description | Payload fields |
|---|---|---|
| contact.created | A contact was created, from any source. | contactId, name, phone, email, channelId, channelType |
| contact.updated | A contact's details changed. | contactId |
| contact.tag.added | A tag was added to a contact. | contactId, tagId |
| contact.deleted | A contact was deleted. | contactId |
| conversation.opened | A conversation was opened with a contact. | conversationId, contactId |
| conversation.assigned | A conversation was assigned to a team member, or unassigned. | conversationId, assigneeUserId, changedByUserId, previousAssigneeUserId |
| conversation.closed | A conversation was resolved. | conversationId |
| message.received | A customer message arrived. | messageId, conversationId, automationRoute, targetWorkflowId, campaignId |
| message.sent | A message or internal note was sent from IgniChat. | messageId, conversationId, visibility, mentionedUserIds |
| message.delivery.updated | The delivery status of an outgoing message changed (sent, delivered, read, failed). | messageId, conversationId, status |
| campaign.completed | A campaign finished sending. | campaignId, sentCount, excludedCount, failedCount |
| campaign.recipient.sent | A campaign message was handed to the channel. | campaignId, recipientId, contactId, messageId |
| campaign.recipient.delivered | A campaign message reached the recipient's phone. | campaignId, recipientId, contactId |
| campaign.recipient.read | A campaign message was read. | campaignId, recipientId, contactId |
| campaign.recipient.failed | A campaign message could not be sent. | campaignId, recipientId, contactId, reason |
| campaign.recipient.replied | A recipient replied to, or tapped a button on, a campaign message. | campaignId, contactId, conversationId, messageId, providerEventId, occurredAt, interactionType, quickReplyId, quickReplyTitle |
Versioning
The version is part of the path (/v1). Within a version we only make additive changes: new operations, new optional parameters, new response fields and new webhook event types. Write your client to ignore fields it does not know.
A breaking change ships as a new version (/v2). The old version keeps working for at least 180 days after the announcement; during that time deprecated operations answer with Deprecation and Sunset headers.
Every change to the public API is recorded in the changelog below.
Changelog
Version 1.0.0 — 2026-10-12
- First published version of the public API.
- Read operations for contacts, tags, WhatsApp templates, campaigns and headline analytics.
- Signed webhooks for every domain event, with retries and automatic disabling after repeated failure.
- Sandbox keys (
ign_test_) for trying the API without touching live data.