Webhooks
Receive signed, retried events for messages, contacts, follow-ups and campaigns.
Webhooks let you react to events inside Lynkist without polling. When something interesting happens — a message is delivered, a contact opts out, a campaign completes — we POST a signed JSON payload to an HTTPS endpoint you configure.
This page describes the receive-side contract: events, headers, signatures, retries. For the API to manage endpoints (create, list, rotate-secret, retry deliveries), see the Webhook management API.
How it works
- You register an endpoint URL via the dashboard or the Webhook management API and subscribe it to one or more event types.
- When a subscribed event fires, Lynkist enqueues a delivery and POSTs a signed JSON body to your URL.
- Your endpoint returns a 2xx status within 10 seconds.
- If it does not, we retry on a fixed back-off curve for up to 5 more attempts.
- After 20 consecutive failures across all deliveries, the endpoint is auto-disabled and stops receiving events until you reactivate it.
Event catalog
Every value below is an event Lynkist actually emits today.
Contacts (contact.*)
| Event | When |
|---|---|
contact.created | A contact was added (dashboard, import, or API). |
contact.updated | Any field on a contact changed. |
contact.deleted | A contact was archived (soft-delete). |
contact.note_added | A note was attached to a contact. |
contact.communication_added | A communication-history row was attached to a contact. |
Follow-ups (followup.*)
| Event | When |
|---|---|
followup.created | A follow-up reminder was scheduled. |
followup.updated | A follow-up was edited or marked done. |
followup.deleted | A follow-up was removed. |
Messages (message.*)
| Event | When |
|---|---|
message.sent | Meta accepted the outbound message. |
message.delivered | Meta confirmed delivery to the recipient device. |
message.read | The recipient opened the message (only when read receipts are enabled). |
message.failed | Meta reported a permanent send failure (includes the error code). |
message.received | An inbound message arrived from a user. |
Campaigns (campaign.*)
| Event | When |
|---|---|
campaign.created | A new campaign was created. |
campaign.scheduled | A campaign was scheduled for a future start time. |
campaign.started | Dispatch began (either at the scheduled time or via "Send now"). |
campaign.paused | A running or scheduled campaign was paused. |
campaign.resumed | A paused campaign was resumed. |
campaign.stopped | A campaign was permanently stopped (cannot be resumed). |
campaign.completed | All recipients were processed. |
campaign.failed | The campaign aborted before completion (e.g. fatal template/WABA error). |
Webhook lifecycle (webhook.*)
| Event | When |
|---|---|
webhook.test | You triggered a test delivery from the dashboard or via POST /webhooks/{id}/test. |
Template lifecycle events (template.approved, template.rejected, …) are not emitted
today. They will appear in this catalogue when wired up — track Changelog.
Request headers
Every delivery includes these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Lynkist-Webhook/1.0 |
X-Lynkist-Event | The event type, e.g. message.delivered |
X-Lynkist-Delivery-ID | UUID of this delivery attempt — unique per attempt |
X-Lynkist-Webhook-ID | UUID of your endpoint |
X-Lynkist-Timestamp | Unix epoch seconds when the request was signed |
X-Lynkist-Signature | sha256=<hex> — see Signature verification |
Test deliveries additionally carry X-Lynkist-Verification: true.
Payload shape
Every webhook payload follows the same envelope:
{
"id": "evt_01HW3K…",
"type": "message.delivered",
"created_at": "2026-05-31T08:30:01Z",
"data": {
"message_id": "wamid.HBgM…",
"to": "+91…",
"delivered_at": "2026-05-31T08:30:01Z"
}
}The data object varies by event type. Stay forward-compatible: ignore unknown fields rather
than failing on them.
Signature verification
The X-Lynkist-Signature value is sha256=<hex> where <hex> is an HMAC-SHA256 over the
string f"{timestamp}.{body}", keyed by the endpoint's signing secret. The timestamp is the
exact value in the X-Lynkist-Timestamp header (seconds since epoch, as a string).
Always reconstruct the signed payload from the raw request body — re-serialising parsed JSON will not round-trip and the signature will not match.
Node.js (TypeScript)
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyLynkistSignature({
rawBody,
timestamp,
signatureHeader,
secret,
}: {
rawBody: string
timestamp: string
signatureHeader: string // e.g. "sha256=abc..."
secret: string
}): boolean {
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signatureHeader)
return a.length === b.length && timingSafeEqual(a, b)
}Python
import hmac, hashlib
def verify_lynkist_signature(raw_body: bytes, timestamp: str, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature_header)Verify before you trust. Without signature checking, anyone who guesses your endpoint URL can
POST fake events. Reject any request whose signature does not match — and consider rejecting
any request whose X-Lynkist-Timestamp is more than a few minutes old to thwart replay
attacks.
Retries
If your endpoint returns a non-2xx response or does not reply within 10 seconds, the delivery is retried on this fixed back-off curve:
| Attempt | Delay before this attempt |
|---|---|
| 1 | — (initial) |
| 2 | 1 min |
| 3 | 5 min |
| 4 | 30 min |
| 5 | 2 hr |
| 6 | 6 hr |
After the 6th failed attempt the delivery is marked abandoned. The delivery row stays
queryable via the Webhook management API for replays and audits.
After 20 consecutive failures across an endpoint (any combination of deliveries), the
endpoint itself is auto-disabled — its is_active flag flips to false and no further
events are queued for it. Reactivate by editing the endpoint via the API or dashboard.
Best practices
- Return 2xx fast. Acknowledge the event, then process asynchronously. A slow handler triggers unnecessary retries and burns through your error budget.
- Verify the signature. Always — even for "internal" endpoints behind a firewall.
- Deduplicate on the envelope
id. Network conditions can deliver the same event more than once. - Subscribe selectively. Pick the event types you actually use; you can always add more
later by
PUTing the endpoint. - Test with
POST /webhooks/{id}/test. This emits awebhook.testenvelope and is the cleanest way to validate your signature code end-to-end. - Reactivate auto-disabled endpoints carefully. If we disabled an endpoint, something at your end was repeatedly broken. Fix it before flipping the flag.