Cada integração de tickets entrega duas vezes. Construa-a para isso.
Webhooks que repetem, parceiros que reenviam, redes que perdem respostas. Como construir uma ponte de tickets assinada, idempotente, sem loops e com âmbito limitado, com código que pode reutilizar.

Ligar o seu sistema de tickets ao portal de um parceiro parece trabalho de uma tarde: envia-se um webhook quando algo muda, chama-se a API deles quando respondem. A primeira demonstração funciona sempre. Os problemas começam na terceira semana, quando um timeout faz o remetente repetir, um ticket recebe a mesma resposta duas vezes, e dois sistemas começam a responder um ao outro em loop às três da manhã. Este artigo é sobre as quatro propriedades que impedem isso, sejam quais forem as plataformas de ticketing em cada extremo.
Assuma entrega at-least-once
Qualquer remetente que repete pode entregar o mesmo evento mais do que uma vez. Não tem alternativa: se o seu servidor processou o pedido mas a resposta se perdeu no caminho de volta, o remetente vê um timeout e tem de tentar de novo. «Exatamente uma vez» não existe numa rede. O que existe é entrega at-least-once mais um recetor que consegue ver a mesma coisa duas vezes em segurança.
Por isso, a primeira regra de desenho é que cada evento transporta um identificador estável e único escolhido pelo remetente, e o recetor regista os identificadores que já aplicou.
CREATE TABLE applied_events (
event_id TEXT PRIMARY KEY,
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ticket_ref TEXT
);
Em cada entrega, tente inserir primeiro o ID. Se o insert falhar na primary key, já fez este trabalho: responda sucesso e pare. Responder sucesso é importante. Se responder com um erro, o remetente vai continuar a repetir algo que já aplicou.
O ID do evento tem de pertencer ao evento, não à tentativa de entrega. Uma resposta no ticket 4711 deve transportar um ID derivado dessa resposta, por exemplo o ID da transação no sistema de origem, para que cada repetição dela transporte o mesmo valor.
Assine cada entrega, e verifique o relógio
Assine cada entrega: um HMAC sobre o raw body com um segredo partilhado, enviado num header, juntamente com um timestamp para que um pedido capturado não possa ser reproduzido na semana seguinte.
import hmac, hashlib, time
SECRET = load_secret() # never in the code repository
MAX_SKEW = 300 # seconds
def verify(raw_body: bytes, ts_header: str, sig_header: str) -> bool:
ts = int(ts_header)
if abs(time.time()-ts) > MAX_SKEW:
return False
signed = f"{ts}.".encode() + raw_body
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_header)
Três detalhes em que a maioria se engana. Assine os bytes em bruto, não um objeto JSON novamente serializado, porque duas bibliotecas não vão serializar o mesmo objeto de forma idêntica. Compare com uma função de tempo constante, não com ==. E mantenha um segredo separado por parceiro, para poder rodar um sem tocar nos outros.
Quebre os loops de propósito
Uma ponte copia respostas nos dois sentidos. Se o sistema A reencaminha as suas respostas para B, e B reencaminha as suas para A, então uma resposta que chegou de B é, do ponto de vista de A, uma nova resposta no ticket. Sem uma salvaguarda, volta diretamente para trás. Depois B faz o mesmo.
A solução é marcar a origem e verificá-la antes de reencaminhar:
- Quando aplicar uma resposta recebida, registe que veio do parceiro, por exemplo num campo personalizado ou num atributo da transação.
- O seu trigger de saída ignora qualquer transação marcada como originada no parceiro.
- Faça o mesmo do lado do parceiro. Não confie apenas num dos extremos.
Depois teste com um ticket real: envie uma resposta de cada lado e verifique que cada uma aparece exatamente uma vez no outro.
Dê ao parceiro uma fila e nada mais
O sentido de entrada significa normalmente que o parceiro chama a API do seu sistema de tickets. Dê-lhe uma conta dedicada, nunca um token de administrador partilhado, e conceda direitos apenas numa fila.
No Request Tracker isso significa um utilizador sem direitos globais e depois, só na fila partilhada, só os direitos de que a ponte precisa: tipicamente SeeQueue, ShowTicket, CreateTicket, ReplyToTicket e ModifyTicket. Deixe de fora ShowTicketComments, para que o parceiro nunca consiga ler comentários internos, e não conceda nada em nenhuma outra fila. Emita um auth token para esse utilizador e entregue-o ao parceiro.
Depois prove o âmbito em vez de confiar nele:
# Should succeed: the shared queue
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: token $PARTNER_TOKEN" \
https://rt.example.com/REST/2.0/queue/Partner-Shared
# Should be refused: any other queue
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: token $PARTNER_TOKEN" \
https://rt.example.com/REST/2.0/queue/General
Uma dica específica do Request Tracker: se o RT corre sob Apache com mod_fcgid, o header Authorization não é passado à aplicação a menos que acrescente FcgidPassHeader Authorization ao virtual host. Os pedidos com token falham então como não autenticados, apesar de o token ser válido. Vale a pena saber isto antes de gastar uma noite com o problema.
Mantenha os comentários fora da ponte
Reencaminhe respostas. Nunca reencaminhe comentários. Os seus engenheiros precisam de um sítio onde dizer «este cliente já perguntou isto três vezes» sem que apareça no portal de outra pessoa. Teste com um comentário que contenha um marcador óbvio e procure-o do lado do parceiro.
Nunca descarte nada em silêncio
Quando o endpoint do parceiro está em baixo, as suas entregas de saída têm de esperar, não desaparecer:
- Repita com exponential backoff e jitter, durante um tempo limitado.
- Depois disso, estacione o evento num dead-letter store juntamente com o erro.
- Varra o dead-letter store de forma agendada e volte a tentar.
- Alerte uma pessoa se algo ficar estacionado mais tempo do que consegue tolerar.
- Registe cada entrega com o ID do evento, o ticket, o código de estado e a duração.
Faça a entrega num worker, não dentro do próprio pedido do sistema de tickets. Se o parceiro for lento, a sua equipa não deve ficar à espera para que uma página grave.
Mais dois hábitos defensivos. Verifique os anexos por checksum dos dois lados durante os testes. «Parece igual» não é um teste. E limite o tamanho do que aceita: uma imagem inline colada em resolução máxima num ticket pode ser muitas vezes maior do que o texto à sua volta. Reduza-a ou guarde-a em separado à entrada, em vez de deixar um ticket pesado atrasar todas as páginas que o mostram.
Uma lista de verificação para a sua própria ponte
- Cada evento tem um ID escolhido pelo remetente, e o recetor regista os ID aplicados.
- Cada pedido é assinado sobre o raw body, com timestamp e um segredo por parceiro.
- As alterações originadas no parceiro são marcadas e nunca enviadas de volta.
- O token do parceiro funciona numa fila e é recusado em todas as outras, comprovado por teste.
- Os comentários nunca passam. As respostas passam sempre.
- Existem repetições, dead letters, varrimentos e alertas, e cada entrega fica registada.
- Os anexos são verificados por checksum. O conteúdo sobredimensionado é tratado à chegada.
Construímos e operamos pontes como esta entre o Request Tracker e portais de parceiros. Se está a planear uma, ou a viver com uma que se porta mal, teremos todo o gosto em ajudar.
Related
Relacionados: Managed Request Tracker com pontes para parceiros · Managed team chat em Mattermost
Publicado em 9 de outubro de 2026.
Todas as notas de campo