Webhooks

Hear about deliveries, new customers and more as they happen.

Webhooks tell your own systems when something happens in Goomzo, so they don't have to keep asking. Register a URL, choose the events it hears, and Goomzo sends each one there as it happens.

Add an endpoint

  1. Open Settings → Webhooks and select New endpoint. Only the organization's owner and admins can.
  2. Enter the URL that will receive events. It must be https:// and reachable from the internet.
  3. Choose the events it hears. Choose at least one.
  4. Open the endpoint and select Send test event to check that your receiver works.

An organization can have up to 10 endpoints. Turn one off to pause it: its events wait and are sent when it's turned on again.

You can also manage endpoints through the API, with a service account that has the webhooks.manage permission:

curl https://api.goomzo.africa/v1/webhook-endpoints \
  -H "Authorization: Bearer $GOOMZO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhooks/goomzo", "events": ["message.delivered", "message.failed"]}'

The answer includes the endpoint's secret, which you'll use to check signatures.

Events

EventWhen
message.sentThe gateway took a message.
message.deliveredA message reached the customer's phone.
message.failedA message couldn't be sent or delivered. error says why.
broadcast.finishedA broadcast finished going out, or couldn't. Includes its counts.
customer.createdA customer was added, one by one, through the API or by import.
sms.balance_lowFewer than 100 SMS are left. Sent once each time it falls below.
webhook.testOnly when you select Send test event.

What arrives

Each event is a POST with a JSON body:

{
  "id": "evt_4hq2v9n1c8x0t6pk",
  "type": "message.delivered",
  "createdAt": "2026-11-08T09:30:00Z",
  "organizationId": "org_...",
  "data": {
    "id": "msg_...",
    "broadcastId": null,
    "customerId": "cus_...",
    "channel": "sms",
    "recipient": "+255712345678",
    "status": "delivered",
    "error": null,
    "cost": 1,
    "sentAt": "2026-11-08T09:29:58Z",
    "deliveredAt": "2026-11-08T09:30:00Z",
    "failedAt": null,
    "test": false
  }
}

data depends on the event:

  • Messages: id, broadcastId, customerId, channel, recipient, status, error, cost, sentAt, deliveredAt, failedAt and test.
  • broadcast.finished: id, name, channel, status (sent or failed), error, recipients, sent, delivered, failed and test.
  • customer.created: id, name, phone, email and attributes.
  • sms.balance_low: left and threshold.

These headers come with it:

HeaderValue
Goomzo-Event-IdThe event's id
Goomzo-Event-TypeThe event's type
Goomzo-Delivery-IdThis delivery, as shown in the log
Goomzo-Signaturet=<unix seconds>,v1=<signature>

Check the signature

Anyone can post to your URL, so check that each event is from Goomzo before you act on it. v1 is a hex HMAC-SHA256 of the time t, a dot, and the raw body, made with the endpoint's secret.

  1. Read t and v1 from the Goomzo-Signature header.
  2. Compute the signature from t + "." + body, using the body exactly as it arrived, before parsing it.
  3. Accept the event only if the two match and t is within 5 minutes of now.
Node.js
import crypto from 'node:crypto'

function isFromGoomzo(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ''))
}
Python
import hashlib, hmac, time

def is_from_goomzo(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts.get("v1", ""))

The secret starts with whsec_. Find it in the endpoint under Signing secret.

Answer quickly

Answer with any 2xx status within 10 seconds. Do the slow work afterwards, for example in a queue.

Anything else counts as a failure: another status, a timeout, or no answer. Redirects aren't followed, so use the final URL. A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, then 2, 5, 10 and 20 hours. After 8 tries, about a day and a half, it's marked failed.

The same event can arrive more than once, and events can arrive out of order. Use the event id to skip ones you've already handled, and the timestamps in data to tell which came last.

The delivery log

Open an endpoint to see its deliveries from the last 30 days: what was sent, how many tries it took, and what your server answered. Select one to see its full body and your server's response, and select Resend to send it again.

Through the API, list an endpoint's deliveries with GET /v1/webhook-endpoints/{id}/deliveries, and send one again with POST /v1/webhook-deliveries/{id}/resend.