Tens de entrar no Payflow primeiro.
Entrar no PayflowIntegrações
Como os outros sistemas ficam a saber das vendas: webhooks que o Payflow envia quando um pedido é pago, reembolsado ou disputado, e chaves para a API. O modo de teste nunca mexe em dinheiro a sério.
Uma chave sk_live_ lê e mexe em pedidos a sério; uma sk_test_ só vê o modo de teste.
Guardamos só um resumo dela: se se perder, revoga-se e cria-se outra.
O prod_ de cada oferta não muda nunca, mesmo que o nome ou o preço mudem. É com ele que
a Kingdom Library faz o link de compra e reconhece o que foi comprado.
O envelope
Cada evento é um POST com um corpo JSON. O corpo vai sempre exactamente com os bytes que foram
assinados: verifica a assinatura sobre o corpo cru, antes de o interpretar.
{
"id": "evt_01JAXXXXXXXXXXXXXXXXXXXXXX",
"type": "order.paid",
"api_version": "2026-09-01",
"created_at": "2026-09-24T12:00:00Z",
"livemode": true,
"data": {
"order": {
"id": "ord_01JAXXXXXXXXXXXXXXXXXXXXXX", "reference": "KG-20260924-7K2QF",
"status": "paid", "amount": 14900, "currency": "ZAR",
"refunded_amount": 0, "full_refund": false,
"paid_at": "2026-09-24T12:00:00Z", "created_at": "2026-09-24T11:58:40Z",
"provider": "paystack", "provider_reference": "KGD-…", "livemode": true
},
"customer": { "id": "cus_01JA…", "email": "cliente@example.invalid", "name": "Cliente Exemplo", "phone": null },
"locale": "en",
"items": [ { "product_id": "prod_01JA…", "sku": null, "name": "Produto de exemplo",
"quantity": 1, "unit_amount": 14900 } ],
"metadata": { "member_user_id": null }
}
}
Os valores estão sempre em subunidades da moeda: 14900 em ZAR são R 149,00.
Os eventos
| Tipo | Quando |
|---|---|
order.paid | O pagamento foi confirmado, por qualquer método (cartão, M-Pesa, transferência confirmada à mão). |
order.refunded | Houve um reembolso, total ou parcial. full_refund diz qual. |
order.disputed | O cliente abriu uma disputa (chargeback) no banco. |
order.dispute_resolved | A disputa fechou: dispute_won ou dispute_lost. |
integration.test | Só quando se carrega em «Enviar evento de teste». Traz data.message. |
Os cabeçalhos
| Cabeçalho | O que é |
|---|---|
X-Kingdom-Signature | t=<unix>,v1=<hex> — HMAC-SHA256 de "<t>.<corpo>"; a chave é o segredo inteiro, whsec_ incluído. |
webhook-id, webhook-timestamp, webhook-signature | Standard Webhooks: v1,<base64> — HMAC-SHA256 de "<id>.<t>.<corpo>"; a chave são os bytes do base64 depois de whsec_. Serve qualquer biblioteca standardwebhooks. |
X-Kingdom-Event-Id, X-Kingdom-Event-Type | O id e o tipo do evento. O id é igual em todas as tentativas: deduplica por ele. |
User-Agent | Começa por KingdomGateway-Webhooks/1.0. O servidor pode acrescentar-lhe texto a seguir: compara o começo, nunca o valor inteiro. |
Depois de rodar o segredo, durante 24 horas cada entrega leva duas assinaturas em cada cabeçalho — a do segredo novo primeiro. Aceita a entrega se uma delas bater certo.
Verificar a assinatura
Node
import crypto from 'node:crypto';
// X-Kingdom-Signature. corpoCru: o texto exacto que chegou, antes de JSON.parse.
function verificarKingdom(cabecalho, corpoCru, segredo, agora = Date.now() / 1000) {
const t = /(?:^|,)t=(\d+)/.exec(cabecalho || '')?.[1];
if (!t || Math.abs(agora - Number(t)) > 300) return false; // 5 minutos de tolerância
const esperado = crypto.createHmac('sha256', segredo).update(`${t}.${corpoCru}`).digest('hex');
return [...(cabecalho || '').matchAll(/v1=([0-9a-f]{64})/g)].some(([, v]) =>
crypto.timingSafeEqual(Buffer.from(v, 'hex'), Buffer.from(esperado, 'hex')));
}
// Standard Webhooks, sem biblioteca. h: os cabeçalhos, em minúsculas.
function verificarStandard(h, corpoCru, segredo, agora = Date.now() / 1000) {
const id = h['webhook-id'], t = h['webhook-timestamp'];
if (!id || !t || Math.abs(agora - Number(t)) > 300) return false;
const chave = Buffer.from(segredo.replace(/^whsec_/, ''), 'base64');
const esperado = crypto.createHmac('sha256', chave).update(`${id}.${t}.${corpoCru}`).digest('base64');
return (h['webhook-signature'] || '').split(' ').some(s => {
const [versao, assinatura] = s.split(',');
return versao === 'v1' && assinatura?.length === esperado.length
&& crypto.timingSafeEqual(Buffer.from(assinatura), Buffer.from(esperado));
});
}
Python
import base64, hashlib, hmac, re, time
def verificar_kingdom(cabecalho, corpo_cru: bytes, segredo: str, agora=None) -> bool:
agora = time.time() if agora is None else agora
t = re.search(r'(?:^|,)t=(\d+)', cabecalho or '')
if not t or abs(agora - int(t.group(1))) > 300:
return False
esperado = hmac.new(segredo.encode(), t.group(1).encode() + b'.' + corpo_cru, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, esperado) for v in re.findall(r'v1=([0-9a-f]{64})', cabecalho))
def verificar_standard(h: dict, corpo_cru: bytes, segredo: str, agora=None) -> bool:
agora = time.time() if agora is None else agora
i, t = h.get('webhook-id'), h.get('webhook-timestamp')
if not i or not t or abs(agora - int(t)) > 300:
return False
chave = base64.b64decode(segredo.removeprefix('whsec_'))
esperado = base64.b64encode(hmac.new(chave, f'{i}.{t}.'.encode() + corpo_cru, hashlib.sha256).digest()).decode()
return any(v == 'v1' and hmac.compare_digest(s, esperado)
for v, _, s in (x.partition(',') for x in h.get('webhook-signature', '').split(' ')))
Vectores de teste
Se o teu código der exactamente estes valores, está certo.
segredo whsec_MfKQ9r8GKYqrTYLCCHSgKgmhAIVEyG4GC3rBJHs8Qdo=
id evt_test_001
t 1790000000
corpo {"id":"evt_test_001","type":"integration.test","api_version":"2026-09-01","created_at":"2026-09-21T13:33:20Z","livemode":false,"data":{"message":"hello"}}
X-Kingdom-Signature t=1790000000,v1=1b0df85aafe409b7f2b6c1f1aafdefe2e22da4adce4baad570619561068db003
webhook-signature v1,8XoEyulc+Miy/jfHuHRrAlq26G6fBIrein2njgwH8ns=
Como responder
- Responde 2xx em menos de 10 segundos e faz o trabalho pesado depois. Não seguimos redireccionamentos.
- O mesmo evento pode chegar mais de uma vez: guarda o
ide ignora o que já viste, respondendo 2xx. - 401, 403, 404 ou um 3xx: tratamos como configuração errada — repetimos e avisamos neste ecrã.
- 400, 413, 422 e os outros 4xx: recusaste este conteúdo, e não o repetimos.
- 408, 425, 429, 5xx, rede ou tempo esgotado: repetimos.
Repetições
Depois da primeira tentativa: 1 min, 5 min, 30 min, 2 h, 6 h, 12 h e 24 h, cada uma com ±10% de variação — oito
tentativas ao todo. Um Retry-After num 429 ou 503 manda, até 24 horas. Os eventos de um pedido saem
pela ordem: um order.refunded espera que o order.paid do mesmo pedido tenha sido entregue
ou tenha falhado de vez. Ao fim de 20 falhas seguidas a integração pausa; as entregas ficam guardadas e
reenviam-se daqui. Os eventos guardam-se 30 dias.
A API
Em https://api.kingdomcompny.com/v1, com Authorization: Bearer sk_live_… (ou sk_test_…).
Erros em application/problem+json (RFC 9457), Idempotency-Key nos POST, paginação por
cursor, Request-Id em todas as respostas e 300 pedidos por minuto por chave. A especificação completa está em
openapi.json (OpenAPI 3.1). Cada evento tem o seu JSON Schema
em api/schemas/ — por exemplo Event.order.paid.json —
e o que muda de versão para versão está no CHANGELOG.