Receiving webhooks
Get Blueticks events — message delivery, incoming WhatsApp messages, group activity, sessions, campaigns — POSTed to your server in real time.
Webhooks push Blueticks events to your server over HTTPS, in near real time — so you can react to message delivery, incoming WhatsApp messages, group activity, session changes, and campaign progress without polling the API.
This guide assumes you have an API key (Authentication) and an SDK client set up (Quickstart).
1. Register a webhook
Point an HTTPS endpoint at Blueticks and subscribe it to the events you care about (pick any from the catalog below). You can register several endpoints, each with its own event list.
created = bt.webhooks.create(
url="https://api.your-server.com/bt-webhooks",
events=["message.delivered", "message.failed"],
description="prod",
)
print(created.id)const webhook = await bt.webhooks.create({
url: 'https://api.your-server.com/bt-webhooks',
events: ['message.delivered', 'message.failed'],
});$created = $bt->webhooks->create(
url: 'https://api.your-server.com/bt-webhooks',
events: ['message.delivered', 'message.failed'],
);
echo $created->id;created = client.webhooks.create(
url: "https://api.your-server.com/bt-webhooks",
events: ["message.delivered", "message.failed"],
description: "prod",
)
puts created.idcreated, _ := c.Webhooks.Create(context.Background(), blueticks.CreateWebhookParams{
URL: "https://api.your-server.com/bt-webhooks",
Events: []string{"message.delivered", "message.failed"},
Description: "prod",
})
fmt.Println(created.ID)curl -X POST https://api.blueticks.co/v1/webhooks \
-H "Authorization: Bearer BLUETICKS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url":"https://api.your-server.com/bt-webhooks",
"events":["message.delivered","message.failed"]
}'Prefer no code? You can also add and toggle webhooks from the dashboard under Settings → API → Webhooks.
2. What you receive
Each event is a single HTTPS POST. The body is always the same envelope —
only type and data change:
{
"id": "evt_01h7za9c8m...",
"type": "message.delivered",
"created_at": "2026-04-23T16:42:00.000Z",
"data": { /* fields specific to this event type */ }
}with these headers:
Content-Type: application/json
Blueticks-Webhook-Id: 6a380a8258bece30b485036eidis unique per delivery — store it and ignore repeats, since a retry can re-deliver an event your server already handled (see Reliability).Blueticks-Webhook-Idis the id of your webhook that matched — handy when one server backs several endpoints.- Respond
2xxwithin 10 seconds. Anything else (or a timeout) is treated as a failure and retried. Do heavy work after you respond.
3. Events you can subscribe to
You only receive the events you subscribe to. Group them however you like across one or more endpoints.
Message delivery — lifecycle of messages you send:
message.queued · message.sending · message.delivered · message.read · message.failed
Incoming WhatsApp activity:
new_message_received_webhook— someone messaged youreply_to_my_message_webhook— a reply to one of your messagesmessage_edited_webhook·message_deleted_revoked_webhook— a message was edited / deletedmessage_reaction_webhook— an emoji reactionpoll_vote_webhook— a vote on your pollack_changed_webhook— a message's delivered/read status changed
Group & chat activity:
participant_joined_via_link_webhook·participant_added_by_admin_webhook·participant_left_group_webhook·participant_kicked_from_group_webhookgroup_admin_changed_webhook·group_name_changed_webhook·group_description_changed_webhook·group_message_pinned_webhook
Sessions — session.connected · session.disconnected
Campaigns — campaign.started · campaign.paused · campaign.resumed · campaign.completed · campaign.aborted
The Create webhook reference lists the exact accepted enum.
4. Verify requests really came from Blueticks
Every webhook gets its own signing secret (whsec_…). It is returned as
signingSecret in the webhook object when you create it, and on every
list / get afterwards — store it on your server like any other credential.
Each delivery from a signed webhook carries three extra headers:
Blueticks-Webhook-Timestamp: 1776962520
Blueticks-Webhook-Signature: v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
X-Blueticks-Signature: sha256=0b4fa9c2e1d7...Blueticks-Webhook-Timestamp— when the delivery was signed, in Unix seconds.Blueticks-Webhook-Signature—v1=followed by the hex HMAC-SHA256 of the string{timestamp}.{raw body}, keyed with your signing secret (the whole string, including thewhsec_prefix). Because the timestamp is signed too, this is the one to use: check the signature and reject timestamps that are too old to stop replays.X-Blueticks-Signature—sha256=followed by the hex HMAC-SHA256 of the raw body alone, same key. Simpler, but gives no replay protection.
Compute the HMAC over the raw request bytes, exactly as received — before any JSON parsing. Re-serializing a parsed object changes key order or whitespace and the signature will never match.
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["BLUETICKS_WEBHOOK_SECRET"] # whsec_...
TOLERANCE_SECONDS = 300
@app.post("/bt-webhooks")
def bt_webhook():
raw = request.get_data() # raw bytes, before any JSON parsing
ts = request.headers.get("Blueticks-Webhook-Timestamp", "")
sig = request.headers.get("Blueticks-Webhook-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > TOLERANCE_SECONDS:
abort(400)
expected = "v1=" + hmac.new(
SECRET.encode(), ts.encode() + b"." + raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
event = request.get_json()
# ... handle event["type"] / event["data"], then respond fast
return "", 204import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.BLUETICKS_WEBHOOK_SECRET!; // whsec_...
const TOLERANCE_SECONDS = 300;
// express.raw keeps the body as a Buffer — do NOT use express.json() here.
app.post('/bt-webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const raw: Buffer = req.body;
const ts = req.header('Blueticks-Webhook-Timestamp') ?? '';
const sig = req.header('Blueticks-Webhook-Signature') ?? '';
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > TOLERANCE_SECONDS) {
return res.sendStatus(400);
}
const expected =
'v1=' +
crypto
.createHmac('sha256', SECRET)
.update(Buffer.concat([Buffer.from(`${ts}.`, 'utf8'), raw]))
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(sig);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const event = JSON.parse(raw.toString('utf8'));
// ... handle event.type / event.data, then respond fast
res.sendStatus(204);
});To use X-Blueticks-Signature instead, drop the timestamp: compare
sha256= + HMAC-SHA256(secret, raw body) against the header.
Webhooks created before signing shipped stay unsigned — they have no
signingSecret and their deliveries carry none of the headers above. Delete and
recreate such a webhook to get a secret. Until then (or if your platform can't
compute an HMAC), you can still protect an endpoint by registering a URL that
contains a hard-to-guess token, e.g.
https://api.your-server.com/bt-webhooks?token=ek3f9q2..., and rejecting any
POST without it. Always serve the endpoint over HTTPS.
5. Reliability: retries & auto-disable
- Each delivery attempt must complete within 10 seconds.
- A non-2xx response or a timeout is retried — up to 8 attempts total (the
first delivery plus 7 retries) over roughly 21 hours, with growing backoff
between attempts:
1m → 5m → 15m → 1h → 2h → 6h → 12h. - Because of retries, the same event can arrive more than once — dedupe on
the delivery
id. - After 8 consecutive failures, the endpoint auto-disables. Once your
server is healthy, re-enable it by patching
status: "enabled"(next section).
6. Manage your webhooks
List every endpoint, fetch one by id, change its URL / events / status, or delete it:
curl -X GET 'https://api.blueticks.co/v1/webhooks' \ -H 'Authorization: Bearer BLUETICKS_API_KEY'import blueticksbt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")result = bt.webhooks.list()import { Blueticks } from 'blueticks';const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });const result = await bt.webhooks.list();use Blueticks\Blueticks;$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);$result = $bt->webhooks->list();require "blueticks"client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")result = client.webhooks.list()import ( "context" blueticks "github.com/serenix-com/blueticks-go")client, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))result, _ := client.Webhooks.List(context.Background())curl -X GET 'https://api.blueticks.co/v1/webhooks/string' \ -H 'Authorization: Bearer BLUETICKS_API_KEY'import blueticksbt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")result = bt.webhooks.get( "id_01H7...",)import { Blueticks } from 'blueticks';const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });const result = await bt.webhooks.get('id_01H7...');use Blueticks\Blueticks;$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);$result = $bt->webhooks->retrieve('id_01H7...');require "blueticks"client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")result = client.webhooks.get( "id_01H7...",)import ( "context" blueticks "github.com/serenix-com/blueticks-go")client, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))result, _ := client.Webhooks.Get(context.Background(), "id_01H7...")curl -X PATCH 'https://api.blueticks.co/v1/webhooks/string' \ -H 'Authorization: Bearer BLUETICKS_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "url": "https://example.com", "events": [ "message.queued" ], "description": "string", "status": "enabled"}'import blueticksbt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")result = bt.webhooks.update( "id_01H7...", # request body fields…,)import { Blueticks } from 'blueticks';const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });const result = await bt.webhooks.update('id_01H7...', { /* … */ });use Blueticks\Blueticks;$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);$result = $bt->webhooks->update('id_01H7...', /* opts */);require "blueticks"client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")result = client.webhooks.update( "id_01H7...", # request body fields…,)import ( "context" blueticks "github.com/serenix-com/blueticks-go")client, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))result, _ := client.Webhooks.Update(context.Background(), "id_01H7...", params){ "url": "https://example.com", "events": [ "message.queued" ], "description": "string", "status": "enabled"}curl -X DELETE 'https://api.blueticks.co/v1/webhooks/string' \ -H 'Authorization: Bearer BLUETICKS_API_KEY'import blueticksbt = blueticks.Blueticks(api_key="BLUETICKS_API_KEY")result = bt.webhooks.delete( "id_01H7...",)import { Blueticks } from 'blueticks';const bt = new Blueticks({ apiKey: 'BLUETICKS_API_KEY' });const result = await bt.webhooks.delete('id_01H7...');use Blueticks\Blueticks;$bt = new Blueticks(['apiKey' => 'BLUETICKS_API_KEY']);$result = $bt->webhooks->delete('id_01H7...');require "blueticks"client = Blueticks::Client.new(api_key: "BLUETICKS_API_KEY")result = client.webhooks.delete( "id_01H7...",)import ( "context" blueticks "github.com/serenix-com/blueticks-go")client, _ := blueticks.NewClient(blueticks.WithAPIKey("BLUETICKS_API_KEY"))result, _ := client.Webhooks.Delete(context.Background(), "id_01H7...")To re-enable an auto-disabled endpoint, PATCH it with status: "enabled" once
your server is healthy again.