Webhooks
Standard Webhooks, tanda tangan nyaur_whsec_, verifikasi, retry, dan replay.
Nyaur delivers platform events to your HTTPS endpoints using the Standard Webhooks convention. Every delivery is signed with your endpoint's secret (nyaur_whsec_…, issued once at creation) so you can verify it came from us and arrived unmodified.
Events
| Event | Fired when |
|---|---|
checkout.created | A checkout session was opened. |
checkout.paid | The buyer completed payment. |
checkout.expired | The session expired unpaid. |
order.paid | The order settled; ledger rows and invoice exist. |
order.refunded | A refund completed (partial or full). |
subscription.created | A subscription started. |
subscription.renewed | A renewal invoice was paid. |
subscription.canceled | The subscription was canceled. |
subscription.past_due | A renewal went unpaid after reminders (dunning). |
benefit.granted | A benefit was granted (e.g. license key issued). |
payout.created | A payout was requested. |
payout.paid | A payout reached the bank. |
organization.reviewed | Product review or org status changed. |
Payload berupa JSON. Isi payload berbeda per event, misalnya sessionId pada checkout.* dan invoiceId, subscriptionId pada subscription.*.
Signature verification
Each delivery carries three headers:
| Header | Value |
|---|---|
webhook-id | Unique delivery id, stable across retries. |
webhook-timestamp | Unix seconds (string). |
webhook-signature | v1,<base64(HMAC-SHA256(key, signedContent))> |
The signed content is <webhook-id>.<webhook-timestamp>.<raw body>. The HMAC key is the secret's nyaur_whsec_ prefix stripped, then base64-decoded.
Verify in TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.NYAUR_WEBHOOK_SECRET as string;
const TOLERANCE_SECONDS = 300;
export function verifyWebhook(headers: Headers, rawBody: string): boolean {
const id = headers.get("webhook-id") ?? "";
const timestamp = headers.get("webhook-timestamp") ?? "";
const signature = headers.get("webhook-signature") ?? "";
// 1. Freshness, reject replays older than 5 minutes.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
// 2. Signature, constant-time comparison over the signed content.
const key = Buffer.from(SECRET.replace("nyaur_whsec_", ""), "base64");
const signedContent = `${id}.${timestamp}.${rawBody}`;
const expected = createHmac("sha256", key)
.update(signedContent)
.digest("base64");
const received = signature.replace(/^v1,/, "");
const a = Buffer.from(expected);
const b = Buffer.from(received);
return a.length === b.length && timingSafeEqual(a, b);
}Verify against the raw request body, not a re-serialized object, key order and whitespace are part of the signature.
Retries and idempotency
- Setiap delivery dicoba sampai 6 kali. Jeda setelah percobaan 1 sampai 5 adalah 5, 25, 125, 625, dan 3125 detik. Respons
2xxmenghentikan retry. webhook-idstabil di antara retry. Simpan id yang sudah diproses sebentar dan lewati duplikat.- Tolak timestamp yang selisihnya lebih dari 300 detik dari jam kamu.
- Replay lewat API:
POST /v1/events/{id}/replaymengirim ulang sebuah event ke semua endpoint aktif,POST /v1/deliveries/{id}/replaymengirim ulang satu delivery. Dari dashboard: Webhooks, endpoint, Replay.
Mengelola endpoint
| Endpoint | Fungsi |
|---|---|
POST /v1/webhooks | Buat endpoint. Body url dan events (1 sampai 20 nama event, atau *). Secret ada di respons, sekali. |
GET /v1/webhooks | Daftar endpoint. |
GET /v1/webhooks/{id} | Detail beserta delivery terbaru. |
PATCH /v1/webhooks/{id} | Ubah url, events, atau active. |
DELETE /v1/webhooks/{id} | Hapus endpoint. |
GET /v1/webhooks/{id}/deliveries | Riwayat delivery. |
GET /v1/events | Log event organisasi. |
Best practices
- Acknowledge fast: validate, enqueue, return
200. Do slow work after responding. - Never trust amounts from payloads for security decisions, re-fetch the order over the API.
- Subscribe to
subscription.past_dueto win back failed renewals; QRIS dan VA tidak bisa auto-debit, jadi dunning berjalan lewat invoice hosted. Lihat Subscriptions.