Carrier Tracking Sync for Zendesk - FedEx + UPS by Kyle TullyCarrier Tracking Sync for Zendesk - FedEx + UPS by Kyle Tully

Carrier Tracking Sync for Zendesk - FedEx + UPS

Kyle Tully

Kyle Tully

A self-updating FedEx and UPS shipment tracker, built inside Zendesk

A tracking number on a Zendesk ticket becomes a live, automatically refreshed shipment record, with HMAC-verified ingestion, idempotent writes, status normalized across two carriers, and documentation an ops team can operate from without me. Dev and carrier-sandbox scope.

Overview

Carrier Tracking Sync turns a tracking number entered on a Zendesk ticket into a durable, self-refreshing shipment record. FedEx and UPS are polled on a schedule, every status is normalized to one internal enum, and agents read live tracking from the ticket without leaving Zendesk or pasting carrier links. Zendesk Custom Objects are the system of record and local n8n runs the orchestration. This is a self-directed reference build on a dev instance and carrier sandboxes, not a production deployment.

The problem

Support teams that field "where is my package" tickets usually track shipments by hand. An agent copies a tracking number into a carrier website, reads the status, and pastes it back into the ticket.
That status is stale the moment it lands, it lives in free text instead of structured data, and nothing links the ticket to a record you can query later. Two carriers, FedEx and UPS, mean two response formats and two status vocabularies to reconcile by eye. The goal was a system where setting a tracking number is the only manual step. Everything after that (creating the shipment, refreshing its status, recording each scan, and retiring it once delivered) happens on its own and stays queryable.

What I built

Four n8n workflows over a two-object Zendesk Custom Objects data model, deployed from version control and documented from the codebase:
Ingest: a Zendesk trigger fires when the tracking-number field is set and POSTs an HMAC-signed webhook. The workflow verifies the signature, validates the payload, detects the carrier, upserts a carrier_shipment, and links it back to the ticket's shipment lookup field. Sync: every 30 minutes it lists active shipments, calls FedEx and UPS, normalizes each carrier status to a single six-value enum, and appends scan history. Delivered and returned shipments are marked inactive and stop being polled. Prune: a daily job deletes shipments terminal for more than 90 days, and their scan events, to bound record growth. Error handler: a shared error workflow attached to every workflow emits a structured alert on any failure, so problems surface instead of failing silently.

Key contributions

Ingestion and idempotency

Inbound webhooks are authenticated with an HMAC-SHA256 signature over the timestamp and raw body, inside a replay window. A bad signature returns 401.
Every write is idempotent. Shipments upsert on external_id (carrier:tracking_number) and scan events dedupe on tracking_number:event_uid, so duplicate deliveries and repeated polls never double-create records.

Status normalization without drift

FedEx and UPS each speak their own status vocabulary. Both are mapped to one internal enum: PRE_TRANSIT, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, EXCEPTION, RETURNED.
The mapping has a single source of truth shared by the runtime and a unit test that fails if the inlined copy drifts from the contract, so the code and the tests can never disagree about what a carrier code means.

Reliability and resilience

Zendesk writes are Retry-After aware: back off on 429, retry 5xx, never retry other 4xx. Writes go through Zendesk's async jobs endpoint, chunked at 100 records per job. The workflow treats a job as queued, not applied, and waits before reading back. FedEx returns per-package errors inside a 200 response, and multi-package test numbers can return canonical numbers that do not pair to the one queried. Sync calls FedEx one tracking number per request for reliable result pairing.

Documentation and handover

The system is documented from the codebase, not from interviews: a System Overview, a Data Model and Entity Overview, and an Integration Specification, each with a traceability table mapping every claim back to the file that backs it.
Deploys are scripted against the n8n Public API: an idempotent, name-based upsert of the workflow definitions, then activate, reconcile credential references, and assign the shared error workflow. An ops user can redeploy or recover without me.

Proof

The four workflow canvases (Ingest, Sync, Prune, Error handler) and the three Notion documents above are the real artifacts behind this write-up.
The documented contracts, faithful to the build: status enum: PRE_TRANSIT -> IN_TRANSIT -> OUT_FOR_DELIVERY -> DELIVERED, EXCEPTION (non-terminal), RETURNED terminal: DELIVERED, RETURNED -> is_active=false, stops polling shipment key: external_id = "{carrier}:{tracking_number}" (upsert) event key: external_id = "{tracking_number}:{event_uid}" (append-only, dedupe) zendesk writes: backoff on 429 (Retry-After), retry 5xx, never retry other 4xx
Ingest: an HMAC-verified Zendesk webhook detects the carrier and upserts an idempotent shipment record linked to the ticket.
Ingest: an HMAC-verified Zendesk webhook detects the carrier and upserts an idempotent shipment record linked to the ticket.
Sync: every 30 minutes, active shipments are polled from FedEx and UPS, statuses normalized to one enum, scan history appended idempotently.
Sync: every 30 minutes, active shipments are polled from FedEx and UPS, statuses normalized to one enum, scan history appended idempotently.
Prune: a daily job retires shipments terminal for over 90 days, deleting their scan events first to bound record growth.
Prune: a daily job retires shipments terminal for over 90 days, deleting their scan events first to bound record growth.
Error handler: a shared workflow on every flow emits a structured alert on failure, so nothing fails silently.
Error handler: a shared workflow on every flow emits a structured alert on failure, so nothing fails silently.
System Overview: purpose, components, flows, and risks, documented from the codebase.
System Overview: purpose, components, flows, and risks, documented from the codebase.
Data model: two Zendesk Custom Objects, carrier_shipment and tracking_event, with the status lifecycle and a traceability table back to source.
Data model: two Zendesk Custom Objects, carrier_shipment and tracking_event, with the status lifecycle and a traceability table back to source.
Integration spec: the Zendesk, n8n, FedEx, and UPS interfaces with their auth methods and resilience rules.
Integration spec: the Zendesk, n8n, FedEx, and UPS interfaces with their auth methods and resilience rules.

Outcome

This is a reference build, so the honest result is what it demonstrates and how well it holds up, not a production metric.
End to end: setting a tracking number on a ticket creates a linked shipment, the scheduled sync refreshes status from both carriers and appends scan history, terminal shipments retire themselves, and failures raise a structured alert.
Both carriers work against their sandbox APIs. The behavior is held in place by 79 tests at 80%+ coverage, including a test that fails if the status mapping drifts.
The system is documented well enough that someone other than the author can operate and extend it.
Like this project

Posted Jun 6, 2026

Self-refreshing FedEx/UPS shipment records inside Zendesk. 4 idempotent n8n workflows, normalized statuses, plus docs an ops team runs without me. Dev/sandbox.