SMS YANU documentation home

Delivery webhooks

Get a POST to your server each time a message is delivered or fails.

A webhook tells your server what happened to a message, so you do not have to keep asking.

Set your webhook URL

In the dashboard open Developers then Webhooks, or call the API:

PUT https://api.smsyanu.com/v1/webhooks
Content-Type: application/json

{ "dlr_url": "https://example.mw/sms/dlr", "is_active": true }
  • The URL must be public HTTPS. Private and local addresses are refused.
  • An empty string clears the URL.
  • GET /v1/webhooks shows the current settings and your signing secret.
  • POST /v1/webhooks/test sends a test event.
  • POST /v1/webhooks/rotate-secret makes a new signing secret. Update your server before you rotate.

What we send

We POST JSON when a message reaches a final status:

{
  "event": "message.delivered",
  "message_id": "6f1c2b9e-1d2a-4c55-9a0e-2b7f0c1d3e4f",
  "status": "delivered",
  "phone": "+265990000000",
  "sender_id": "YANU",
  "segments": 1,
  "encoding": "GSM-7",
  "livemode": true,
  "cost": 20.0,
  "cost_currency": "MWK",
  "error_code": null,
  "error_message": null,
  "retry_count": 0,
  "delivered_at": "2026-11-05T08:30:06+00:00",
  "failed_at": null
}

Headers:

HeaderMeaning
X-Webhook-IDUnique id for this event. Use it to skip duplicates.
X-Webhook-Signaturesha256= then the hex HMAC-SHA256 of the raw body, keyed with your signing secret.

Check the signature

Always check the signature before you trust the body. Compare in constant time.

import hashlib, hmac

def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")
import crypto from 'node:crypto';

export function isValid(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(header || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
<?php
function is_valid(string $rawBody, string $header, string $secret): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $header);
}

Answer quickly

  • Reply with any 2xx status within 15 seconds. Do slow work after you reply.
  • If your server fails, we retry twice, after about 5 and 15 seconds. After that the event stays in your webhook log in the dashboard, where you can send it again.
  • The same event can arrive more than once. Use X-Webhook-ID or message_id to ignore repeats.

To be confirmed

The payload fields, retry timing and dashboard menu names come from the current code and may change before launch.

On this page