Webhooks
Get notified on your server when a message status changes.
Receive real-time notifications for messaging events by registering HTTPS endpoints. Base path: https://sms.chinqit.com/api/v2.
Overview
Webhooks let you register public HTTPS endpoints that ChinqIT pushes notifications to whenever an event occurs. Each endpoint has its own signing secret and can subscribe to any combination of events.
Authentication and the base URL are documented in the API Reference.
Event catalog
All notifications use the following envelope:
{
"id": "evt_01j9z...",
"type": "message.sent",
"createdAt": "2026-06-27T10:00:00.000Z",
"data": { ... }
}| Event | Trigger |
|---|---|
message.sent | Single SMS or OTP-SMS accepted by the carrier (successful submission) |
message.rejected | Single SMS or OTP-SMS the operator received but refused — you were charged |
message.failed | Single SMS or OTP-SMS our gateway could not submit to the carrier — not charged |
bulk.completed | Bulk job finished, with success/failure counts |
otp.verified | End user entered the correct OTP code |
Note: End-to-end delivery confirmation is not available (no carrier delivery receipts). There is therefore no
deliveredevent.message.sent,message.rejected, andmessage.failedreflect only the submission outcome to the carrier.Note — hard cut:
message.sentmeans the operator received the message and you were billed for it.message.rejectedfires when the operator refused it after receiving it — usually billed, so checkdata.billed(it isfalsein the rare case ChinqIT blocks a known-invalid number before submission, which never reaches the operator).message.failedmeans the message never reached the operator and was not charged (or refunded if it had already been charged). If your integration previously relied onmessage.failedto detect rejections, you must now also subscribe tomessage.rejected— as of this change, a rejection no longer firesmessage.failed.Note: Bulk recipients do not emit per-message events. Only
bulk.completedis emitted when the batch finishes processing.
message.sent
{
"id": "evt_01j9z...",
"type": "message.sent",
"createdAt": "2026-06-27T10:00:00.000Z",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"phoneNumber": "+22238089336",
"senderId": "CHINQIT",
"channel": "sms",
"type": "message",
"submittedAt": "2026-06-27T10:00:00.000Z"
}
}message.rejected
{
"id": "evt_01j9z...",
"type": "message.rejected",
"createdAt": "2026-06-27T10:00:03.000Z",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"phoneNumber": "+22238089336",
"senderId": "CHINQIT",
"channel": "sms",
"type": "message",
"billed": true
}
}data.billed is authoritative: it is true when the operator received and refused the message (you were charged), and false in the rare case ChinqIT blocks a known-invalid number before submission (never reached the operator, not charged).
message.failed
{
"id": "evt_01j9z...",
"type": "message.failed",
"createdAt": "2026-06-27T10:00:05.000Z",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"phoneNumber": "+22238089336",
"senderId": "CHINQIT",
"channel": "sms",
"type": "message",
"failureReason": "The API key sender ID has expired. Remove it or renew the sender ID before sending SMS."
}
}bulk.completed
{
"id": "evt_01j9z...",
"type": "bulk.completed",
"createdAt": "2026-06-27T10:05:00.000Z",
"data": {
"batchId": "batch_01j9z...",
"status": "completed",
"totalMessages": 1000,
"successfulCount": 985,
"failedCount": 12,
"invalidCount": 3,
"totalCost": 9.85
}
}otp.verified
For email OTPs, the payload contains email instead of phoneNumber.
{
"id": "evt_01j9z...",
"type": "otp.verified",
"createdAt": "2026-06-27T10:02:00.000Z",
"data": {
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"phoneNumber": "+22238089336",
"verifiedAt": "2026-06-27T10:02:00.000Z"
}
}Security
HTTPS required
All webhook endpoints must use HTTPS. HTTP URLs are rejected.
Verifying the X-Chinqit-Signature header
Every delivery includes the following headers:
| Header | Description |
|---|---|
X-Chinqit-Signature | t=<unix>,v1=<hex> — timestamp and HMAC-SHA256 signature |
X-Chinqit-Timestamp | Unix timestamp in seconds (same value as t= in the signature) |
X-Chinqit-Delivery | Unique delivery-attempt identifier (idempotency key) |
Verification recipe:
signature = HMAC_SHA256(secret, "${t}.${rawBody}")- Extract
tand allv1values from theX-Chinqit-Signatureheader. - Compute
HMAC_SHA256(secret, t + "." + rawBody)using the raw (unparsed) request body. - Compare the result (hex) against each
v1value. Accept if at least one matches. - Reject the request if
X-Chinqit-Timestampis outside your tolerance window (e.g. ±5 minutes) to protect against replay attacks.
Example (Node.js):
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifySignature(secret, rawBody, sigHeader, tsHeader, toleranceSec = 300) {
const now = Math.floor(Date.now() / 1000);
const ts = parseInt(tsHeader, 10);
if (Math.abs(now - ts) > toleranceSec) return false;
const parts = sigHeader.split(',');
const t = parts.find(p => p.startsWith('t=')).slice(2);
const v1s = parts.filter(p => p.startsWith('v1=')).map(p => p.slice(3));
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return v1s.some(v => {
try { return timingSafeEqual(Buffer.from(v, 'hex'), Buffer.from(expected, 'hex')); }
catch { return false; }
});
}Idempotency
The X-Chinqit-Delivery header contains a stable identifier for the delivery — the same value is reused across every retry attempt. Treat it as an idempotency key to deduplicate events you receive more than once.
Reliability
Retries and exponential backoff
On failure (non-2xx response or timeout), ChinqIT automatically retries delivery up to 8 times using exponential backoff — retries are spaced progressively (1 min, 5 min, 30 min, 2 h, 5 h, 10 h, up to 24 h apart), spanning roughly 1.5–2 days in total.
Auto-disable
An endpoint is automatically disabled after 20 consecutive failures. An alert is sent to your service administrator. You can re-enable the endpoint via the management API.
Secret rotation with overlap window
When you rotate a secret (POST .../rotate-secret), the old secret continues to sign deliveries for a 24-hour overlap window. During this window, deliveries include two v1= values in the signature header — one signed with the old secret, one with the new. Accept if either one is valid.
Management API
All routes require the X-API-Key header.
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v2/webhooks | Create a webhook endpoint |
GET | /api/v2/webhooks | List your endpoints |
GET | /api/v2/webhooks/{id} | Get an endpoint |
PATCH | /api/v2/webhooks/{id} | Update an endpoint |
DELETE | /api/v2/webhooks/{id} | Delete an endpoint |
POST | /api/v2/webhooks/{id}/rotate-secret | Rotate the signing secret |
POST | /api/v2/webhooks/{id}/reveal-secret | Reveal the current signing secret (owner-only, available anytime) |
POST | /api/v2/webhooks/{id}/test | Send a test delivery |
GET | /api/v2/webhooks/{id}/deliveries | View delivery history |
Create an endpoint (example):
curl -X POST https://sms.chinqit.com/api/v2/webhooks \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/chinqit",
"events": ["message.sent", "message.failed", "otp.verified"]
}'Request body fields:
url(required) — the public HTTPS endpoint to deliver events to.events(required) — array of event types to subscribe to.apiKeyIds(optional) — restrict this endpoint to events triggered by specific API keys ("apps"). Omit or pass[]to receive events from all of your API keys. When set, only events from the listed keys are delivered to this endpoint.
Response:
{
"success": true,
"webhook": {
"_id": "665f1a2b3c4d5e6f7a8b9c0d",
"userId": "6953a1b2c3d4e5f6a7b8c9d0",
"url": "https://example.com/webhooks/chinqit",
"events": ["message.sent", "message.failed", "otp.verified"],
"status": "enabled",
"secretPrefix": "whsec_ab12c…",
"consecutiveFailures": 0,
"createdAt": "2026-06-27T10:00:00.000Z",
"updatedAt": "2026-06-27T10:00:00.000Z"
},
"secret": "whsec_4f3c8e21a9b0c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2"
}Important: The full
secret(prefixwhsec_) is only returned here at creation time and from/rotate-secretand/reveal-secret. Every other response exposes onlysecretPrefix. Store the full secret securely immediately.
Delivery history (pagination):
GET /api/v2/webhooks/{id}/deliveries accepts two query parameters:
limit(optional) — deliveries per page, from 1 to 200 (50 by default).cursor(optional) — the opaque cursor from a previous response.
The response adds nextCursor. It is null on the last page. To walk the whole history, send nextCursor as the cursor of the next request until you get null. The cursor is opaque: do not interpret it or build one yourself.
# First page: the most recent deliveries
curl "https://sms.chinqit.com/api/v2/webhooks/WEBHOOK_ID/deliveries?limit=50" \
-H "X-API-Key: YOUR_API_KEY"
# Next page
curl "https://sms.chinqit.com/api/v2/webhooks/WEBHOOK_ID/deliveries?limit=50&cursor=CURSOR_FROM_PREVIOUS_RESPONSE" \
-H "X-API-Key: YOUR_API_KEY"{
"success": true,
"deliveries": [{ "_id": "665f1a2b3c4d5e6f7a8b9c0e", "status": "succeeded", "eventType": "message.sent" }],
"nextCursor": "WyIyMDI2LTA2LTI3VDEwOjAwOjAwLjAwMFoiLCI2NjVmMWEyYjNjNGQ1ZTZmN2E4YjljMGUiXQ"
}An invalid
cursorreturns a400. IgnoringnextCursoris perfectly valid: the first page is identical to the one before this pagination was added.
Managing endpoints in the dashboard
On the Webhooks page of the dashboard, each endpoint appears as a card. Open an endpoint's menu (⋯) to edit it, send a test event, rotate its secret, disable or re-enable it, or delete it. Select the card to open its details panel: the signing secret, a table of recent deliveries (outcome, event, response code, duration, time) with a Send test event button, and an example payload for an event the endpoint receives. The panel's footer has Edit and Rotate secret; delete an endpoint from its card menu. Rotating asks for confirmation and keeps the current secret valid for the 24-hour overlap window.
The full signing secret is shown once, when you create an endpoint or rotate its secret, so copy it at that point. The details panel keeps the secret masked and reveals it on request. An endpoint that ChinqIT disabled after repeated failures shows a warning on its card and in its details panel, with how many deliveries failed in a row and when the last one failed. Fix your server, then choose Re-enable from the card menu.