> ## Documentation Index
> Fetch the complete documentation index at: https://doctpay.o1center.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Callback

> Terima notifikasi real-time setiap kali status payment berubah.

Terima notifikasi real-time ke URL merchant Anda setiap kali status payment berubah. Atur URL webhook di **Toko Saya → Webhook Callback**.

## Contoh payload

```json theme={null}
{
  "event": "payment.paid",
  "timestamp": "2026-08-17T14:20:33.000Z",
  "data": {
    "orderId": "P_20260817wN9dsiK545H9uxgk2mXj",
    "externalId": "INV-001",
    "storeId": "store_xxxxxxxxxxxxxxxx",
    "storeName": "Toko Demo",
    "merchantName": "Wati Toto",
    "merchantCode": "MERCHANT-AGT-187F2398D5A6",
    "amount": 50000,
    "fee": { "type": "fixed", "value": 1000 },
    "totalAmount": 51000,
    "qrisDynamic": "000201010212...",
    "qrCode": "000201010212...",
    "status": "paid",
    "customerName": "Budi",
    "customerEmail": "budi@example.com",
    "paidAt": "2026-08-17T14:20:33.000Z",
    "expiredAt": "2026-08-17T14:30:00.000Z",
    "providerCode": "your_provider",
    "provider": "Nama Provider Anda",
    "providerOrderId": "BP-123456",
    "channel": "qris",
    "paymentMethod": "QRIS",
    "providerFee": 1500
  }
}
```

## Header

| Header | Deskripsi |
| - | - |
| `X-Webhook-Signature` | HMAC SHA-256 dari raw request body, format `sha256=<hex>` |
| `X-Webhook-Delivery-Id` | ID unik delivery, gunakan sebagai idempotency key |

## Verifikasi signature

```javascript theme={null}
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;
if (!signingSecret) throw new Error("WEBHOOK_SIGNING_SECRET belum diisi");

app.post(
  "/webhook/payment",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body.toString("utf8");
    const providedHex = (req.get("X-Webhook-Signature") || "")
      .replace(/^sha256=/, "");
    const expected = createHmac("sha256", signingSecret)
      .update(rawBody)
      .digest();
    const provided = Buffer.from(providedHex, "hex");

    const signatureValid =
      provided.length === expected.length &&
      timingSafeEqual(provided, expected);
    if (!signatureValid) return res.sendStatus(401);

    const payload = JSON.parse(rawBody);
    const deliveryId = req.get("X-Webhook-Delivery-Id");
    // Simpan deliveryId sebagai idempotency key sebelum memproses payload.data.

    res.sendStatus(204);
  }
);
```

> [!WARNING]
>
> Signature harus dihitung dari raw request body, sebelum payload diproses oleh `express.json()`. Daftarkan route webhook ini lebih dahulu daripada JSON middleware global.

## Retry dan pengiriman

* URL merchant wajib merespons status HTTP 2xx dalam 10 detik.
* Jika gagal (non-2xx atau timeout), delivery diproses ulang hingga maksimal 3 attempt total. Scheduler retry berjalan sekitar setiap 2 menit secara default.
* Log seluruh attempt tersedia di halaman **Callback Logs**.
* Handler harus idempotent karena delivery yang sama dapat dikirim ulang. Gunakan `X-Webhook-Delivery-Id` sebagai idempotency key.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.