Webhook e entrega
Este é o passo mais delicado da integração, e o único em que a plataforma chama você. Quando o pagamento é confirmado, a Lootfy manda ao seu servidor uma ordem de transferência assinada — um webhook. O seu servidor valida a assinatura, entrega o item dentro do jogo e responde à plataforma se conseguiu ou não. Depois, um segundo webhook fecha o ciclo com o estado terminal.
Para o dono de servidor: até aqui o seu servidor sempre falou primeiro. Agora é a Lootfy quem bate na sua porta — e por isso o seu endpoint precisa checar, a cada batida, se é mesmo a Lootfy. Um endpoint que não valida a assinatura aceita qualquer um mandando “transfira este item”, e isso é exatamente o que você não pode permitir.
Como os webhooks chegam
Seção intitulada “Como os webhooks chegam”Os dois webhooks chegam no mesmo formato, na sua webhookUrl configurada:
POST <sua-webhookUrl>Content-Type: application/jsonX-Lootfy-Signature: <protegido>..<assinatura>A assinatura usa o mesmo JWS destacado das suas chamadas de saída, só que na direção contrária: assinada com a chave PLATFORM, e o kid do header protegido é o platformKeyKid que você recebeu no provisionamento.
Validar a assinatura (obrigatório)
Seção intitulada “Validar a assinatura (obrigatório)”Antes de agir sobre qualquer webhook, valide-o. São cinco passos, e nenhum é opcional:
-
Separe o header em
<protegido>..<assinatura>. O segmento do meio tem de estar vazio (assinatura destacada). -
Decodifique o segmento protegido (base64url → JSON) e confira dois campos:
alg == "RS256"ekid == platformKeyKid. -
Reconstrua o
signing_input=base64url(protegido) + "." + base64url(corpo_cru), usando os bytes recebidos, antes de qualquer parse do JSON. -
Verifique a assinatura com o
platformPublicKeyPem. -
Confira o
exp— a plataforma emite com 5 minutos de validade.
A mecânica completa da assinatura, nos dois sentidos, está em Assinatura JWS na prática.
O webhook transfer_order — a ordem de transferência
Seção intitulada “O webhook transfer_order — a ordem de transferência”Significa: o pagamento foi confirmado, transfira o item.
{ "event": "transfer_order", "transactionId": 42, "itemRef": "srv-offer-9931", "quantity": 2, "buyerCharacterRef": "char-123", "sellerCharacterRef": "char-777", "paidAt": "2026-09-14T12:05:00.000Z"}A sua resposta DEVE carregar o estoque atualizado do anúncio — é a observação mais informativa que existe, e alimenta a parte observada da contabilidade da plataforma:
// 200{ "observedQuantity": 3 }// ou, se preferir envelopar: { "data": { "observedQuantity": 3 } }Aceitamos as duas formas: você não segue a nossa convenção de resposta, e exigir a forma dela seria inventar contrato.
Entregar: tudo ou nada
Seção intitulada “Entregar: tudo ou nada”A entrega é atômica. Se o comprador pediu 5 e você só tem 3, não entregue 3 — reporte falha. Não existe entrega parcial, e por isso a confirmação de entrega não tem campo de quantidade entregue.
O destino da entrega precisa ser durável: um depot que sobrevive a rollback/crash, nunca o inventário de quem pode deslogar. É a capacidade B3 dos pré-requisitos — item entregue que evapora num crash deixa a transação concluída sem o item ter chegado.
Confirmar ou reportar
Seção intitulada “Confirmar ou reportar”POST /v1/api/transactions/42/deliveryX-Service-Api-Key: <sua-api-key>X-Lootfy-Signature: <protegido>..<assinatura>Content-Type: application/json
{}// 200{ "data": { "transactionId": 42, "paymentStatus": "PAGO", "deliveryStatus": "ENTREGUE", "deliveredAt": "2026-09-14T12:06:00.000Z"} }Esta confirmação é a fonte da verdade da entrega: só depois dela a fatia do vendedor se torna sacável.
POST /v1/api/transactions/42/delivery-failureX-Service-Api-Key: <sua-api-key>X-Lootfy-Signature: <protegido>..<assinatura>Content-Type: application/json
{ "reason": "OUT_OF_STOCK", "observedQuantity": 0, "detail": "vendido pela moeda do jogo" }// 200 (OUT_OF_STOCK){ "data": { "transactionId": 42, "paymentStatus": "PAGO", "deliveryStatus": "FALHOU", "refundStarted": true, "retryAt": null} }O motivo decide o desfecho, e só você sabe distinguir:
reason | Significado | O que acontece |
|---|---|---|
OUT_OF_STOCK | o item não existe mais para entregar | deliveryStatus: FALHOU → estorno automático |
TEMPORARY | a entrega ainda pode acontecer (servidor reiniciando, receptor indisponível) | segue PAGO / AGUARDANDO_SERVIDOR, a ordem volta para a fila e você a recebe de novo (veja retryAt) |
observedQuantityé obrigatório nos dois motivos.detailé livre, vai para o nosso log e para a tela de admin, e nunca é exibido ao comprador.
O webhook transaction_terminal — o estado terminal
Seção intitulada “O webhook transaction_terminal — o estado terminal”Depois do desfecho, a plataforma manda um segundo webhook para você reconciliar o seu lado:
{ "event": "transaction_terminal", "transactionId": 42, "itemRef": "srv-offer-9931", "paymentStatus": "ESTORNADO", "deliveryStatus": "FALHOU", "reason": "OUT_OF_STOCK", "occurredAt": "2026-09-14T12:07:00.000Z"}O desfecho terminal vem no par de status paymentStatus + deliveryStatus — o mesmo par de dois eixos que você já vê nas respostas de transação. Você deriva o estado terminal da combinação, conforme a tabela abaixo. O reason pode ser null. Repare que uma falha de entrega reportada com TEMPORARY não é terminal e não dispara este webhook; o que dispara é o desfecho definitivo da transação.
| Desfecho | paymentStatus | deliveryStatus |
|---|---|---|
| Concluída (entregue e liberada) | PAGO | ENTREGUE (com completedAt) |
| Estornada | ESTORNADO | FALHOU ou EM_DISPUTA |
| Expirada | EXPIRADO | null |
| Cancelada | CANCELADO | null |
Retentativa, duplicata e a corrida
Seção intitulada “Retentativa, duplicata e a corrida”O seu endpoint precisa ser projetado para três realidades:
- Qualquer
2xxencerra a entrega. Qualquer outra coisa — status fora da faixa, timeout de 10s, erro de rede — vira retentativa com backoff exponencial (15s dobrando até 1h). - A retentativa não esgota. Ela para só quando você acusa recebimento ou quando a transação vira terminal. Servidor que ficou horas fora do ar recebe a ordem ao voltar — projete o endpoint para isso.
- A entrega é at-least-once: trate duplicata. Use o
transactionIdcomo chave de deduplicação (capacidade A4 dos pré-requisitos). Reentregar uma ordem é inócuo por desenho; reentregar uma entrega não seria — por isso a resposta do08não decide nada.
Testar contra staging
Seção intitulada “Testar contra staging”O roteiro que exercita o ciclo inteiro, contra https://api.staging.lootfy.gg/v1:
- Vincule um personagem comprador e um vendedor (vincular personagem,
POST /v1/api/character-links). - Anuncie um item barato (registrar anúncio,
POST /v1/api/market/listings). - Crie a transação e abra a
checkoutUrl(criar transação,POST /v1/api/transactions). - Pague o Pix — o sandbox do PSP paga sozinho em segundos.
- Espere o webhook
transfer_orderchegar no seu endpoint e valide a assinatura. - Responda
{ "observedQuantity": N }e confira, via consultar transação (GET /v1/api/transactions/{transactionId}), que a transação estáPAGO/AGUARDANDO_SERVIDOR. - Feche com confirmar entrega (vai a
PAGO/ENTREGUE) ou reportar falha de entrega comOUT_OF_STOCK(vai aFALHOUe depois estorna). - Confirme que o webhook
transaction_terminalchegou com o estado final.
Se o webhook transfer_order não chegar, confira a webhookUrl cadastrada e se o seu endpoint responde 2xx — a entrega fica retentando, então ela não se perde enquanto você conserta.