Desenvolvedores/Integração
Webhooks
Um webhook é uma mensagem que a CardV envia ao seu servidor quando um pedido termina.
Com ele, você não precisa ficar consultando o pedido. O webhook nunca traz códigos: depois de recebê-lo, consulte o pedido com
GET/api/v1/orders/{order_id}.
Você cadastra as URLs de webhook no Portal (Owner, Integrations → Webhooks), separadamente no Live e no Sandbox. Não existe API para isso.
Veja também: Catálogo e pedidos · Segurança
#Eventos
| Evento | Enviado quando o pedido passa para |
|---|---|
order.succeeded | succeeded |
order.partially_succeeded | partially_succeeded |
order.failed | failed (isso ainda não é um reembolso) |
order.refunded | refunded (o dinheiro voltou para a sua carteira) |
- Cada evento é enviado no máximo uma vez por pedido e por URL de webhook (além das novas tentativas).
- Não há eventos para
acceptednemprocessing, nem para adição de saldo.
#Conteúdo da mensagem
A CardV envia um POST para a sua URL HTTPS:
POST /cardv/webhook HTTP/1.1
Content-Type: application/json
User-Agent: CardV-B2B-Webhook/1.0
X-CardV-Event: order.succeeded
X-CardV-Delivery: 5521
X-CardV-Timestamp: 1790000100
X-CardV-Signature: t=1790000100,v1=2baac51da272ee8d4b7b1382d48037976c7272f1749c9399a50e758604de8a27O corpo, formatado aqui para facilitar a leitura (a CardV envia tudo em uma linha):
{
"event": "order.succeeded",
"order": {
"id": "O-00000001",
"order_id": "O-00000001",
"external_order_id": "TEST-0001",
"status": "succeeded",
"currency": "USD",
"total_amount": "9.2500",
"items": [
{
"sku_id": "S000001",
"product_name": "Example Card",
"quantity": 1,
"delivery_count": 1
}
]
}
}order.order_idé o número do pedido na CardV.order.idtem o mesmo valor.order.external_order_idé o seu número de pedido.order.statusé o status no momento do evento. Ele pode estar desatualizado.X-CardV-Deliveryé o ID desta mensagem. O Portal mostra esse ID no histórico de envios.
#Verificação da assinatura
Todo webhook é assinado com o segredo de assinatura do webhook (whsec_...).
O Portal mostra o segredo uma única vez, quando você cria o webhook ou gera um segredo novo.
Como a assinatura funciona:
X-CardV-Signature: t=<segundos Unix>,v1=<hexadecimal minúsculo>
v1 = HMAC-SHA256(chave = segredo de assinatura, mensagem = "<t>" + "." + bytes brutos do corpo)Passos:
- Leia os bytes brutos do corpo, antes de interpretar o JSON.
- Pegue
tev1do cabeçalho. - Recuse a mensagem se
testiver a mais de 300 segundos do seu relógio. - Calcule o
v1esperado e compare usando uma comparação de tempo constante. - Só então interprete o JSON.
Só t e o corpo são assinados. Pegue o tipo de evento do campo event do corpo,
e não do cabeçalho X-CardV-Event.
Vetor de teste (segredo falso):
segredo whsec_TEST_ONLY_not_a_real_secret_000000000000
t 1790000100
v1 2baac51da272ee8d4b7b1382d48037976c7272f1749c9399a50e758604de8a27O corpo deste vetor é exatamente esta linha (266 bytes, sem quebra de linha no final):
{"event":"order.succeeded","order":{"id":"O-00000001","order_id":"O-00000001","external_order_id":"TEST-0001","status":"succeeded","currency":"USD","total_amount":"9.2500","items":[{"sku_id":"S000001","product_name":"Example Card","quantity":1,"delivery_count":1}]}}Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["CARDV_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, header: str, tolerance: int = 300) -> bool:
try:
parts = dict(item.split("=", 1) for item in header.split(","))
t, v1 = parts["t"].strip(), parts["v1"].strip()
except (KeyError, ValueError):
return False
if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
signed = t.encode() + b"." + raw_body
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), v1.encode()) # constant time
@app.post("/cardv/webhook")
def cardv_webhook():
raw = request.get_data() # raw bytes, before JSON parsing
if not verify(raw, request.headers.get("X-CardV-Signature", "")):
abort(400)
event = request.get_json()
store_event(request.headers["X-CardV-Delivery"], event) # your code
return "", 204 # answer fast; do the work laterNode.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.CARDV_WEBHOOK_SECRET;
function verify(rawBody, header, toleranceSeconds = 300) {
const parts = {};
for (const p of String(header || "").split(",")) {
const i = p.indexOf("=");
parts[p.slice(0, i).trim()] = p.slice(i + 1).trim();
}
const { t, v1 } = parts;
if (!t || !v1 || !/^\d+$/.test(t)) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(t)) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", SECRET)
.update(Buffer.concat([Buffer.from(`${t}.`, "utf8"), rawBody]))
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(v1, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// express.raw keeps the exact bytes. Do not use express.json() on this route.
const rawJson = express.raw({ type: "application/json" });
app.post("/cardv/webhook", rawJson, async (req, res) => {
if (!verify(req.body, req.get("X-CardV-Signature"))) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
await storeEvent(req.get("X-CardV-Delivery"), event); // your code
res.sendStatus(204);
});Os dois exemplos passam no vetor de teste.
#Novas tentativas
- Responda com qualquer status 2xx em até 15 segundos, depois de salvar o evento. Deixe o trabalho demorado (consultar o pedido, enviar os códigos) para depois.
- Qualquer outra coisa conta como falha: outro status, timeout ou erro de conexão.
- A CardV não segue redirecionamentos. Uma resposta 3xx é falha. Cadastre a URL final exata.
- Uma mensagem com falha é reenviada, em até 6 tentativas no total:
| Tentativa | Quando |
|---|---|
| 1 | Logo após o evento |
| 2 | Cerca de 1 minuto após a falha da tentativa 1 |
| 3 | Cerca de 5 minutos após a falha da tentativa 2 |
| 4 | Cerca de 15 minutos após a falha da tentativa 3 |
| 5 | Cerca de 30 minutos após a falha da tentativa 4 |
| 6 | Cerca de 60 minutos após a falha da tentativa 5 |
Os horários podem atrasar até um minuto. Depois da 6ª falha, a CardV para de tentar. No histórico de envios do Portal, você vê todas as mensagens e pode reenviar qualquer uma.
#Duplicados
O mesmo evento pode chegar mais de uma vez, e os eventos podem chegar fora de ordem.
- Ignore eventos que você já tratou.
Compare pelo
order.order_idjunto com oeventdo corpo verificado. Uma mensagem reenviada pelo Portal ganha um novo ID emX-CardV-Delivery, então esse ID sozinho não basta. - O corpo é uma foto do momento do evento. Consulte sempre o pedido de novo e aja de acordo com o status atual.
- Webhooks podem se perder. Tenha também uma rotina que verifica pedidos não terminados com mais de alguns minutos.
#Regras da URL
- A URL precisa começar com
https://e apontar para um endereço público na internet. Endereços privados e locais são recusados. - Você pode assinar os quatro eventos ou escolher só alguns.
- Gerar um novo segredo de assinatura vale na hora. O segredo antigo para de funcionar. Para trocar sem parar o serviço, crie um segundo webhook com o segredo novo, coloque-o em produção e depois desative o antigo.
- A CardV guarda os primeiros 1.000 caracteres da sua resposta para investigar problemas. Não coloque segredos nem dados pessoais na resposta.
Dúvidas sobre a integração? Envie um e-mail para [email protected] com seu Merchant ID e o ID do pedido ou da requisição.