Webhook'lar
Webhook'ları yönet
Bir sipariş veya yenileme değiştiğinde çağrı başına bir imzalı olay alan herkese açık bir https adresi kaydeder. Uç noktalar hesaba aittir ve her birinin kendi gizli anahtarı vardır.
https://notpanel.com/api/v3action=webhook.add- API anahtarı gerekli
- İstek limiti aşıldı
- Gövde:
application/x-www-form-urlencoded
Parametreler
Her zaman gönderilir
keystringZorunlu- API anahtarınız.
actionstringZorunluwebhook.addBirebir "webhook.add" dizesi olmalıdır.
İşlem parametreleri
urlstring (URL)Zorunlu- Olayları alan herkese açık https adresi.
eventsstring (CSV)İsteğe bağlı- Abone olunacak, virgülle ayrılmış olay türleri, 8 arasından. Hepsini almak için boş bırakın.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));
YOUR_API_KEY yerine API sayfanızdaki anahtarı yazın.
{
"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"
}Hatalar
- 400
{"error":"Invalid webhook URL","error_code":"INVALID_WEBHOOK_URL","retryable":false}Adres herkese açık bir https adresi değil veya özel bir adresi gösteriyor.
- 400
{"error":"Invalid events: not_allowed","error_code":"INVALID_PARAMETER","retryable":false,"details":{"param":"events","rule":"not_allowed"}}Bir olay adı listede yok. Yazımı kontrol edin: adlar
order.completedgibidir. - 409
{"error":"Maximum 5 webhook endpoints per account","error_code":"WEBHOOK_LIMIT","retryable":false,"details":{"limit":5}}Hesap zaten en fazla 5 uç noktaya sahip. Önce birini kaldırın.
Olaylar
Bunların herhangi birine abone olun. Her biri değişiklik kaydedildiğinde kendi çağrısı olarak gönderilir; geri alınan bir değişiklik hiçbir şey göndermez. Ping yalnızca webhook.test tarafından gönderilir.
Olay türleri
order.processing- Sipariş gönderildi ve teslimatın başlaması bekleniyor.
order.in_progress- Teslimat başladı.
order.completed- Sipariş tamamen teslim edildi.
order.partial- Siparişin bir kısmı teslim edildi, kalanı iade edildi.
order.canceled- Sipariş iptal edildi ve iade edildi. Önceki
order.refundedolayının yerini alır. order.cancel_refused- Bir iptal talebi reddedildi. Sipariş çalışmaya devam eder.
refill.completed- Bir yenileme yapıldı. Önceki
order.refill_completedolayının yerini alır. refill.rejected- Bir yenileme reddedildi. Veri nedeni taşır.
ping- Yalnızca
webhook.testtarafından gönderilen bir test olayı.
Uç noktaları listeleme
action=webhook.list, hesabın her uç noktasını olaylarıyla, etkin olup olmadığıyla ve duraklatıldıysa ne zaman duraklatıldığıyla birlikte döndürür. Gizli anahtar asla döndürülmez. Eski status, failureCount, lastDeliveredAt ve createdAt anahtarları da yer alır.
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"
}
]Test olayı gönderme
action=webhook.test, uç noktaya bir ping kuyruğa alır. Alıcınızı ve imza kodunuzu denetlemek için kullanın. Yanıt teslimat kimliğini içerir.
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"
}Uç nokta kaldırma
action=webhook.remove uç noktayı ve gizli anahtarını siler. Yolda olan bir teslimat yine de ulaşabilir; onun için yeni bir şey kuyruğa alınmaz.
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
}Teslimat yükü
Her teslimat, Content-Type: application/json ile tek bir POST içinde bir olaydır. Gövde bir zarftır ve data, siparişi veya yenilemeyi status ve refill_status ile aynı biçimde taşır. Bu, şema sürümü 1'dir.
Zarf
idstring- Olayın benzersiz kimliği. Yinelenenleri yok saymak için kullanın.
typestring- Olay türü.
schema_versioninteger- Bu biçimin sürümü. Şu an 1.
created_atstring- Olayın oluşturulduğu zaman.
sequenceinteger- Hesabın her olayıyla artar; böylece olayları sıraya koyabilirsiniz.
dataobject- İlgili okuma işleminin biçiminde
orderveyarefill. Ping için boş.
İstek başlıkları
X-Webhook-Signature- Zaman damgası, bir nokta ve tam gövdenin HMAC-SHA256 değeri;
sha256=ve ardından onaltılık rakamlar olarak yazılır. X-Webhook-Timestamp- Bu denemenin saniye cinsinden Unix zamanı. Yeniden deneme yeni bir zaman damgası ve imza alır.
X-Webhook-Event- Olay türü, gövdedeki
typeile aynı. X-Webhook-Delivery-Id- Bu teslimatın kimliği, her yeniden denemesinde aynıdır.
{
"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"
}
}
}İmzaları doğrulama
X-Webhook-Timestamp değerini ve gövdenin tam baytlarını okuyun. Uç noktanın gizli anahtarıyla <timestamp>.<body bytes> üzerinde HMAC-SHA256 hesaplayın, sha256=<hex> olarak yazın ve X-Webhook-Signature ile sabit sürede karşılaştırın. Yeniden denemenin olay kimliği aynıdır ama zaman damgası ve imzası yenidir.
Ham gövdeyi ve başlıkları okuyun
İmzayı hesaplayın ve karşılaştırın
Zaten işlediğiniz olayları atlayın
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);
}Çalışılmış bir örnek
Kodunuzu test etmek için kullanın. İmza, zaman damgasını, bir noktayı ve gövdeyi tam olarak gösterildiği gibi, fazladan boşluk veya satır sonu olmadan kapsar.
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=6e8e90578f566d2e14b5c3914eba7e378c4107a5a4d4ddd52c747791f2264fffTeslimat ve yeniden denemeler
En az bir kez
10 saniye içindeki her 2xx yanıtı teslim edilmiş sayılır. Yönlendirmeler izlenmez ve yanıtınızın yalnızca ilk 4096 baytı okunur. Aynı olay birden fazla kez gelebilir; bu yüzden zaten işlediğiniz bir olay kimliğini yok sayın.
Bir gün içinde sekiz deneme
Toplam 8 deneme: ilki hemen, sonra olaydan 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h sonra. Her deneme aynı olay kimliğini ve teslimat kimliğini korur.
Uç noktalar kendiliğinden duraklar
Art arda 50 başarısızlıktan sonra veya 3 gün başarısız kalınca uç nokta duraklatılır ve hesap sahibine haber verilir. Alıcınızı düzeltin ve uç noktayı yeniden ekleyin.
Sıra garanti edilmez
Olaylar sırasız gelebilir. Onları sıraya koymak için
sequencekullanın, her olay kimliğini bir kez işleyin ve güncel duruma ihtiyaç duyduğunuzdaaction=statusokuyun.