ChinqIT VerifyChinqIT Verify

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": { ... }
}
EventTrigger
message.sentSingle SMS or OTP-SMS accepted by the carrier (successful submission)
message.rejectedSingle SMS or OTP-SMS the operator received but refused — you were charged
message.failedSingle SMS or OTP-SMS our gateway could not submit to the carrier — not charged
bulk.completedBulk job finished, with success/failure counts
otp.verifiedEnd user entered the correct OTP code

Note: End-to-end delivery confirmation is not available (no carrier delivery receipts). There is therefore no delivered event. message.sent, message.rejected, and message.failed reflect only the submission outcome to the carrier.

Note — hard cut: message.sent means the operator received the message and you were billed for it. message.rejected fires when the operator refused it after receiving it — usually billed, so check data.billed (it is false in the rare case ChinqIT blocks a known-invalid number before submission, which never reaches the operator). message.failed means the message never reached the operator and was not charged (or refunded if it had already been charged). If your integration previously relied on message.failed to detect rejections, you must now also subscribe to message.rejected — as of this change, a rejection no longer fires message.failed.

Note: Bulk recipients do not emit per-message events. Only bulk.completed is 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:

HeaderDescription
X-Chinqit-Signaturet=<unix>,v1=<hex> — timestamp and HMAC-SHA256 signature
X-Chinqit-TimestampUnix timestamp in seconds (same value as t= in the signature)
X-Chinqit-DeliveryUnique delivery-attempt identifier (idempotency key)

Verification recipe:

signature = HMAC_SHA256(secret, "${t}.${rawBody}")
  1. Extract t and all v1 values from the X-Chinqit-Signature header.
  2. Compute HMAC_SHA256(secret, t + "." + rawBody) using the raw (unparsed) request body.
  3. Compare the result (hex) against each v1 value. Accept if at least one matches.
  4. Reject the request if X-Chinqit-Timestamp is 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.

MethodEndpointDescription
POST/api/v2/webhooksCreate a webhook endpoint
GET/api/v2/webhooksList 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-secretRotate the signing secret
POST/api/v2/webhooks/{id}/reveal-secretReveal the current signing secret (owner-only, available anytime)
POST/api/v2/webhooks/{id}/testSend a test delivery
GET/api/v2/webhooks/{id}/deliveriesView 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 (prefix whsec_) is only returned here at creation time and from /rotate-secret and /reveal-secret. Every other response exposes only secretPrefix. 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 cursor returns a 400. Ignoring nextCursor is 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.


See also

On this page