Webhooks
Status callbacks, signatures, and retries
Pass webhook.url and optional webhook.secret on POST /messages. EcoWave POSTs an envelope with id, event, createdAt, and data for message.accepted, message.sent, message.delivered, and message.failed. Inbound replies may also emit message.received.
{
"id": "4f2a8b10-1c2d-4e5f-9012-abcdef123456",
"event": "message.delivered",
"createdAt": "2026-08-08T09:12:19.000Z",
"data": {
"id": "7f8d9e1a-4b2c-4d5e-9f01-234567890abc",
"organizationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event": "message.delivered",
"status": "delivered",
"from": "EcoWave",
"to": "+251911234567",
"segments": 1,
"providerMessageId": null,
"errorCode": null,
"errorMessage": null,
"createdAt": "2026-08-08T09:12:04.000Z",
"sentAt": "2026-08-08T09:12:10.000Z",
"deliveredAt": "2026-08-08T09:12:18.000Z",
"failedAt": null
}
}Verify signatures
When a secret is set, headers include X-EcoWave-Timestamp, X-EcoWave-Delivery, X-EcoWave-Event, and X-EcoWave-Signature as sha256=<hex>. Compute HMAC-SHA256 over timestamp + "." + rawBody, strip the sha256= prefix, then compare in constant time. Deduplicate on X-EcoWave-Delivery and return 2xx within 10 seconds.
import crypto from "node:crypto";
function verify(rawBody, timestamp, signatureHeader, secret) {
const provided = signatureHeader.startsWith("sha256=")
? signatureHeader.slice("sha256=".length)
: signatureHeader;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(provided);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Non-2xx responses and timeouts retry with backoff. Exhausted deliveries land in the webhook DLQ and can be redriven via the API or console.