ProductsDocsGuidesPricing
Sign inGet Started

Intelligence APIs for developers.

Product

  • All APIs
  • Support Triage
  • Pricing

Developers

  • Getting started
  • Guides
  • API reference
  • Errors
  • Dashboard

Company

  • About
  • Security
  • Contact

Legal

  • Privacy Policy
  • Terms of Service
© Kovca. kovca.io
GuidesOperations

Catching Delivery Exceptions Before Your Customers Do

Turn raw carrier tracking events into routed delivery exceptions: late and stalled parcels, address problems, losses and disputes. A scheduled pipeline with curl, TypeScript and Python.

Shipment Intelligence6 min readcURL · TypeScript · Python
monitorcontact_carrierinvestigate

Most "where is my order?" tickets are predictable. The tracking page told the story days earlier: a weather delay in Denver, then nothing. No scan for two days, and the promised date three days gone. The customer noticed before you did because nobody reads tracking events for parcels that look fine.

This guide builds a small job that reads tracking for every in-flight shipment, spots the ones that have gone wrong, and sends each to the right workflow: contact the carrier, contact the customer, or open an investigation. The interpretation step uses the Kovca Shipment Intelligence API. Kovca never fetches tracking or contacts carriers. You supply the events, and it tells you what they mean.

Why tracking is hard to read in code

Every carrier says it differently. "Delivery exception: weather delay at destination facility", "EX - WTHR", and "Shipment on hold" can all mean the same thing. A keyword list misses some of them and catches the wrong ones.

Silence is a signal. The most important event is often the one that didn't happen. A parcel with no scan for three days can't be found by text matching, because there's no text to match.

Dates need arithmetic, not judgement. "Late" means the current time is past the promised date. That's subtraction, and it should never be estimated.

Kovca splits the work the same way. Days late and days since the last scan are computed in code from your timestamps. Carrier wording and customer messages are interpreted to find the type of problem.

The pipeline

  1. 1
    Scheduler
    every 4–6 hours, in-flight shipments only
  2. 2
    Tracking store
    events from carrier webhooks or polling
  3. 3
    Your policy
    no_scan_days, escalate_days_late per service level
  4. 4
    Kovca Shipment Intelligence
    exception_type, priority, recommended_action
    Investigationsdamaged, lost, disputes: requires_human_review
  5. 5
    Workflow router
    carrier ticket, customer email, or keep monitoring
  6. 6
    Order system
    request_id, last result, next check time

1Pick the shipments worth checking

Don't re-check everything every hour. Select shipments that are in transit, have had a new event since the last check, or have passed a time threshold (for example, no scan in 48 hours). Delivered shipments with no customer complaint can drop out of the loop.

2Send the evidence

Send up to 50 events in any order. Kovca sorts them and tells you if they arrived out of order. Include promised_delivery_date whenever you have one. Without it, a delay is still reported, but its lateness can't be measured, and missing_information will say so. Set as_of when you replay old shipments so results are reproducible.

curl

cURL
curl https://kovca.io/api/v1/shipments/triage \
  -H "Authorization: Bearer $KOVCA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "shipment_id": "SHP-48213",
    "carrier": "Example Parcel",
    "events": [
      { "timestamp": "2026-03-02T09:10:00Z", "status": "PU", "description": "Picked up", "location": "Columbus, OH" },
      { "timestamp": "2026-03-03T22:41:00Z", "status": "IT", "description": "Departed sort facility", "location": "Indianapolis, IN" },
      { "timestamp": "2026-03-05T14:05:00Z", "status": "EX", "description": "Delivery exception: weather delay at destination facility", "location": "Denver, CO" }
    ],
    "promised_delivery_date": "2026-03-05",
    "as_of": "2026-03-08T12:00:00Z",
    "service_policy": { "no_scan_days": 2, "escalate_days_late": 3 }
  }'
JSON
{
  "request_id": "req_Tz7nW2qLr5YbK9cM",
  "exception_type": "delayed",
  "priority": "high",
  "recommended_action": "contact_carrier",
  "requires_human_review": false,
  "signals": ["past_promised_date", "no_recent_scan"],
  "missing_information": [],
  "days_past_promised": 3,
  "days_since_last_event": 2
}

The parcel is three days late and has had no scan for two days. That reaches your escalate_days_late threshold, so priority is high. The next step is a call to the carrier, not an apology email.

TypeScript (Node 18+)

TypeScript
type TrackingEvent = { timestamp: string; description: string; status?: string; location?: string };

type ShipmentResult = {
  request_id: string;
  exception_type: "delayed" | "delivery_failed" | "address_issue" | "damaged" | "lost_or_missing" | "delivery_dispute" | "customs_hold" | "no_exception" | "other";
  priority: "low" | "medium" | "high";
  recommended_action: "monitor" | "contact_carrier" | "contact_customer" | "request_information" | "investigate" | "manual_review";
  requires_human_review: boolean;
  signals: string[];
  missing_information: ("tracking_events" | "promised_delivery_date")[];
  days_past_promised?: number;
  days_since_last_event?: number;
};

export async function checkShipment(s: {
  id: string;
  carrier: string;
  events: TrackingEvent[];
  promisedBy?: string;
  customerMessage?: string;
}): Promise<ShipmentResult> {
  const res = await fetch("https://kovca.io/api/v1/shipments/triage", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.KOVCA_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      shipment_id: s.id,
      carrier: s.carrier,
      events: s.events.slice(-50), // the most recent 50
      promised_delivery_date: s.promisedBy,
      customer_message: s.customerMessage?.slice(0, 2000),
      service_policy: { no_scan_days: 2, escalate_days_late: 3 },
    }),
  });
  if (!res.ok) throw new Error(`shipment check failed: ${res.status}`); // retry on the next scheduled run
  return res.json();
}

Python

Python
import os, requests

def check_shipment(shipment_id, carrier, events, promised_by=None, customer_message=None):
    body = {
        "shipment_id": shipment_id,
        "carrier": carrier,
        "events": events[-50:],
        "service_policy": {"no_scan_days": 2, "escalate_days_late": 3},
    }
    if promised_by:
        body["promised_delivery_date"] = promised_by
    if customer_message:
        body["customer_message"] = customer_message[:2000]
    r = requests.post(
        "https://kovca.io/api/v1/shipments/triage",
        headers={"Authorization": f"Bearer {os.environ['KOVCA_API_KEY']}"},
        json=body, timeout=30,
    )
    r.raise_for_status()  # a scheduled job can simply try again next run
    return r.json()

A scheduled job has a useful property: retries are free. If a request fails, the shipment is picked up on the next run. You don't need a retry loop, but do honor Retry-After on 429 RATE_LIMITED so a big batch doesn't keep hitting the limit.

3Route each exception to one workflow

exception_typerecommended_actionYour workflow
delayedmonitor, or contact_carrier once late or silentOpen a carrier trace. Email the customer a new estimate before they ask.
delivery_failed, address_issue, customs_holdcontact_customerAsk the customer to fix the address, arrange redelivery, or provide customs documents.
damaged, lost_or_missing, delivery_disputeinvestigateAssign to a person. Hold refunds and reships until they decide.
no_exceptionmonitorCheck again on the next run.
other or unclearmanual_reviewAssign to a person with the raw events.

requires_human_review is true for damage, loss, disputes, other, contradictory tracking, and unclear types. Treat it as a hard stop for automation: no refunds, reships, or claims without a person.

Lost is a strong word. Kovca returns loss_confirmed_by_carrier only when a carrier event explicitly declares the loss. Otherwise it's loss_suspected. Keep that distinction in your customer emails, because "we're looking into it" and "it's lost" lead to very different conversations.

4Fold in the customer's side

When a "where is my order?" ticket arrives, run the same check with the customer's words in customer_message. That's how a "delivered" scan becomes a delivery_dispute: tracking says it was left at the door, and the customer says it wasn't. Tracking data alone can never show that.

Rolling it out

  1. Read-only first. Run the job for a week and post a daily digest of high priority exceptions to a team channel. Compare it with the tickets that arrived.
  2. Proactive emails for delays. Automate the "your order is running late" email for delayed with contact_carrier. Most customers would rather hear it from you first.
  3. Leave the rest with people. Investigations stay manual. The win is that they start days earlier and with the evidence already attached.

Volume and cost

Cost scales with in-flight shipments × checks per day. Checking only shipments with new events or a crossed threshold keeps it small. Free covers 1,000 successful requests a month at 10 per minute. Pay As You Go is $0.005 per successful request at 60 per minute, with a spending cap (pricing).

Try it on last week's late parcels

Export the tracking history for five shipments that turned into tickets and run them through with as_of set to the day before the customer wrote in. Sign up free, create a key, and try it in the playground. The routing table and every signal are in the Shipment Intelligence docs.

On this page

  • Why tracking is hard to read in code
  • The pipeline
  • Rolling it out
  • Volume and cost
  • Try it on last week's late parcels

Shipment Intelligence

Interpret shipment tracking exceptions and route each one to the right workflow.

Try it in the playground

Keep going

Shipment IntelligenceAPI reference
billingtechnicalneeds_review
Support Triage7 min read

Automating Support Ticket Triage in Your Helpdesk Pipeline

Build a support ticket triage pipeline: webhook intake, category and priority classification, human-review routing, and safe fallbacks. Code in curl, TypeScript and Python.

Read guide
hotwarmnurture
Lead Intelligence7 min read

Qualifying and Routing Inbound Leads in Minutes

Qualify inbound demo requests against your own criteria, route them to the right territory, and keep research and support requests out of sales queues. Code in curl, TypeScript and Python.

Read guide
same_variantdifferent_variantuncertain
Catalog Matching9 min read

How to Match Supplier Product Catalogs to Your Own SKUs

A practical guide to matching supplier feeds to your SKUs: blocking candidates, using GTINs safely, handling variants, and routing uncertain matches. Code in curl, TypeScript and Python.

Read guide