Skip to main content
Webhooks let Zexa push delivery events to your server in real time instead of requiring you to poll the API for status updates. When a message is delivered, fails, or a recipient opts out, Zexa sends an HTTP POST request to your registered endpoint with a JSON payload describing the event. This makes it easy to keep your own database in sync with message delivery state and react to opt-outs immediately.

Supported Events

EventDescription
message.deliveredMessage was successfully delivered to the recipient
message.failedMessage delivery failed after all retry attempts
message.undeliverableMessage could not be delivered (e.g. invalid number, deactivated account)
message.optoutRecipient replied STOP or clicked an unsubscribe link
campaign.sentAll messages in a campaign have been dispatched to carriers
campaign.completedCampaign delivery is finished — all messages have reached a final status

Register a Webhook

Via the Dashboard

1

Open Webhook settings

Go to Settings → Webhooks in your Zexa dashboard and click Add Webhook.
2

Enter your endpoint URL

Provide the HTTPS URL of the endpoint on your server that will receive webhook events (e.g. https://yourapp.com/webhooks/zexa). HTTP endpoints are not accepted.
3

Select events

Choose the specific events you want to subscribe to. You can subscribe to all events or a targeted subset.
4

Save and verify

Click Save. Zexa will immediately send a test event to your endpoint to verify it is reachable and returning a 2xx response.

Via the API

You can also register webhooks programmatically:
POST https://api.zexa.ao/v1/webhooks

Request Parameters

url
string
required
The HTTPS URL of your endpoint that will receive webhook events. Must use HTTPS — plain HTTP is not accepted.
events
array
required
An array of event names to subscribe to. Subscribe to one or more of: message.delivered, message.failed, message.undeliverable, message.optout, campaign.sent, campaign.completed. Pass ["*"] to subscribe to all events.
curl -X POST https://api.zexa.ao/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/zexa",
    "events": ["message.delivered", "message.failed", "message.optout"]
  }'

Response Fields

id
string
The unique webhook identifier, prefixed with wh_ (e.g. wh_ghi789).
url
string
The registered endpoint URL.
events
array
The list of event names this webhook is subscribed to.
created_at
string
ISO 8601 UTC timestamp of when the webhook was registered.
Example response (201 Created):
{
  "id": "wh_ghi789",
  "url": "https://yourapp.com/webhooks/zexa",
  "events": ["message.delivered", "message.failed", "message.optout"],
  "created_at": "2026-06-24T10:00:00Z"
}

Error Scenarios

StatusErrorDescription
400invalid_requestMissing required fields or malformed JSON body
401unauthorizedAPI key is missing or invalid
422validation_errorurl is not a valid HTTPS URL, or events contains an unrecognised event name

Webhook Payload

When a subscribed event occurs, Zexa sends a POST request to your endpoint with a JSON body in the following structure:
{
  "event": "message.delivered",
  "timestamp": "2026-06-24T10:05:00Z",
  "data": {
    "message_id": "msg_abc123",
    "channel": "sms",
    "to": "+244912345678",
    "status": "delivered",
    "delivered_at": "2026-06-24T10:04:55Z"
  }
}
FieldDescription
eventThe event type that triggered this delivery
timestampISO 8601 UTC datetime of when the event occurred
dataEvent-specific payload containing resource IDs, status, and timestamps

Responding to Webhooks

Your endpoint must return a 2xx HTTP status code within 10 seconds of receiving the request. If your endpoint does not respond in time, or returns a non-2xx status, Zexa treats the delivery as failed and retries automatically up to 3 times using exponential backoff (approximately 1 minute, 5 minutes, then 30 minutes between attempts).
Acknowledge the webhook immediately by returning 200 OK as soon as you receive the request, then process the event asynchronously (e.g. via a background job or message queue). This prevents timeout failures caused by slow processing logic inside the request handler.

Verify Webhook Authenticity

Zexa signs every webhook request with an HMAC-SHA256 signature computed from the raw request body and your webhook secret. The signature is included in the X-Zexa-Signature request header. Always verify this signature before processing the payload to ensure the request genuinely originated from Zexa. You can find your webhook secret in Settings → Webhooks next to the registered endpoint.
import hashlib
import hmac

def verify_zexa_signature(raw_body: bytes, secret: str, header_signature: str) -> bool:
    """
    Verify the HMAC-SHA256 signature on an incoming Zexa webhook request.

    Args:
        raw_body: The raw, undecoded request body bytes.
        secret: Your webhook secret from the Zexa dashboard.
        header_signature: The value of the X-Zexa-Signature header.

    Returns:
        True if the signature is valid, False otherwise.
    """
    expected = hmac.new(
        key=secret.encode("utf-8"),
        msg=raw_body,
        digestmod=hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, header_signature)


# Example usage in a Flask handler
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_here"

@app.route("/webhooks/zexa", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Zexa-Signature", "")

    if not verify_zexa_signature(request.get_data(), WEBHOOK_SECRET, signature):
        abort(403)

    event = request.get_json()
    # Queue the event for async processing here
    return "", 200
Always verify the X-Zexa-Signature header before processing any webhook payload. Skipping signature verification makes your endpoint vulnerable to spoofed events that could trigger unintended actions in your application.