Webhooks
Set callback_url when creating an invoice and Muamla will POST signed events there as the invoice changes state. Treat webhooks as the source of truth — don't rely on the browser redirect alone.
Events
| Event | When |
|---|---|
invoice.created | An invoice is created. |
invoice.verified | The transfer was confirmed with the bank — fulfil the order. |
invoice.failed | Verification failed. |
More events (invoice.expired, payment.refunded) are planned.
Request headers
| Header | Value |
|---|---|
x-muamla-signature | HMAC-SHA256 of the raw body (hex). |
x-muamla-event | The event type, e.g. invoice.verified. |
content-type | application/json |
user-agent | Muamla-Webhook/1.0 |
Payload
json
{
"type": "invoice.verified",
"data": {
"id": "inv_8x2K9aFqR3mB7nZ1",
"status": "verified",
"amount": 500000,
"currency": "MRU",
"amount_format": "5,000 MRU",
"reference": "MR-KSIEYR",
"metadata": {}
}
}The data object is the same shape as the invoice returned by the API.
Managing endpoints
Instead of a per-invoice callback_url, you can register reusable endpoints that receive every matching event. Each endpoint has its own signing secret.
bash
# create
curl -X POST https://app.muamla.org/v1/webhooks \
-u sk_test_your_key: \
-H "content-type: application/json" \
-d '{ "url": "https://example.com/hooks", "events": ["invoice.verified"] }'
# → { "id": "...", "url": "...", "events": [...], "secret": "whsec_…" }
curl https://app.muamla.org/v1/webhooks -u sk_test_your_key: # list
curl -X DELETE https://app.muamla.org/v1/webhooks/{id} -u sk_test_your_key: # deleteYou can also manage endpoints from the dashboard (Webhooks page). Omit events to receive all events.
Verifying the signature
Compute HMAC-SHA256 over the raw request body using your webhook signing secret (MUAMLA_WEBHOOK_SECRET) and compare it to x-muamla-signature with a timing-safe check.
typescript
import crypto from "node:crypto";
export async function POST(req: Request) {
const raw = await req.text();
const signature = req.headers.get("x-muamla-signature") ?? "";
const expected = crypto
.createHmac("sha256", process.env.MUAMLA_WEBHOOK_SECRET!)
.update(raw)
.digest("hex");
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return new Response("invalid signature", { status: 400 });
const event = JSON.parse(raw);
if (event.type === "invoice.verified") {
// fulfil the order for event.data.id
}
return new Response("ok");
}Use the raw body, not a re-serialized JSON object — re-encoding changes the bytes and breaks the signature. Return
200 quickly and make your handler idempotent; the same event may arrive more than once.