Webhook
Kelola webhook
Mendaftarkan alamat https publik yang menerima satu event bertanda tangan per panggilan saat order atau isi ulang berubah. Endpoint milik akun, dan masing-masing memiliki secret sendiri.
https://notpanel.com/api/v3action=webhook.add- API key diperlukan
- Batas permintaan tercapai
- Body:
application/x-www-form-urlencoded
Parameter
Selalu dikirim
keystringWajib- API key Anda.
actionstringWajibwebhook.addHarus berupa string literal "webhook.add".
Parameter action
urlstring (URL)Wajib- Alamat https publik yang menerima event.
eventsstring (CSV)Opsional- Jenis event yang dilanggani, dipisahkan koma, dari 8. Kosongkan untuk menerima semuanya.order.processing, order.in_progress, order.completed, order.partial, order.canceled, order.cancel_refused, refill.completed, refill.rejected
curl -X POST https://notpanel.com/api/v3 \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "key=YOUR_API_KEY&action=webhook.add&url=https://your-server.example.com/notpanel-webhook&events=order.completed,order.partial,order.canceled"const res = await fetch("https://notpanel.com/api/v3", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
key: "YOUR_API_KEY",
action: "webhook.add",
url: "https://your-server.example.com/notpanel-webhook",
events: "order.completed,order.partial,order.canceled",
}),
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
"https://notpanel.com/api/v3",
data={
"key": "YOUR_API_KEY",
"action": "webhook.add",
"url": "https://your-server.example.com/notpanel-webhook",
"events": "order.completed,order.partial,order.canceled",
},
)
print(res.json())<?php
$body = http_build_query([
'key' => 'YOUR_API_KEY',
'action' => 'webhook.add',
'url' => 'https://your-server.example.com/notpanel-webhook',
'events' => 'order.completed,order.partial,order.canceled',
]);
$response = file_get_contents('https://notpanel.com/api/v3', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
'content' => $body,
'ignore_errors' => true,
],
]));
print_r(json_decode($response, true));
Ganti YOUR_API_KEY dengan kunci dari halaman API Anda.
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"url": "https://your-server.example.com/notpanel-webhook",
"events": ["order.completed", "order.partial", "order.canceled"],
"active": true,
"paused_at": null,
"created_at": "2026-10-03T09:00:00Z",
"status": "active",
"failureCount": 0,
"lastDeliveredAt": null,
"createdAt": "2026-10-03T09:00:00.000Z",
"secret": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"webhook_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
}Error
- 400
{"error":"Invalid webhook URL","error_code":"INVALID_WEBHOOK_URL","retryable":false}Alamat bukan alamat https publik, atau menunjuk ke alamat privat.
- 400
{"error":"Invalid events: not_allowed","error_code":"INVALID_PARAMETER","retryable":false,"details":{"param":"events","rule":"not_allowed"}}Nama event tidak ada dalam daftar. Periksa ejaannya: namanya seperti
order.completed. - 409
{"error":"Maximum 5 webhook endpoints per account","error_code":"WEBHOOK_LIMIT","retryable":false,"details":{"limit":5}}Akun sudah memiliki maksimum 5 endpoint. Hapus satu terlebih dahulu.
Event
Berlangganan salah satunya. Masing-masing dikirim sebagai panggilan tersendiri saat perubahan disimpan, dan perubahan yang dibatalkan tidak mengirim apa pun. Ping hanya dikirim oleh webhook.test.
Jenis event
order.processing- Order dikirim dan menunggu pengiriman dimulai.
order.in_progress- Pengiriman telah dimulai.
order.completed- Order terkirim sepenuhnya.
order.partial- Sebagian order terkirim dan sisanya di-refund.
order.canceled- Order dibatalkan dan di-refund. Menggantikan
order.refundedyang lama. order.cancel_refused- Permintaan pembatalan ditolak. Order terus berjalan.
refill.completed- Isi ulang dilakukan. Menggantikan
order.refill_completedyang lama. refill.rejected- Isi ulang ditolak. Datanya membawa alasannya.
ping- Event uji, hanya dikirim oleh
webhook.test.
Daftar endpoint
action=webhook.list mengembalikan setiap endpoint akun beserta event-nya, apakah aktif, dan kapan dijeda, bila dijeda. Secret tidak pernah dikembalikan. Kunci lama status, failureCount, lastDeliveredAt, dan createdAt juga disertakan.
https://notpanel.com/api/v3action=webhook.listkey=YOUR_API_KEY&action=webhook.list[
{
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"url": "https://your-server.example.com/notpanel-webhook",
"events": ["order.completed", "order.partial", "order.canceled"],
"active": true,
"paused_at": null,
"created_at": "2026-10-03T09:00:00Z",
"status": "active",
"failureCount": 0,
"lastDeliveredAt": null,
"createdAt": "2026-10-03T09:00:00.000Z"
}
]Kirim event uji
action=webhook.test mengantrekan ping ke endpoint. Gunakan untuk memeriksa penerima dan kode tanda tangan Anda. Balasan berisi ID pengiriman.
https://notpanel.com/api/v3action=webhook.testkey=YOUR_API_KEY&action=webhook.test&id=3f2504e0-4f89-41d3-9a0c-0305e82c3301{
"queued": true,
"delivery_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}Hapus endpoint
action=webhook.remove menghapus endpoint beserta secret-nya. Pengiriman yang sudah dalam perjalanan masih bisa tiba; tidak ada yang baru diantrekan untuknya.
https://notpanel.com/api/v3action=webhook.removekey=YOUR_API_KEY&action=webhook.remove&id=3f2504e0-4f89-41d3-9a0c-0305e82c3301{
"removed": true,
"id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"success": true
}Payload pengiriman
Setiap pengiriman adalah satu event dalam satu POST dengan Content-Type: application/json. Body-nya adalah amplop, dan data berisi order atau isi ulang dalam format yang sama dengan status dan refill_status. Ini adalah skema versi 1.
Amplop
idstring- ID unik event. Gunakan untuk mengabaikan duplikat.
typestring- Jenis event.
schema_versioninteger- Versi format ini. Saat ini 1.
created_atstring- Kapan event dibuat.
sequenceinteger- Naik setiap ada event akun, sehingga Anda dapat mengurutkan event.
dataobjectorderataurefill, dalam format aksi baca yang sesuai. Kosong untuk ping.
Header permintaan
X-Webhook-Signature- HMAC-SHA256 dari timestamp, sebuah titik, dan body yang persis sama, ditulis sebagai
sha256=diikuti digit heksadesimal. X-Webhook-Timestamp- Waktu Unix dalam detik untuk percobaan ini. Percobaan ulang mendapat timestamp dan tanda tangan baru.
X-Webhook-Event- Jenis event, sama dengan
typedi body. X-Webhook-Delivery-Id- ID pengiriman ini, sama pada setiap percobaan ulangnya.
{
"id": "e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e51",
"type": "order.completed",
"schema_version": 1,
"created_at": "2026-10-03T12:30:00Z",
"sequence": 1042,
"data": {
"order": {
"charge": "1.20",
"start_count": "3572",
"status": "Completed",
"remains": "0",
"currency": "USD",
"order": 1234567,
"status_key": "completed",
"refund": "0.00",
"reason_code": null,
"reason": null,
"can_cancel": false,
"cancel_available": false,
"cancel_state": null,
"can_refill": false,
"refill_available_at": "2026-10-03T18:35:00Z",
"updated_at": "2026-10-03T12:35:00Z"
}
}
}{
"id": "e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e52",
"type": "order.partial",
"schema_version": 1,
"created_at": "2026-10-03T12:30:00Z",
"sequence": 1043,
"data": {
"order": {
"charge": "0.87",
"start_count": "3572",
"status": "Partial",
"remains": "157",
"currency": "USD",
"order": 1234568,
"status_key": "partial",
"refund": "0.13",
"reason_code": "partially_delivered",
"reason": "Part of the order was delivered. The rest was refunded.",
"can_cancel": false,
"cancel_available": false,
"cancel_state": null,
"can_refill": true,
"refill_available_at": null,
"updated_at": "2026-10-03T12:00:00Z"
}
}
}{
"id": "e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e53",
"type": "order.canceled",
"schema_version": 1,
"created_at": "2026-10-03T12:30:00Z",
"sequence": 1044,
"data": {
"order": {
"charge": "0.00",
"start_count": "0",
"status": "Canceled",
"remains": "1000",
"currency": "USD",
"order": 1234569,
"status_key": "cancelled",
"refund": "1.20",
"reason_code": "customer_canceled",
"reason": "You canceled this order. The charge was refunded.",
"can_cancel": false,
"cancel_available": false,
"cancel_state": null,
"can_refill": false,
"refill_available_at": null,
"updated_at": "2026-10-03T10:00:09Z"
}
}
}{
"id": "e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e56",
"type": "refill.rejected",
"schema_version": 1,
"created_at": "2026-10-03T12:30:00Z",
"sequence": 1047,
"data": {
"refill": {
"refill": 5502,
"status": "Rejected",
"status_key": "rejected",
"order": 1234568,
"reason_code": "refill_too_early",
"reason": "This order cannot be refilled yet. Try again later.",
"created_at": "2026-10-03T12:40:00Z",
"updated_at": "2026-10-03T12:41:00Z"
}
}
}Memverifikasi tanda tangan
Baca X-Webhook-Timestamp dan byte persis dari body. Hitung HMAC-SHA256 atas <timestamp>.<body bytes> dengan secret endpoint, tulis sebagai sha256=<hex>, dan bandingkan dengan X-Webhook-Signature dalam waktu konstan. Percobaan ulang memiliki ID event yang sama tetapi timestamp dan tanda tangan baru.
Baca body mentah dan header
Hitung dan bandingkan tanda tangan
Lewati event yang sudah Anda tangani
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* @param {{rawBody: unknown, signatureHeader: unknown, timestampHeader: unknown,
* secret: unknown, nowSeconds?: number, toleranceSeconds?: number}} input
*/
function verifyNotPanelBody({
rawBody,
signatureHeader,
timestampHeader,
secret,
nowSeconds = Math.floor(Date.now() / 1000),
toleranceSeconds = 300, // Example receiver policy, not a NotPanel rule.
}) {
if (!Buffer.isBuffer(rawBody)) return false;
if (typeof secret !== "string" || secret.length === 0) return false;
if (typeof signatureHeader !== "string") return false;
if (typeof timestampHeader !== "string") return false;
if (!/^(0|[1-9]\d{0,15})$/.test(timestampHeader)) return false;
if (!Number.isSafeInteger(nowSeconds)) return false;
if (!Number.isSafeInteger(toleranceSeconds) || toleranceSeconds < 0) {
return false;
}
const timestamp = Number(timestampHeader);
if (!Number.isSafeInteger(timestamp)) return false;
if (Math.abs(nowSeconds - timestamp) > toleranceSeconds) return false;
const match = /^sha256=([0-9a-f]{64})$/i.exec(signatureHeader);
if (!match?.[1]) return false;
const expected = createHmac("sha256", secret)
.update(timestampHeader, "utf8")
.update(".", "utf8")
.update(rawBody)
.digest();
const received = Buffer.from(match[1], "hex");
return timingSafeEqual(expected, received);
}Contoh yang dikerjakan
Gunakan ini untuk menguji kode Anda. Tanda tangan mencakup timestamp, sebuah titik, dan body persis seperti yang ditampilkan, tanpa spasi atau baris baru tambahan.
secret = 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
timestamp = 1790000000
body = {"id":"e1f0c9a2-3a8d-4a0e-9d8f-6c1b2a3d4e51","type":"order.completed","schema_version":1,"created_at":"2026-10-03T12:30:00Z","sequence":1042,"data":{"order":{"charge":"1.20","start_count":"3572","status":"Completed","remains":"0","currency":"USD","order":1234567,"status_key":"completed","refund":"0.00","reason_code":null,"reason":null,"can_cancel":false,"cancel_available":false,"cancel_state":null,"can_refill":false,"refill_available_at":"2026-10-03T18:35:00Z","updated_at":"2026-10-03T12:35:00Z"}}}X-Webhook-Signature: sha256=6e8e90578f566d2e14b5c3914eba7e378c4107a5a4d4ddd52c747791f2264fffPengiriman dan percobaan ulang
Minimal satu kali
Balasan 2xx apa pun dalam 10 detik dianggap terkirim. Pengalihan tidak diikuti, dan hanya 4096 byte pertama dari balasan Anda yang dibaca. Event yang sama bisa tiba lebih dari sekali, jadi abaikan ID event yang sudah Anda tangani.
Delapan percobaan selama sehari
8 percobaan secara total: yang pertama segera, lalu 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h setelah event. Setiap percobaan mempertahankan ID event dan ID pengiriman yang sama.
Endpoint menjeda dirinya sendiri
Setelah 50 kegagalan berturut-turut, atau 3 hari tanpa keberhasilan, endpoint dijeda dan pemilik akun diberi tahu. Perbaiki penerima Anda dan tambahkan endpoint lagi.
Urutan tidak dijamin
Event bisa tiba tidak berurutan. Gunakan
sequenceuntuk mengurutkannya, tangani setiap ID event sekali, dan bacaaction=statusbila Anda butuh status terkini.