Blueticks

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.id
created, _ := 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: 6a380a8258bece30b485036e
  • id is unique per delivery — store it and ignore repeats, since a retry can re-deliver an event your server already handled (see Reliability).
  • Blueticks-Webhook-Id is the id of your webhook that matched — handy when one server backs several endpoints.
  • Respond 2xx within 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 you
  • reply_to_my_message_webhook — a reply to one of your messages
  • message_edited_webhook · message_deleted_revoked_webhook — a message was edited / deleted
  • message_reaction_webhook — an emoji reaction
  • poll_vote_webhook — a vote on your poll
  • ack_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_webhook
  • group_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 the whsec_ 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 "", 204
import 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.