Pular para o conteúdo

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.

Os dois webhooks chegam no mesmo formato, na sua webhookUrl configurada:

POST <sua-webhookUrl>
Content-Type: application/json
X-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.

Antes de agir sobre qualquer webhook, valide-o. São cinco passos, e nenhum é opcional:

  1. Separe o header em <protegido>..<assinatura>. O segmento do meio tem de estar vazio (assinatura destacada).

  2. Decodifique o segmento protegido (base64url → JSON) e confira dois campos: alg == "RS256" e kid == platformKeyKid.

  3. Reconstrua o signing_input = base64url(protegido) + "." + base64url(corpo_cru), usando os bytes recebidos, antes de qualquer parse do JSON.

  4. Verifique a assinatura com o platformPublicKeyPem.

  5. 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.

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.

POST /v1/api/transactions/42/delivery
X-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.

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.

DesfechopaymentStatusdeliveryStatus
Concluída (entregue e liberada)PAGOENTREGUE (com completedAt)
EstornadaESTORNADOFALHOU ou EM_DISPUTA
ExpiradaEXPIRADOnull
CanceladaCANCELADOnull

O seu endpoint precisa ser projetado para três realidades:

  • Qualquer 2xx encerra 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 transactionId como 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 do 08 não decide nada.

O roteiro que exercita o ciclo inteiro, contra https://api.staging.lootfy.gg/v1:

  1. Vincule um personagem comprador e um vendedor (vincular personagem, POST /v1/api/character-links).
  2. Anuncie um item barato (registrar anúncio, POST /v1/api/market/listings).
  3. Crie a transação e abra a checkoutUrl (criar transação, POST /v1/api/transactions).
  4. Pague o Pix — o sandbox do PSP paga sozinho em segundos.
  5. Espere o webhook transfer_order chegar no seu endpoint e valide a assinatura.
  6. Responda { "observedQuantity": N } e confira, via consultar transação (GET /v1/api/transactions/{transactionId}), que a transação está PAGO / AGUARDANDO_SERVIDOR.
  7. Feche com confirmar entrega (vai a PAGO / ENTREGUE) ou reportar falha de entrega com OUT_OF_STOCK (vai a FALHOU e depois estorna).
  8. Confirme que o webhook transaction_terminal chegou 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.