Reference
Webhooks
Webhook adalah callback keluar dari Shiftwa ke endpoint milik aplikasi Anda. Halaman ini menjelaskan event dan payload yang akan kami kirim ke URL Anda.
What this page is
This page documents the HTTP requests that Shiftwa sends to your webhook receiver. It is not a list of Shiftwa API endpoints. You create and manage webhook configuration from the Shiftwa dashboard, then your application receives signed POST callbacks.
- Your receiver URL is the endpoint in your app, for example
https://your-app.example.com/shiftwa/webhook. - Assign each webhook to one or more WhatsApp numbers in the dashboard.
- Events with
data.numberIdare only delivered to webhooks assigned to that number. - Return
2xxquickly after storing or queueing the event.
Configure in dashboard
Open the Shiftwa dashboard, go to Developer - Webhooks, add your receiver URL, choose the WhatsApp numbers, then select the events your app wants to receive. Shiftwa shows the signing secret once when the webhook is created.
Callback request
POST /shiftwa/webhook HTTP/1.1
Host: your-app.example.com
Content-Type: application/json
User-Agent: Shiftwa-Webhooks/1.0
X-Shiftwa-Event: message.sent
X-Shiftwa-Delivery-Id: 33333333-3333-4333-8333-333333333333
X-Shiftwa-Timestamp: 2026-06-02T03:10:21.000Z
X-Shiftwa-Signature: 7f9f0f...lowercase-hex
{
"id": "evt_44444444-4444-4444-8444-444444444444",
"event": "message.sent",
"timestamp": "2026-06-02T03:10:20.000Z",
"data": {
"messageId": "01J0YEXAMPLEMESSAGEID",
"numberId": "11111111-1111-4111-8111-111111111111",
"to": "6281234567890",
"chatType": "personal",
"messageType": "text",
"providerMessageId": "3EB0C767D097B3",
"status": "sent",
"sentAt": "2026-06-02T03:10:20Z"
}
}Headers
| Header | Meaning |
|---|---|
x-shiftwa-event | Event name, for example message.received. |
x-shiftwa-delivery-id | Unique delivery attempt id. Use it for idempotency. |
x-shiftwa-timestamp | Timestamp included in the signature input. |
x-shiftwa-signature | Lowercase hex HMAC-SHA256 signature. |
Verify the signature
Verify the signature with the exact raw body bytes. Do this before you parse or trust the JSON payload.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyShiftwaWebhook(input: {
rawBody: string;
timestamp: string;
signature: string;
secret: string;
}) {
const timestampMs = Date.parse(input.timestamp);
if (!Number.isFinite(timestampMs)) return false;
const ageMs = Math.abs(Date.now() - timestampMs);
if (ageMs > 5 * 60 * 1000) return false;
const expected = createHmac("sha256", input.secret)
.update(input.rawBody)
.update(".")
.update(input.timestamp)
.digest("hex");
if (expected.length !== input.signature.length) return false;
return timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(input.signature, "hex"),
);
}Events
| Event | When it fires | Typical data fields |
|---|---|---|
message.queued | An outbound message was accepted by the API and queued for sending. | messageId, numberId, to, chatType, messageType, status |
message.sent | An outbound message was accepted by WhatsApp. | messageId, numberId, to, chatType, messageType, providerMessageId, status, sentAt |
message.delivered | WhatsApp reports that an outbound message reached the recipient device. | messageId, numberId, providerMessageId, status, messageStatus, deliveredAt, chat, recipient, isGroup |
message.read | WhatsApp reports that the recipient opened the chat and saw the message. | messageId, numberId, providerMessageId, status, messageStatus, deliveredAt, readAt, chat, recipient, isGroup |
message.failed | An outbound message reached a terminal failed state. | messageId, numberId, to, chatType, messageType, status, errorMessage |
message.received | A paired number receives an inbound WhatsApp message. | messageId, numberId, providerMessageId, from, to, messageType, text, isGroup, pushName, chatType, groupId?, senderLid? |
device.connected | A WhatsApp session becomes active. | numberId, phoneNumber, connectedAt |
device.disconnected | A WhatsApp session drops or is marked disconnected. | numberId, phoneNumber, reason, lastSeenAt |
device.logged_out | WhatsApp logs the device out permanently. | numberId, phoneNumber, reason |
quota.exceeded | A send was rejected because the monthly message quota is exhausted. | used, limit, resetAt |
webhook.test | You press the Test button in the dashboard. | message |
Understanding message status events
An outbound message moves through up to four states. Each state is a separate webhook, so you can update your own UI as the message progresses. Think of them like the WhatsApp ticks:
| Event | WhatsApp tick | What it really means |
|---|---|---|
message.queued | — | Shiftwa accepted your API request and queued it. Nothing has left for WhatsApp yet. |
message.sent | ✓ (one grey) | WhatsApp's servers accepted the message. |
message.delivered | ✓✓ (two grey) | The message reached the recipient's phone. They have not necessarily opened it. |
message.read | ✓✓ (two blue) | The recipient opened the chat and saw the message. |
Recipient identity: phone number vs LID
On receipt events (message.delivered, message.read) the person who received or read your message is identified two ways, and you should understand both before you key your data on either one.
data.recipient— the recipient's phone number (for example6281234567890) whenever WhatsApp gives it to us. This is the value you normally want.data.recipientLid— a LID("Linked ID"), an internal privacy alias WhatsApp uses to refer to a user without revealing their phone number (for example100571675648212). It looks like a long number but is not a phone number and cannot be messaged directly.
Sample payloads
{
"id": "evt_11111111-1111-4111-8111-111111111111",
"event": "message.queued",
"timestamp": "2026-06-02T03:11:30.000Z",
"data": {
"messageId": "01J0YEXAMPLEMESSAGEID",
"numberId": "11111111-1111-4111-8111-111111111111",
"to": "6281234567890",
"chatType": "personal",
"messageType": "text",
"status": "queued"
}
}{
"id": "evt_22222222-2222-4222-8222-222222222222",
"event": "message.sent",
"timestamp": "2026-06-02T03:12:45.000Z",
"data": {
"messageId": "01J0YEXAMPLEMESSAGEID",
"numberId": "11111111-1111-4111-8111-111111111111",
"to": "6281234567890",
"chatType": "personal",
"messageType": "text",
"providerMessageId": "3EB0D8A1",
"status": "sent",
"sentAt": "2026-06-02T03:12:45.000Z"
}
}{
"id": "evt_55555555-5555-4555-8555-555555555555",
"event": "message.received",
"timestamp": "2026-06-02T03:12:00.000Z",
"data": {
"messageId": "msg_3EB0D8A1",
"numberId": "11111111-1111-4111-8111-111111111111",
"providerMessageId": "3EB0D8A1",
"from": "6281299990000",
"to": "6285138741668",
"messageType": "text",
"text": "Halo, pesanan saya sampai kapan?",
"isGroup": false,
"pushName": "Ayu",
"chatType": "personal"
}
}{
"id": "evt_66666666-6666-4666-8666-666666666666",
"event": "message.failed",
"timestamp": "2026-06-02T03:13:00.000Z",
"data": {
"messageId": "01J0YEXAMPLEMESSAGEID",
"numberId": "11111111-1111-4111-8111-111111111111",
"to": "6281234567890",
"chatType": "personal",
"messageType": "text",
"status": "failed",
"errorMessage": "send timeout"
}
}{
"id": "evt_99999999-9999-4999-8999-999999999999",
"event": "message.delivered",
"timestamp": "2026-06-02T03:13:18.000Z",
"data": {
"messageId": "01J0YEXAMPLEMESSAGEID",
"numberId": "11111111-1111-4111-8111-111111111111",
"providerMessageId": "3EB0D8A1",
"status": "delivered",
"messageStatus": "delivered",
"deliveredAt": "2026-06-02T03:13:18.000Z",
"chat": "6281234567890@s.whatsapp.net",
"recipient": "6281234567890",
"recipientLid": "100571675648212",
"isGroup": false
}
}{
"id": "evt_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
"event": "message.read",
"timestamp": "2026-06-02T03:13:40.000Z",
"data": {
"messageId": "01J0YEXAMPLEMESSAGEID",
"numberId": "11111111-1111-4111-8111-111111111111",
"providerMessageId": "3EB0D8A1",
"status": "read",
"messageStatus": "delivered",
"deliveredAt": "2026-06-02T03:13:18.000Z",
"readAt": "2026-06-02T03:13:40.000Z",
"chat": "6281234567890@s.whatsapp.net",
"recipient": "6281234567890",
"recipientLid": "100571675648212",
"isGroup": false
}
}{
"id": "evt_77777777-7777-4777-8777-777777777777",
"event": "device.disconnected",
"timestamp": "2026-06-02T03:14:00.000Z",
"data": {
"numberId": "11111111-1111-4111-8111-111111111111",
"phoneNumber": "6285138741668",
"reason": "connection closed",
"lastSeenAt": "2026-06-02T03:14:00Z"
}
}{
"id": "evt_bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
"event": "device.connected",
"timestamp": "2026-06-02T03:10:00.000Z",
"data": {
"numberId": "11111111-1111-4111-8111-111111111111",
"phoneNumber": "6285138741668",
"connectedAt": "2026-06-02T03:10:00Z"
}
}{
"id": "evt_cccccccc-cccc-4ccc-8ccc-cccccccccccc",
"event": "device.logged_out",
"timestamp": "2026-06-02T03:16:00.000Z",
"data": {
"numberId": "11111111-1111-4111-8111-111111111111",
"phoneNumber": "6285138741668",
"reason": "logged out from another device"
}
}{
"id": "evt_dddddddd-dddd-4ddd-8ddd-dddddddddddd",
"event": "quota.exceeded",
"timestamp": "2026-06-02T03:17:00.000Z",
"data": {
"used": 500,
"limit": 500,
"resetAt": "2026-07-01T00:00:00.000Z"
}
}{
"id": "evt_88888888-8888-4888-8888-888888888888",
"event": "webhook.test",
"timestamp": "2026-06-02T03:15:00.000Z",
"data": {
"message": "This is a test webhook from Shiftwa."
}
}Receiver rules
- Use HTTPS in production.
- Read and verify the raw body before parsing JSON.
- Reject stale timestamps, for example older than 5 minutes.
- Dedupe by
x-shiftwa-delivery-idor payloadid. - Store or enqueue the event, then return
2xxquickly. - Do not run slow CRM, AI, or order-processing work inside the webhook request.
Retries
2xx: delivery is marked successful.4xx: delivery is terminal and will not be retried.5xx, network error, or timeout: Shiftwa retries.- Maximum attempts: 5.
- Retry waits after failed attempts: 30 seconds, 2 minutes, 10 minutes, 30 minutes.
- Receiver timeout: 10 seconds.
- Circuit breaker: after ~25 consecutive failed attempts (≈ five deliveries through the full retry ladder) the webhook is disabled automatically and you get an email/Telegram alert. Fix the receiver, then re-enable it from the dashboard — one successful delivery resets the failure streak.