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

EventWhen
invoice.createdAn invoice is created.
invoice.verifiedThe transfer was confirmed with the bank — fulfil the order.
invoice.failedVerification failed.

More events (invoice.expired, payment.refunded) are planned.

Request headers

HeaderValue
x-muamla-signatureHMAC-SHA256 of the raw body (hex).
x-muamla-eventThe event type, e.g. invoice.verified.
content-typeapplication/json
user-agentMuamla-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:  # delete

You 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.