Webhooks
Moria calls an HTTPS address you own whenever something terminal happens. This is how you find out a customer paid; polling is the fallback, not the design.
Set the address in the console, under Webhook → Alamat pengiriman. Until one is set, events are recorded but sent nowhere.
The events
Section titled “The events”| Event | When |
|---|---|
payment.succeeded | A customer paid, and the money is yours |
payment.failed | The provider refused the payment |
payment.expired | The instrument was never paid in time |
payout.succeeded | A transfer settled |
payout.failed | A transfer failed; your reservation has been returned |
test.ping | You pressed the test button. No money involved |
An expiry is confirmed by the provider where possible; where it is not, Moria expires the instrument itself once it is well past expires_at. Either way the event fires, so expires_at is a deadline you can act on rather than a hint.
The envelope
Section titled “The envelope”{ "id": "evt_01J9…", "type": "payment.succeeded", "occurred_at": "2026-09-03T07:20:00.000Z", "partner_id": "54e39b24-…", "data": { "reference_number": "INV-2026-0912", "amount": "100000.0000", "net_amount": "96000.0000", "currency": "IDR" }}The payment.succeeded event reports the gross amount and the partner’s net_amount. A payout.succeeded event reports the three payout figures: amount leaving the partner balance, one combined fee, and net_amount reaching the bank. The provider/margin fee split is not sent to partners.
Answer 2xx to acknowledge. Anything else — including a redirect — counts as a failure and will be retried.
Verify the signature
Section titled “Verify the signature”Every call carries:
x-moria-signature: t=1788505200,v1=<hex>v1 is HMAC-SHA256 over the string `${t}.${rawBody}`, keyed by your webhook signing secret.
const [t, v1] = header.split(',').map((part) => part.split('=')[1])
const expected = crypto .createHmac('sha256', webhookSigningSecret) .update(`${t}.${rawBody}`, 'utf8') .digest('hex')
// Constant-time. `===` leaks the answer through how long it takes.const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))Reject anything that fails verification, and reject a t that is far from now — that is what stops an old, genuine call being replayed at you later.
Deduplicate on id
Section titled “Deduplicate on id”Retries
Section titled “Retries”A delivery that does not get a 2xx is retried with widening gaps, up to ten attempts, then marked exhausted. A 4xx is retried too — a partner mid-deploy answering 404 should not permanently lose events.
Every attempt is visible in the console under Webhook, with the response code and the error, and there is a Kirim ulang button. Pressing it twice is safe: the event id does not change, so your deduplication still holds.
Testing before you go live
Section titled “Testing before you go live”On Webhook → Alamat pengiriman there is Kirim event uji. It sends one test.ping down exactly the same path as a real event — same signature, same retry behaviour, recorded in the same log. If your verification is wrong, this is where you find out, rather than on your first real payment.