Webhooks
Gerenciar webhooks
Registra um endereço https público que recebe um evento assinado por chamada quando um pedido ou uma reposição muda. Os endpoints pertencem à conta, e cada um tem seu próprio segredo.
https://notpanel.com/api/v3action=webhook.add- Chave de API obrigatória
- Limite de requisições atingido
- Corpo:
application/x-www-form-urlencoded
Parâmetros
Sempre enviado
keystringObrigatório- Sua chave de API.
actionstringObrigatóriowebhook.addDeve ser a string literal "webhook.add".
Parâmetros da ação
urlstring (URL)Obrigatório- Endereço https público que recebe os eventos.
eventsstring (CSV)Opcional- Tipos de evento separados por vírgula para assinar, entre 8. Omita para receber todos.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));
Substitua YOUR_API_KEY pela chave da sua página de API.
{
"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"
}Erros
- 400
{"error":"Invalid webhook URL","error_code":"INVALID_WEBHOOK_URL","retryable":false}O endereço não é um endereço https público, ou aponta para um privado.
- 400
{"error":"Invalid events: not_allowed","error_code":"INVALID_PARAMETER","retryable":false,"details":{"param":"events","rule":"not_allowed"}}Um nome de evento não está na lista. Confira a grafia: os nomes são como
order.completed. - 409
{"error":"Maximum 5 webhook endpoints per account","error_code":"WEBHOOK_LIMIT","retryable":false,"details":{"limit":5}}A conta já tem o máximo de 5 endpoints. Remova um primeiro.
Eventos
Assine qualquer um destes. Cada um é enviado como uma chamada própria quando a mudança é salva, e uma mudança desfeita não envia nada. Um ping só é enviado por webhook.test.
Tipos de evento
order.processing- O pedido foi enviado e aguarda o início da entrega.
order.in_progress- A entrega começou.
order.completed- O pedido foi totalmente entregue.
order.partial- Parte do pedido foi entregue e o restante foi reembolsado.
order.canceled- O pedido foi cancelado e reembolsado. Substitui o antigo
order.refunded. order.cancel_refused- Uma solicitação de cancelamento foi recusada. O pedido continua em andamento.
refill.completed- Uma reposição foi feita. Substitui o antigo
order.refill_completed. refill.rejected- Uma reposição foi recusada. Os dados trazem o motivo.
ping- Um evento de teste, enviado somente por
webhook.test.
Listar endpoints
action=webhook.list retorna todos os endpoints da conta com seus eventos, se está ativo e quando foi pausado, se foi. O segredo nunca é retornado. As chaves antigas status, failureCount, lastDeliveredAt e createdAt também vêm incluídas.
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"
}
]Enviar um evento de teste
action=webhook.test coloca um ping na fila para o endpoint. Use-o para conferir seu receptor e seu código de assinatura. A resposta traz o ID da entrega.
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"
}Remover um endpoint
action=webhook.remove exclui o endpoint e seu segredo. Uma entrega que já está a caminho ainda pode chegar; nada novo é colocado na fila para ele.
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
}Conteúdo da entrega
Cada entrega é um evento em um POST com Content-Type: application/json. O corpo é um envelope, e data traz o pedido ou a reposição no mesmo formato de status e refill_status. Esta é a versão de esquema 1.
Envelope
idstring- ID único do evento. Use-o para ignorar duplicatas.
typestring- O tipo do evento.
schema_versioninteger- Versão deste formato. Agora é 1.
created_atstring- Quando o evento foi criado.
sequenceinteger- Aumenta a cada evento da conta, para que você possa ordenar os eventos.
dataobjectorderourefill, no formato da ação de leitura correspondente. Vazio para um ping.
Cabeçalhos da requisição
X-Webhook-Signature- HMAC-SHA256 do timestamp, um ponto e o corpo exato, escrito como
sha256=seguido dos dígitos hexadecimais. X-Webhook-Timestamp- Horário Unix em segundos desta tentativa. Uma nova tentativa recebe um novo timestamp e assinatura.
X-Webhook-Event- O tipo do evento, igual a
typeno corpo. X-Webhook-Delivery-Id- ID desta entrega, o mesmo em cada nova tentativa dela.
{
"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"
}
}
}Verificando assinaturas
Leia X-Webhook-Timestamp e os bytes exatos do corpo. Calcule HMAC-SHA256 sobre <timestamp>.<body bytes> com o segredo do endpoint, escreva como sha256=<hex> e compare com X-Webhook-Signature em tempo constante. Uma nova tentativa tem o mesmo ID de evento, mas novo timestamp e assinatura.
Leia o corpo bruto e os cabeçalhos
Calcule e compare a assinatura
Pule os eventos que você já tratou
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);
}Um exemplo prático
Use isto para testar seu código. A assinatura cobre o timestamp, um ponto e o corpo exatamente como mostrado, sem espaços ou quebras de linha extras.
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=6e8e90578f566d2e14b5c3914eba7e378c4107a5a4d4ddd52c747791f2264fffEntrega e novas tentativas
Pelo menos uma vez
Qualquer resposta 2xx em até 10 segundos conta como entregue. Redirecionamentos não são seguidos, e apenas os primeiros 4096 bytes da sua resposta são lidos. O mesmo evento pode chegar mais de uma vez, então ignore um ID de evento que você já tratou.
Oito tentativas ao longo de um dia
8 tentativas no total: a primeira na hora, depois 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h após o evento. Cada tentativa mantém o mesmo ID de evento e de entrega.
Os endpoints se pausam sozinhos
Após 50 falhas seguidas, ou 3 dias sem sucesso, o endpoint é pausado e o dono da conta é avisado. Corrija seu receptor e adicione o endpoint de novo.
A ordem não é garantida
Os eventos podem chegar fora de ordem. Use
sequencepara ordená-los, trate cada ID de evento uma única vez e leiaaction=statusquando precisar do estado atual.