Skip to content
WbizTool

API v2 (beta)

Webhooks

A webhook endpoint is a URL on your server that WbizTool calls when something happens to a message: it was sent, it failed, it was cancelled or it expired. You get the news straight away instead of polling, and every request is signed so you can check it came from WbizTool.

OperationEndpointScope
Add an endpointPOST /api/v2/webhooksmessages:read
List endpointsGET /api/v2/webhooksmessages:read
Get an endpointGET /api/v2/webhooks/{id}messages:read
Remove an endpointDELETE /api/v2/webhooks/{id}messages:read
Replace the signing secretPOST /api/v2/webhooks/{id}/rotate-secretmessages:read
Send a test eventPOST /api/v2/webhooks/{id}/testmessages:read

Endpoints belong to the workspace and get events for all its messages, whatever sent them: the API, the dashboard, campaigns, reminders or integrations. That's why a key limited to some numbers can't manage them: every call answers 403 number_not_allowed.

Events#

TypeWhen
message.sentA message went out from your number.
message.failedA message couldn't be sent; error.message says why.
message.cancelledA waiting message was cancelled: with the API, from the dashboard, or because its number or campaign was removed.
message.expiredA message wasn't sent before its expires_at.
pingA test event you asked for.

Events for numbers (connected, logged out) and for incoming messages come later. Delivered and read ticks don't send events; get the message to see them.

Add an endpoint#

POSThttps://wbiztool.com/api/v2/webhooks

Body: JSON (application/json)

Body

urlstringrequired

Your endpoint: a public https:// address, up to 500 characters. http://, private and local addresses (such as localhost or 192.168.x.x) are refused.

eventsarray of stringsoptional

The event types it gets: any of message.sent, message.failed, message.cancelled and message.expired. Default: all four. ping always arrives when you ask for one.

descriptionstringoptional

A note for yourself, up to 100 characters, such as Order system.

A workspace can have up to 5 endpoints. The answer is 201 with the endpoint and its signing secret, shown this once. Store it where your receiver can read it, such as an environment variable. Later answers show only secret_last4; if you lose the secret, replace it.

curl -X POST https://wbiztool.com/api/v2/webhooks \
  -H "Authorization: Bearer $WBIZTOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/wbiztool/events",
    "events": ["message.sent", "message.failed"],
    "description": "Order system"
  }'

Answer (201):

{
  "id": 41,
  "url": "https://example.com/wbiztool/events",
  "description": "Order system",
  "events": ["message.sent", "message.failed"],
  "secret_last4": "9c1e",
  "active": true,
  "disabled_reason": null,
  "created_at": "2026-10-18T11:20:05.733Z",
  "last_delivery": null,
  "secret": "3f0b…9c1e"
}

The real secret is 64 hexadecimal characters. A retry of this request with the same Idempotency-Key doesn't add a second endpoint, and its answer leaves the secret out.

Errors when adding an endpoint#

HTTPCodeWhen
400validation_failedurl is missing or too long, events is empty or has an unknown type, or a field is unknown
403webhook_limit_reachedThe workspace already has 5 endpoints
403number_not_allowedThe key is limited to some numbers
422invalid_webhook_urlurl isn't a public https:// address

List endpoints#

GEThttps://wbiztool.com/api/v2/webhooks

The workspace's endpoints that receive events, oldest first, in one data list.

curl https://wbiztool.com/api/v2/webhooks \
  -H "Authorization: Bearer $WBIZTOOL_API_KEY"

The list leaves out endpoints that stopped. Owners also see the endpoints, and can remove them, on Settings → API keys under Webhook endpoints.

Get an endpoint#

GEThttps://wbiztool.com/api/v2/webhooks/{id}

One endpoint. last_delivery shows how its latest delivery went. An endpoint that doesn't exist or was removed answers 404 webhook_not_found; a stopped one is shown with active: false.

Remove an endpoint#

DELETEhttps://wbiztool.com/api/v2/webhooks/{id}

Nothing more is sent to it, including retries still waiting. The answer is {"id": 41, "deleted": true}.

Replace the signing secret#

POSThttps://wbiztool.com/api/v2/webhooks/{id}/rotate-secret

Makes a new secret for the endpoint. The answer is 200 with the endpoint and its new secret, shown this once. The old secret stops at once: events sent after this are signed with the new one, retries of earlier events included. Update your receiver straight away.

curl -X POST https://wbiztool.com/api/v2/webhooks/41/rotate-secret \
  -H "Authorization: Bearer $WBIZTOOL_API_KEY"

Send a test event#

POSThttps://wbiztool.com/api/v2/webhooks/{id}/test

Queues a ping event to this endpoint, signed like every event, so you can test your receiver. The answer is 202:

{ "event_id": "evt_8a2f4c1d9b7e6a5f4c3b2a19", "type": "ping", "queued": true }

Your endpoint receives:

{
  "id": "evt_8a2f4c1d9b7e6a5f4c3b2a19",
  "type": "ping",
  "created_at": "2026-10-18T11:21:40.002Z",
  "data": { "webhook_id": 41 }
}

The event payload#

Each event is a POST with a JSON body:

{
  "id": "evt_c41d7e2a9f3b8c6d5e4f3a21",
  "type": "message.sent",
  "created_at": "2026-10-18T10:15:09.912Z",
  "data": {
    "message": {
      "id": 9817263,
      "status": "sent",
      "from": { "id": 101, "phone": "919800000000", "platform": "whatsapp" },
      "to": { "phone": "+919876543210", "contact_id": 991 },
      "text": "Hi Asha, your order #1042 is ready.",
      "media": null,
      "send_at": null,
      "expires_at": null,
      "created_at": "2026-10-18T10:15:02.311Z",
      "sent_at": "2026-10-18T10:15:09.870Z",
      "error": null
    }
  }
}
FieldDescription
idThe event's id, evt_ and 24 characters. The same event keeps its id on every retry.
typeOne of the event types.
created_atWhen the event happened.
data.messageFor message.* events: the Message as it was then. A media.url in it works for at least 26 hours.
data.webhook_idFor ping: the endpoint's id.

Headers:

HeaderValue
Content-Typeapplication/json
User-AgentWbizTool-Webhooks/2
WbizTool-Event-IdThe event's id, the same on every retry
WbizTool-TimestampWhen this attempt was sent, in Unix seconds, such as 1792310409
WbizTool-Signaturev1= and the hex HMAC-SHA256 of <timestamp>.<raw body>, made with the endpoint's secret

Verify the signature#

Check every request before you trust it: work out the signature from the raw body (before your framework parses the JSON), compare it with the header in constant time, and refuse timestamps more than 5 minutes off, so an old request can't be replayed. It's the same scheme as API v1's webhook signatures, with the endpoint's own secret.

Then use WbizTool-Event-Id to drop repeats: the same event can arrive more than once, for example when your answer was lost and we tried again.

import hashlib, hmac, json, os, time

SECRET = os.environ["WBIZTOOL_WEBHOOK_SECRET"]

def verify(secret: str, body: bytes, headers) -> bool:
    ts, sig = headers.get("WbizTool-Timestamp", ""), headers.get("WbizTool-Signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    mac = "v1=" + hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, sig)

# Flask
from flask import Flask, request

app = Flask(__name__)
seen = set()  # use your database in production

@app.post("/wbiztool/events")
def events():
    body = request.get_data()  # the raw bytes
    if not verify(SECRET, body, request.headers):
        return "", 401
    event_id = request.headers["WbizTool-Event-Id"]
    if event_id in seen:
        return "", 200  # a repeat: already handled
    seen.add(event_id)
    event = json.loads(body)
    if event["type"] == "message.failed":
        message = event["data"]["message"]
        print("Failed:", message["id"], message["error"]["message"])
    return "", 200

In Django, pass request.body and request.headers.

Answer 2xx quickly, then do slow work afterwards (in a queue, say).

Retries#

  • Answer with any 2xx within 10 seconds. Anything else is a failure: another status code (4xx included), no answer within 10 seconds, or no connection within 3. Redirects are followed, at most 3, and each must lead to a public address.
  • A failed delivery is tried again after 1, 5 and 15 minutes, then 1, 2, 4, 8 and 8 hours: 9 tries over about 23 hours. After the last one, that event is dropped.
  • The retry has the same body and WbizTool-Event-Id, with a new WbizTool-Timestamp and signature.
  • Order isn't guaranteed. A retried message.sent can arrive after a later event. Use the message's status, or get the message, when the order matters.

last_delivery on the endpoint shows how the latest delivery went: state (pending, delivered or failed), the status_code your server answered, and when the event was made.

When endpoints stop#

An endpoint stops getting events when one of these happens. It stays in GET /webhooks with active: false and a disabled_reason, so you can see why; it can still be read and removed, but not tested or given a new secret (404).

  • It has failed for 24 hours. If nothing has been delivered to it for 24 hours while its deliveries kept failing, it's turned off and its waiting retries are dropped. Settings → API keys then shows it as Off. Fix your server, then add the endpoint again.
  • The key that made it is revoked or expires. Revoking a key stops everything it set up. Add the endpoint again with another key.
  • It's removed, with Remove an endpoint or on Settings → API keys.

Endpoints that stopped don't count toward the 5.

The webhook endpoint object#

FieldTypeDescription
idintegerThe endpoint's id.
urlstringWhere events are sent.
descriptionstringYour note; "" when there's none.
eventsarrayThe event types it gets.
secretstringThe signing secret: only in the answers to Add an endpoint and Replace the signing secret.
secret_last4stringThe last 4 characters of the secret, to tell which one you have.
activebooleanfalse when it gets no events any more – see When endpoints stop.
disabled_reasonstring or nullWhy it stopped (failing for 24 hours, or the key that made it was revoked or expired); null while active.
created_atstringWhen it was added.
last_deliveryobject or nullThe latest delivery tried: state, status_code (or null when your server didn't answer) and created_at. null before the first.