OneProvider

Webhooks da API de revenda

Eventos de webhook, payloads, verificação de assinatura HMAC em Python e Node, novas tentativas com backoff, URLs seguras e deduplicação por id de evento.

Eventos de webhook, payloads, verificação da assinatura HMAC em Python e Node, novas tentativas com backoff, regras de segurança da URL e deduplicação pelo id do evento.

Todos os modelos, recursos e preços Índice da documentação para agentes de IA: https://oneprovider.dev/docs/llms.txt

Criar um webhook

POST /webhooks (permissão webhooks): uma URL somente https e uma lista não vazia de eventos. No máximo 5 webhooks. Um Idempotency-Key opcional: uma repetição retorna o mesmo webhook, sem o segredo. last_delivered_at é o horário da última resposta 2xx do seu servidor (null enquanto não houve nenhuma).

bash
curl -s -X POST https://app.oneprovider.dev/partner/v1/webhooks \
-H "Authorization: Bearer $OP_PARTNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://shop.example.com/op-webhook","events":["order.completed","balance.low"]}'
json
{
"id": "<webhook id>",
"url": "https://shop.example.com/op-webhook",
"events": ["order.completed", "balance.low"],
"active": true,
"secret_last4": "a1b2",
"created_at": "2026-10-08T12:00:00Z",
"last_delivered_at": null,
"secret": "whsec_..."
}
O segredo de assinatura é exibido uma única vez O campo secret vem apenas na resposta de criação. Guarde-o no seu servidor; um segredo perdido não pode ser recuperado, crie o webhook de novo.

Eventos

Eventos de webhook
EventoQuandodata
key.createdUma chave foi emitida (pela API, pelo painel, por um pedido ou por um código).key_id, name, limit_usd, customer_id, source (api, cabinet, order, code)
key.pausedUma chave foi pausada (à mão ou quando o cliente dela foi bloqueado).key_id, reason (somente em um bloqueio: customer_blocked)
key.resumedUma chave foi retomada (à mão ou quando o cliente dela foi desbloqueado).key_id, reason (somente em um desbloqueio: customer_unblocked)
code.redeemedUm código foi resgatado.batch_id, code_tail, key_id, amount_usd
order.completedUm pedido foi concluído.order_id, external_order_id, kind, key_id, amount_usd
balance.topped_upO saldo de revendedor foi recarregado.amount_usd, order_id
balance.lowO saldo caiu abaixo do limiar (definido pela OneProvider, $10 por padrão, igual para todos). Chega uma vez e só chega de novo depois que o saldo voltar a ficar acima do limiar.balance_usd, threshold_usd
key.exhaustedO limite de uma chave se esgotou. Uma vez por chave e por limite vendido, em poucos minutos; uma recarga da chave reativa o evento.key_id, limit_usd

Os dados do evento nunca contêm segredos: nem chaves, nem tokens, nem códigos. O teste (POST /webhooks/{id}/test) envia o evento de serviço webhook.test com data {"message": "test"}; não é possível se inscrever nele.

Requisição e assinatura

Enviamos um POST com corpo JSON:

json
{
"id": "evt_...",
"type": "order.completed",
"created_at": "2026-10-08T12:00:00Z",
"data": {
"order_id": 42,
"external_order_id": "shop-1042",
"kind": "new_key",
"key_id": "<key id>",
"amount_usd": "10.00"
}
}

O corpo é enviado como JSON compacto, com as chaves em ordem alfabética; verifique a assinatura sobre os bytes exatamente como chegaram, não sobre um JSON reconstruído.

Cabeçalhos da entrega
CabeçalhoValor
X-OP-Signaturet=<unix>,v1=<hex>
X-OP-EventO tipo do evento.
X-OP-DeliveryIgual ao id do evento.
User-AgentOneProvider-Webhooks/1

v1 é o hexadecimal de um HMAC-SHA256 cuja chave é o segredo do webhook (whsec_...) e cuja mensagem é a string "<t>" + "." + raw_body.

Verificar a assinatura

  1. Leia o corpo bruto da requisição: a assinatura cobre os bytes exatos, um JSON serializado de novo não vai coincidir.
  2. Recalcule o HMAC e compare-o com o v1 em tempo constante (hmac.compare_digest, timingSafeEqual).
  3. Rejeite a requisição se |agora - t| for maior que 5 minutos, para que uma assinatura antiga não possa ser reutilizada.
  4. Responda 2xx rápido (o tempo limite é de 10 segundos) e faça o trabalho pesado de forma assíncrona.

Python

python
import hashlib
import hmac
import time
TOLERANCE = 300 # seconds
def verify(secret: str, header: str, raw_body: bytes) -> bool:
"""header is X-OP-Signature: t=<unix>,v1=<hex>; raw_body is the unparsed request body."""
parts = dict(p.strip().partition("=")[::2] for p in header.split(","))
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or abs(time.time() - int(t)) > TOLERANCE:
return False
signed = t.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
python
import json
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
SECRET = os.environ["OP_WEBHOOK_SECRET"] # whsec_...
seen: set[str] = set() # use a database table with a unique index in production
@app.post("/op-webhook")
async def op_webhook(request: Request):
raw = await request.body() # the raw bytes, before any JSON parsing
if not verify(SECRET, request.headers.get("X-OP-Signature", ""), raw):
raise HTTPException(status_code=400, detail="bad signature")
event = json.loads(raw)
if event["id"] in seen: # retries resend the same event id
return {"ok": True}
seen.add(event["id"])
if event["type"] == "order.completed":
... # your logic
return {"ok": True} # any 2xx marks the event as delivered

Node.js

javascript
import crypto from "node:crypto"
import express from "express"
const SECRET = process.env.OP_WEBHOOK_SECRET // whsec_...
const TOLERANCE = 300 // seconds
const seen = new Set() // use a database table with a unique index in production
function verify(secret, header, raw) {
const parts = Object.fromEntries(
String(header).split(",").map((p) => p.trim().split("=")),
)
const t = Number(parts.t)
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > TOLERANCE) return false
const expected = crypto
.createHmac("sha256", secret)
.update(parts.t + ".")
.update(raw)
.digest("hex")
const a = Buffer.from(expected)
const b = Buffer.from(parts.v1 ?? "")
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
const app = express()
// express.raw keeps the body as a Buffer: do not use express.json() on this route
app.post("/op-webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(SECRET, req.get("X-OP-Signature"), req.body)) return res.sendStatus(400)
const event = JSON.parse(req.body.toString("utf8"))
if (!seen.has(event.id)) {
seen.add(event.id) // retries resend the same event id
if (event.type === "order.completed") {
// your logic
}
}
res.sendStatus(200) // any 2xx marks the event as delivered
})
app.listen(3000)

Novas tentativas

Só uma resposta 2xx conta como entregue. Todo o resto (inclusive 3xx, já que os redirecionamentos não são seguidos, além de tempos esgotados e erros de conexão) é reenviado com backoff exponencial: 1 minuto, 2, 4 e assim por diante, com no máximo 4 horas entre as tentativas, até completar 24 horas depois do evento; então a entrega passa a failed. Cada nova tentativa traz o mesmo id de evento, então deduplique por ele: guarde os ids já processados em uma tabela com índice único.

Regras da URL

  • Somente https, porta 443 (qualquer outra porta dá 400 invalid_url) e um nome de host; IPs literais são rejeitados. A URL tem no máximo 500 caracteres ASCII, sem nome de usuário, senha nem fragmento.
  • Todo endereço para o qual o nome resolve não pode ser privado, de loopback, de link-local nem reservado.
  • A verificação roda na criação e em cada entrega. Uma violação na criação dá 400 invalid_url.

Teste e registro

bash
curl -s -X POST https://app.oneprovider.dev/partner/v1/webhooks/<webhook id>/test -H "Authorization: Bearer $OP_PARTNER_TOKEN"
curl -s "https://app.oneprovider.dev/partner/v1/webhooks/<webhook id>/deliveries?limit=20" -H "Authorization: Bearer $OP_PARTNER_TOKEN"

Objeto da entrega: id, event_id, event, status (pending, delivered, failed), attempts, next_attempt_at, last_error (http_<status>, timeout, blocked_address ou o nome do erro de conexão), last_status (o status HTTP que o seu servidor respondeu, null quando ele não respondeu), last_body (o início da resposta, até 300 caracteres, em uma linha), created_at. Excluir um webhook: DELETE /webhooks/{id}. Os testes e as novas tentativas manuais (no painel) somam no máximo 6 por minuto; só um evento não entregue pode ser reenviado, um já entregue dá 409 already_delivered. A nova tentativa manual existe apenas no painel, não com um token.

Última atualização: 2026-11-08 Encontrou um erro? @Oneproviderhelpbot