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
Add an endpoint
Give Oxtro a public HTTPS URL and choose events, in the app or with POST /v1/webhooks.
Store the secret
The signing secret is shown once. Keep it with your other server secrets.
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.
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.
Related
Connect your first number today
Create a workspace, scan the QR code and send a message.