Toda integración de tickets entrega dos veces. Constrúyala para eso.
Los webhooks reintentan, los partners reenvían, las redes pierden respuestas. Cómo construir un puente de tickets firmado, idempotente, sin bucles y acotado, con código que puede reutilizar.

Conectar su sistema de tickets con el portal de un partner parece trabajo de una tarde: publicar un webhook cuando algo cambia, llamar a su API cuando responden. La primera demo siempre funciona. El problema empieza en la tercera semana, cuando un timeout hace que el emisor reintente, un ticket recibe la misma respuesta dos veces, y dos sistemas empiezan a responderse el uno al otro en bucle a las tres de la madrugada. Este artículo trata de las cuatro propiedades que lo evitan, sean cuales sean las plataformas de ticketing de cada extremo.
Asuma entrega al menos una vez
Cualquier emisor que reintenta puede entregar el mismo evento más de una vez. No tiene elección: si su servidor procesó la petición pero la respuesta se perdió en el camino de vuelta, el emisor ve un timeout y tiene que intentarlo de nuevo. «Exactamente una vez» no existe en una red. Lo que existe es la entrega al menos una vez más un receptor que puede ver lo mismo dos veces sin peligro.
Así que la primera regla de diseño es que cada evento lleva un identificador estable y único elegido por el emisor, y el receptor registra los identificadores que ya ha aplicado.
CREATE TABLE applied_events (
event_id TEXT PRIMARY KEY,
received_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ticket_ref TEXT
);
En cada entrega, intente insertar primero el ID. Si el insert falla por la clave primaria, ese trabajo ya está hecho: responda éxito y pare. Responder éxito importa. Si responde con un error, el emisor seguirá reintentando algo que usted ya aplicó.
El ID del evento debe pertenecer al evento, no al intento de entrega. Una respuesta en el ticket 4711 debería llevar un ID derivado de esa respuesta, por ejemplo el ID de transacción en el sistema de origen, de modo que cada reintento lleve el mismo valor.
Firme cada entrega, y compruebe el reloj
Firme cada entrega: un HMAC sobre el cuerpo en bruto con un secreto compartido, enviado en una cabecera, junto con una marca de tiempo para que una petición capturada no pueda reproducirse la semana que viene.
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)
Tres detalles en los que la gente se equivoca. Firme los bytes en bruto, no un objeto JSON reserializado, porque dos bibliotecas no serializarán el mismo objeto de forma idéntica. Compare con una función de tiempo constante, no con ==. Y mantenga un secreto distinto por partner, para poder rotar uno sin tocar los demás.
Rompa los bucles a propósito
Un puente copia respuestas en ambas direcciones. Si el sistema A reenvía sus respuestas a B, y B reenvía las suyas a A, entonces una respuesta que llegó desde B es, desde el punto de vista de A, una respuesta nueva en el ticket. Sin una protección, vuelve directa. Después B hace lo mismo.
La solución es marcar el origen y comprobarlo antes de reenviar:
- Cuando aplique una respuesta entrante, registre que vino del partner, por ejemplo en un campo personalizado o en un atributo de la transacción.
- Su disparador de salida omite cualquier transacción marcada como originada por el partner.
- Haga lo mismo en el lado del partner; no confíe en un solo extremo.
Después pruébelo con un ticket real: envíe una respuesta desde cada lado y compruebe que cada una aparece exactamente una vez en el otro.
Dé al partner una cola y nada más
La dirección de entrada normalmente significa que el partner llama a la API de su sistema de tickets. Dele una cuenta dedicada, nunca un token de administrador compartido, y conceda derechos sobre una sola cola.
En Request Tracker eso significa un usuario sin derechos globales, y después, solo en la cola compartida, los derechos que el puente necesita: normalmente SeeQueue, ShowTicket, CreateTicket, ReplyToTicket y ModifyTicket. Deje fuera ShowTicketComments, para que el partner nunca pueda leer los comentarios internos, y no conceda nada en ninguna otra cola. Emita un token de autenticación para ese usuario y entrégueselo al partner.
Después demuestre el alcance en lugar de confiar en él:
# 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
Un consejo específico de Request Tracker: si RT corre bajo Apache con mod_fcgid, la cabecera Authorization no llega a la aplicación salvo que añada FcgidPassHeader Authorization al virtual host. Las peticiones con token fallan entonces como no autenticadas aunque el token sea válido. Merece la pena saberlo antes de gastar una tarde en ello.
Mantenga los comentarios fuera del puente
Reenvíe respuestas; nunca reenvíe comentarios. Sus ingenieros necesitan un sitio donde decir «este cliente ya ha preguntado esto tres veces» sin que aparezca en el portal de otro. Pruébelo con un comentario que contenga un marcador obvio y búsquelo en el lado del partner.
Nunca descarte nada en silencio
Cuando el endpoint del partner esté caído, sus entregas salientes deben esperar, no desaparecer:
- Reintente con backoff exponencial y jitter, durante un tiempo acotado.
- Después, aparque el evento en un almacén de cartas muertas junto con el error.
- Barra el almacén de cartas muertas con una cadencia fija y reintente.
- Alerte a un humano si algo permanece aparcado más tiempo del que puede tolerar.
- Registre cada entrega con ID de evento, ticket, código de estado y duración.
Haga la entrega en un worker, no dentro de la propia petición del sistema de tickets. Si el partner va lento, su personal no debería estar esperando a que se guarde una página.
Dos hábitos defensivos más. Verifique los adjuntos por checksum en ambos lados durante las pruebas; «parece igual» no es una prueba. Y limite el tamaño de lo que acepta: una imagen pegada a resolución completa en un ticket puede ser muchas veces mayor que el texto que la rodea. Recórtela o guárdela aparte en la puerta, en lugar de dejar que un ticket pesado ralentice cada página que lo muestre.
Una lista de comprobación para su propio puente
- Cada evento tiene un ID elegido por el emisor, y el receptor registra los ID aplicados.
- Cada petición va firmada sobre el cuerpo en bruto, con marca de tiempo y un secreto por partner.
- Los cambios originados por el partner se marcan y nunca se devuelven.
- El token del partner funciona en una cola y se rechaza en todas las demás, demostrado con una prueba.
- Los comentarios nunca cruzan; las respuestas siempre.
- Existen reintentos, cartas muertas, barridos y alertas, y cada entrega queda registrada.
- Adjuntos verificados por checksum; contenido sobredimensionado tratado a la llegada.
Construimos y operamos puentes como este entre Request Tracker y portales de partners. Si está planeando uno, o convive con uno que se porta mal, estaremos encantados de ayudar.
Related
Relacionado: Request Tracker gestionado con puentes a partners · Chat de equipo gestionado en Mattermost
Publicado el 9 de octubre de 2026.
Todas las notas de campo