COD is a black hole with two mouths.
D2C brands in India ship 60–70% of orders as cash-on-delivery. Money leaks at two points. Before dispatch: ghost numbers, buyer's remorse and fake orders sail straight to the warehouse, get picked, packed, shipped — and come back as RTO, with the brand paying freight both ways. After dispatch: genuine orders fail delivery for fixable reasons — customer unavailable, address unclear, OTP not received — and the courier gives you a 24–72 hour window to fix it before the parcel turns around.
The old setup was reactive on both fronts: orders flowed unfiltered, and the courier's daily NDR sheet was worked by hand out of Excel, row by row, phone call by phone call.
Nothing ships without a verified tag.
Tag-gating is the whole trick. The moment a COD order lands, it's tagged unverified in Shopify — and the warehouse's fulfillment sync only pulls orders tagged verified. Between those two tags sit three verification channels running in parallel, none waiting on the others:
- WhatsApp — every COD order gets a template with Confirm / Cancel quick-reply buttons, sent 10 minutes after checkout. Free-text replies ("haan", "nahi", "ok") are pattern-matched too.
- Formant AI voice call — a decision engine (14,241 evaluations to date) scores each unflagged order and pushes risky ones to an automated calling layer, capped at 10 concurrent calls.
- Human review queue — orders tripping flag rules land with the team; agent outcomes feed the same state machine as the bots.
Underneath is a last-write-wins state machine: pending → confirmed → cancelled, flip-floppable until Shopify reports fulfillment or cancellation — then the state locks and later responses are logged for audit but change nothing. Every event from every channel lands in an immutable audit table, and an SSE stream pushes state changes to every open dashboard, so an agent sees the instant a bot beats them to a confirmation.
Every failed delivery gets a second chance.
When delivery fails anyway, the courier's NDR sheet lands in a companion app on the same WhatsApp rail. An operator uploads the raw xlsx, rows dedup by AWB, and each failure type maps to its own approved template — refused, unavailable, OTP, payment, address — because "customer refused" and "OTP not received" deserve different messages.
Replies bucket automatically into reattempt / cancel / update info / contact support, with an incident history per AWB — an address hit by NDR three days running shows as a history, not an overwrite. What needs a human surfaces in a clean operator queue; the target is a full courier file worked in under an hour with no row falling through.
The two systems share one WABA and one inbound webhook: replies to ndr_* templates are routed to the NDR app, everything else flows to COD verification. One phone number, two lifecycles.
Production from day one.
FastAPI services on AWS Tokyo behind Caddy, Postgres with immutable event tables, Shopify webhooks in, Shopify Admin API tags out, Meta Cloud API for WhatsApp with HMAC-verified inbound. A mirror of 48,494 Shopify orders backs the decision engine; the routing rules were backtested on 250k historical orders before going live. Runbooks, state diagrams and a HANDOFF.md ship with it.
RTO: 15% → 5.1%.
The number that matters: returns-to-origin cut from 15% to 5.1% — two-thirds of the problem gone. Every point of RTO saved is freight not paid twice, inventory not stuck in transit, and a customer not lost to a failed delivery.
The pipeline runs unattended. Confirmations arrive by button click, voice call or agent note and resolve to a single tag the warehouse trusts. Doomed parcels don't ship; failed ones get a structured second attempt instead of a phone-tag lottery.
Built solo — schema to templates to deployment. The ops team's only job is the queue the machines couldn't clear.