Nyaur Docs

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

EventFired when
checkout.createdA checkout session was opened.
checkout.paidThe buyer completed payment.
checkout.expiredThe session expired unpaid.
order.paidThe order settled; ledger rows and invoice exist.
order.refundedA refund completed (partial or full).
subscription.createdA subscription started.
subscription.renewedA renewal invoice was paid.
subscription.canceledThe subscription was canceled.
subscription.past_dueA renewal went unpaid after reminders (dunning).
benefit.grantedA benefit was granted (e.g. license key issued).
payout.createdA payout was requested.
payout.paidA payout reached the bank.
organization.reviewedProduct 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:

HeaderValue
webhook-idUnique delivery id, stable across retries.
webhook-timestampUnix seconds (string).
webhook-signaturev1,<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 2xx menghentikan retry.
  • webhook-id stabil 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}/replay mengirim ulang sebuah event ke semua endpoint aktif, POST /v1/deliveries/{id}/replay mengirim ulang satu delivery. Dari dashboard: Webhooks, endpoint, Replay.

Mengelola endpoint

EndpointFungsi
POST /v1/webhooksBuat endpoint. Body url dan events (1 sampai 20 nama event, atau *). Secret ada di respons, sekali.
GET /v1/webhooksDaftar 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}/deliveriesRiwayat delivery.
GET /v1/eventsLog 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_due to win back failed renewals; QRIS dan VA tidak bisa auto-debit, jadi dunning berjalan lewat invoice hosted. Lihat Subscriptions.

On this page