Package tracking usually feels backwards.
The customer gets a tracking number, opens a page, refreshes it, waits, refreshes again, and tries to decode carrier status updates. Even when a business sends SMS updates, those messages are usually one-way.
I wanted to build the opposite pattern: a shipment that can talk.
This example turns each shipment into a small agent. It receives carrier updates, remembers package state, sends proactive SMS messages, handles customer replies, and exposes a voice/call path for the same shipment context.
Code: https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent
What the app does
The app is a Python Flask service built around a ShipmentAgent class.
Each agent owns the state for one shipment:
- shipment ID
- current carrier status
- ETA
- customer contact
- event history
- scheduled follow-ups
When something changes, the agent wakes up and decides what to do next.
Carrier status update
-> POST /webhooks/carrier
-> ShipmentAgent updates package state
-> Telnyx Messaging sends the customer an SMS
Customer SMS reply
-> POST /webhooks/telnyx
-> verified Telnyx webhook
-> ShipmentAgent responds with shipment context
Customer call
-> POST /api/agents/<shipment_id>/call
-> ShipmentAgent returns the current shipment summary
The interesting idea is not just "send a text when a package moves." The interesting idea is that the package has a durable communication object behind it.
Why this pattern matters
Most support automations are built around sessions.
A chat session starts, a user asks a question, the bot responds, and then the session disappears.
Shipments do not work that way. A package may be created today, delayed tomorrow, delivered three days from now, and followed up on next week. The agent needs to survive across events, channels, and time.
That is why this example treats the agent as the package.
Instead of asking "what should this webhook do?" the app asks "what happened to this shipment, and what should its agent do now?"
Carrier updates become customer messages
The carrier webhook accepts statuses like:
PICKED_UP
IN_TRANSIT
DELAYED
OUT_FOR_DELIVERY
DELIVERED
The app maps those carrier statuses into agent events.
For example, when a shipment is picked up, the agent sets its status to IN_TRANSIT, calculates an ETA, and sends the customer an SMS through Telnyx Messaging.
When the package is delivered, the agent sends a delivery message and schedules a follow-up request.
For this demo, state is stored in memory and scheduling uses a background thread. In production, you would move that state into Postgres, Redis, or another durable store, and use a real job runner for future wakes.
The webhook fix that matters
The latest update to this example tightened the inbound SMS path.
That matters because customer replies only work if the Telnyx webhook is verified and parsed correctly.
The app now:
- sets the Telnyx public key on the SDK module at startup
- reads the correct
Telnyx-Timestampheader - verifies incoming webhooks with
telnyx.Webhook.construct_event - handles the Telnyx v2 message payload shape
- supports
from_when the SDK maps the JSONfromfield - pins the Telnyx Python SDK to
telnyx>=2.0,<3.0
That last point is easy to overlook. The example uses the v2 SDK API shape, including telnyx.Message.create and telnyx.Webhook.construct_event, so the dependency is pinned to avoid accidentally installing a newer incompatible SDK.
Small fix, big difference: inbound SMS replies can now verify correctly and route back to the right shipment agent.
Local setup
Clone the repo:
git clone https://github.com/team-telnyx/telnyx-code-examples.git
cd telnyx-code-examples/shipment-agent
Create a virtual environment and install dependencies:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
Create your environment file:
cp .env.example .env
Fill in:
TELNYX_API_KEY=<your_telnyx_api_key>
TELNYX_PUBLIC_KEY=<your_telnyx_public_key>
TELNYX_MESSAGING_PROFILE_ID=<your_messaging_profile_id>
TELNYX_FROM_NUMBER=<your_telnyx_number>
TELNYX_TO_NUMBER=<customer_test_number>
Then run:
python app.py
The app starts on port 5000.
Example carrier event
You can simulate a package delay with:
curl -X POST http://localhost:5000/webhooks/carrier \
-H "Content-Type: application/json" \
-d '{
"shipment_id": "SHP-DEMO-001",
"status": "DELAYED",
"event_data": {
"new_eta": "2026-09-04"
}
}'
The app finds or creates the shipment agent, updates the package status, and sends the customer an SMS update.
You can inspect the agent state with:
curl http://localhost:5000/api/agents/SHP-DEMO-001
Where AI fits in
When a customer replies by SMS, the app builds a prompt from the shipment context and the customer's message.
That is the right place to use Telnyx AI Inference for natural language shipment Q&A: "Can I change the delivery instructions?", "Why is this delayed?", or "What happens if I am not home?"
The important part is that the AI response is grounded in the shipment state the agent already owns.
It is not a generic chatbot. It is answering as the communication layer for a specific package.
Production notes
Before turning this into a real logistics workflow, I would add:
- persistent storage for shipment agents
- idempotency for duplicate carrier webhooks
- retry handling for SMS delivery
- customer authentication before changing delivery preferences
- audit logs for agent decisions
- a real background job queue
- escalation to a human support workflow
But as a developer example, the core pattern is useful: model real-world objects as durable agents, then give those agents communication channels.
Resources
- Code example: https://github.com/team-telnyx/telnyx-code-examples/tree/main/shipment-agent
- Telnyx AI toolkit: https://github.com/team-telnyx/ai
- Telnyx Messaging docs: https://developers.telnyx.com/docs/messaging
- Telnyx Call Control docs: https://developers.telnyx.com/docs/voice/programmable-voice
- Webhook signing: https://developers.telnyx.com/development/api-fundamentals/webhooks/receiving-webhooks#webhook-signing
- Telnyx Portal: https://portal.telnyx.com
Top comments (0)