Pular para conteúdo

Política de retentativa

A Gubee entrega webhooks com semântica at-least-once: cada evento é tentado até 5 vezes e, se ainda assim falhar, vai para uma fila persistente de notificações perdidas que você pode recuperar por API.

Ciclo de vida de um evento

  1. Evento de negócio acontece na Gubee (pedido faturado, estoque alterado, etc.).
  2. A Gubee registra o evento com timestamp received e o coloca na fila de saída.
  3. Primeira tentativa (attempts: 1): POST no endpoint cadastrado.
  4. Se resposta 2xx: evento marcado como entregue, fim.
  5. Se resposta não-2xx, timeout ou erro de rede: agenda nova tentativa com backoff exponencial.
  6. Após 5 tentativas sem sucesso, o evento é movido para a fila de notificações perdidas (missed), onde fica disponível para consulta por API.
  7. Você lê a fila periodicamente, reprocessa e marca como lido (DELETE).

Backoff exponencial

A Gubee aplica backoff exponencial entre tentativas (lado servidor). Os intervalos exatos são ajustáveis e não fazem parte do contrato público, mas a política é:

  • Não há retentativa imediata — sempre há um intervalo mínimo.
  • O intervalo cresce a cada nova tentativa, tipicamente da ordem de minutos, depois dezenas de minutos, depois horas.
  • A janela total até um evento ir para missed costuma ficar em algumas horas.

Como receptor, isso significa que:

  • Se o seu endpoint cair por 1 minuto, você provavelmente vai receber por retentativa.
  • Se cair por horas, parte dos eventos vai parar em missed — planeje consumir essa fila.

Fila de notificações perdidas

Endpoint: GET /integration/notifications/missed/{urlName}

O {urlName} é o nome que você deu ao endpoint ao cadastrá-lo no painel Gubee (Minha Conta → Notificações de Eventos). Não é a URL; é o rótulo. Se você não lembra, consulte o painel.

A resposta é paginada em HAL+JSON (PagedModel<EntityModel<MissedNotificationApiDTO>>):

{
  "_embedded": {
    "missedNotificationApiDTOList": [
      {
        "id": "60d5ecb8b3d4c20001a1b2c3",
        "resource": "/integration/orders/1234567890",
        "sellerId": "5f3e2a1b2c3d4e5f6a7b8c9d",
        "domain": "order",
        "endpointId": "erp-prod-001",
        "type": "OrderInvoiced",
        "attempts": 5,
        "lastAttemptDateTime": "2022-06-15T17:55:12.000000Z",
        "request": {
          "url": "https://erp.seudominio.com.br/gubee/webhook",
          "body": "{\"resource\":\"/integration/orders/1234567890\",...}"
        },
        "response": {
          "httpStatus": 504,
          "body": "Gateway Timeout"
        }
      }
    ]
  },
  "page": {
    "size": 20,
    "totalElements": 1,
    "totalPages": 1,
    "number": 0
  },
  "_links": {
    "self": { "href": "https://api.gubee.com.br/integration/notifications/missed/erp-prod-001?page=0&size=20" }
  }
}

Campos da notificação perdida

Campo Descrição
id Identificador da notificação na fila missed. Use no DELETE para marcar como lida.
resource, sellerId, domain, type Mesmo significado do payload de webhook.
endpointId O urlName que você cadastrou.
attempts Total de tentativas (sempre 5 quando chega aqui).
lastAttemptDateTime Timestamp da última tentativa falha.
request.url URL que a Gubee tentou chamar.
request.body Payload bruto enviado — útil para auditoria e replay.
response.httpStatus Status HTTP devolvido pelo seu endpoint na última tentativa (0 se timeout/connrefused).
response.body Corpo da resposta do seu endpoint, se houver.

Os campos request e response estão presentes apenas em chamadas que de fato aconteceram (se a Gubee nunca conseguiu abrir conexão, podem estar ausentes).

Fluxo de recuperação completo

TOKEN="<seu jwt>"
URL_NAME="erp-prod-001"
BASE="https://api.gubee.com.br"

# 1. Lista as primeiras 50 perdidas
curl -sS \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Accept: application/hal+json' \
  "$BASE/integration/notifications/missed/$URL_NAME?size=50" \
  | jq '._embedded.missedNotificationApiDTOList[] | {id, type, resource, response: .response.httpStatus}'

Saída típica:

{
  "id": "60d5ecb8b3d4c20001a1b2c3",
  "type": "OrderInvoiced",
  "resource": "/integration/orders/1234567890",
  "response": 504
}

Para cada item retornado:

  1. Reprocesse — use o resource para buscar a entidade atualizada e execute sua lógica de negócio.
  2. Aplique idempotência — o mesmo evento pode ter sido entregue com sucesso em uma tentativa anterior e ainda assim ter chegado à fila por inconsistência. Veja idempotency.md.
  3. Marque como lida com DELETE:
NOTIFICATION_ID="60d5ecb8b3d4c20001a1b2c3"

curl -sS -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE/integration/notifications/missed/$NOTIFICATION_ID" \
  -w "%{http_code}\n"

Resposta esperada: 204 No Content. A notificação some da próxima paginação.

Script de drenagem

Exemplo de loop que consome todas as páginas até esvaziar a fila:

#!/usr/bin/env bash
set -euo pipefail

TOKEN="<seu jwt>"
URL_NAME="erp-prod-001"
BASE="https://api.gubee.com.br"

page=0
while :; do
  resp=$(curl -sS \
    -H "Authorization: Bearer $TOKEN" \
    -H 'Accept: application/hal+json' \
    "$BASE/integration/notifications/missed/$URL_NAME?size=50&page=$page")

  ids=$(echo "$resp" | jq -r '._embedded.missedNotificationApiDTOList[].id // empty')
  total=$(echo "$resp" | jq -r '.page.totalElements')

  [[ -z "$ids" ]] && { echo "fila vazia"; break; }

  while read -r nid; do
    echo "reprocessando $nid..."
    # ... sua lógica aqui (chamar o resource, gravar no ERP, etc.) ...
    curl -sS -X DELETE \
      -H "Authorization: Bearer $TOKEN" \
      "$BASE/integration/notifications/missed/$nid" -o /dev/null -w "ack $nid: %{http_code}\n"
  done <<< "$ids"

  ((page++))
  echo "total restante (antes do ack): $total"
done

Retenção

A fila missed tem retenção longa (configurável internamente). Trate-a como uma fila operacional, não como armazenamento permanente — faça a drenagem periódica (ex.: a cada hora) e nunca conte com eventos antigos para reconciliação histórica. Para histórico de pedidos, use diretamente GET /integration/orders.

Boas práticas

  • Monitore a fila. Se ela crescer, seu endpoint está instável. Alerta se totalElements > threshold.
  • Implemente replay por tabela interna. Salve todo webhook recebido numa tabela webhook_inbox com estado (PENDING, PROCESSED, FAILED). Quando precisar refazer, dê replay local, não dependa da fila da Gubee.
  • Tenha sua própria chave de dedup. A entrega é at-least-once (veja idempotency.md). Sugestão de chave: hash(itemId + type + received).
  • Não processe síncrono no handler HTTP. Salve o payload e responda 2xx rápido. O processamento pesado vai num worker assíncrono.
  • Combine com a fila de pedidos. A fila /integration/orders/queue/{status} é complementar: ela contém os pedidos por status, independente de webhook ter sido disparado ou não. Útil como reconciliação diária.

Fila de pedidos como alternativa ao webhook

Se você não quer (ou não pode) expor um endpoint público, pode usar a fila de pedidos como única via:

# Lista pedidos prontos para faturar
curl -sS \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE/integration/orders/queue/invoiced?size=50"

# Ack após processar
curl -sS -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  "$BASE/integration/orders/queue/invoiced/1234567890"

Status suportados na fila: created, canceled, paid/payed, invoiced, shipped, delivered, rejected. Os pedidos reaparecem na fila até que você faça o DELETE de ack — então a semântica é at-least-once também aqui. Há ainda um endpoint dedicado /integration/orders/queue/rejected com retenção de 30 dias para pedidos cuja atualização de canceled/shipped/delivered foi rejeitada pelo marketplace.

Veja mais detalhes no guia de jornada Receber pedidos no ERP.

Próximos passos