Skip to content
Oxtro

Integrations

WhatsApp webhooks for replies and delivery status

Oxtro tells your backend when a customer replies, when a message is delivered or read, and when a number goes offline.

Ten events, one envelope

Incoming messages, four delivery statuses, connection changes and OTP activity all arrive in the same JSON shape: an event name, a timestamp, your workspace and a data object. See the event list.

Signed with HMAC-SHA256

Every request carries an X-Webhook-Signature header with a timestamp and a signature made with your endpoint secret, so you can reject forged or replayed requests.

Retried when your server is down

A delivery that does not get a 2xx response within 10 seconds is tried again, up to 5 times, with a growing wait between tries.

Visible deliveries

The Developer API page of your workspace lists recent deliveries per endpoint with the status your server returned, and can send a test event.

How it works

  1. Add an endpoint

    Give Oxtro a public HTTPS URL and choose events, in the app or with POST /v1/webhooks.

  2. Store the secret

    The signing secret is shown once. Keep it with your other server secrets.

  3. Verify and respond

    Check the signature against the raw body and answer with a 2xx status.

Verify the signature

An Express handler that checks the signature before it trusts the event. Examples for PHP, Laravel and Python are in the docs.

Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';

const app = express();

// express.raw keeps the body as the exact bytes Oxtro signed.
app.post('/webhooks/oxtro', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('X-Webhook-Signature');
  if (!isValidSignature(req.body, header, process.env.OXTRO_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }

  const event = JSON.parse(req.body);
  if (event.event === 'message.received') {
    console.log(event.data.from, event.data.content);
  }

  res.status(200).end();
});

function isValidSignature(rawBody, header = '', secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map((pair) => pair.split('=')));
  if (!/^\d+$/.test(parts.t ?? '') || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.`)
    .update(rawBody)
    .digest();
  const received = Buffer.from(parts.v1, 'hex');

  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

app.listen(3000);

Questions about Webhooks

Which events can I subscribe to?

message.received, message.sent, message.delivered, message.read, message.failed, connection.connected, connection.disconnected, otp.sent, otp.verified and otp.failed.

Can the same webhook event arrive twice?

Yes. A retry after a timeout can deliver an event your server already processed. The X-Webhook-Delivery header stays the same across retries, so use it to ignore duplicates.

Do I get messages from WhatsApp groups?

No. Incoming events cover direct messages to your connected numbers. Group messages and status broadcasts are not forwarded.

Can I register webhooks through the API?

Yes. An API key with the webhooks:manage scope can create, list and delete endpoints under /v1/webhooks. The signing secret is returned when the endpoint is created.

Connect your first number today

Create a workspace, scan the QR code and send a message.

Create account