Chargeback & Dispute Proof-of-Delivery Tracking API (Stripe, Shopify, PayPal & Escrow)

When a cardholder files an "Item Not Received" (INR) chargeback (Visa Reason Code 13.1, Mastercard 4855, American Express C08) or a PayPal / Shopify Payments dispute, card networks give merchants a strict window to submit compelling delivery evidence. A screenshot of an internal ERP status or an unverified tracking link rarely wins—issuing banks look for a complete, chronological carrier audit trail showing:

  1. Carrier name and tracking number (carrier, trackingNumber, service).
  2. Terminal delivery confirmation (status: "delivered", deliveredAt UTC timestamp, deliveredAtLocal wall-clock time, and signedBy when available).
  3. Destination city/state/country alignment (destination) matching the cardholder's shipping address.
  4. Full chronological checkpoint history (events[]) from origin acceptance through local sortation, out_for_delivery, and terminal delivered scan.

lastscanned gives dispute-automation platforms, Shopify brands, 3PLs, and B2B escrow systems a deterministic, normalized JSON audit trail across 542 carriers—via synchronous GET /v1/track/:number, batch GET /v1/track?numbers=..., or Watches & Webhooks (managed in the Web UI Dashboard or via POST /v1/watches).

For one-off manual disputes, you can also generate formatted evidence packets in your browser with the free Chargeback & Delivery Dispute Evidence Packet Generator.


Why Dispute Automation Fails Without lastscanned

Dispute Evidence ChallengeWhat Happens With Standard Carrier Portals / Legacy AggregatorsHow lastscanned Solves It
Carrier purges tracking history before the 120-day chargeback windowCardholders can file INR disputes up to 120 days after purchase, when many regional and postal carriers have already recycled or hidden web tracking pages.Create a Watch at fulfillment (POST /v1/watches or in app.lastscanned.com/watches). When the package transitions to delivered, your webhook receives and archives the complete immutable result payload immediately.
Cross-border parcels show "Delivered to local courier" instead of buyer's cityQuerying Yanwen (UL494089685YP), 4PX (4PX3002639757438CN), Cainiao (LP00836938654367), or YunExpress (YT2621200705470145) alone often stops at customs handover, losing the dispute.lastscanned extracts the domestic last-mile carrier and barcode in message ("Handed to UniUni as UUS5BM1269395824723"), letting you attach both the international leg and the local doorstep delivery scan (UniUni, SpeedX, GOFO, OnTrac, USPS).
Regional couriers (UniUni, SpeedX, Veho, GOFO, OnTrac) lack Stripe auto-populateStripe and Shopify Payments only auto-populate basic tracking for major legacy carriers, leaving regional deliveries blank in automated responses.Fetch the normalized TrackingResult from GET /v1/track/:number and format the complete events[] log directly into Stripe's shipping_documentation / uncategorized_text fields.

1. Building a Stripe dispute.created Automated Evidence Handler

When Stripe sends a charge.dispute.created webhook with reason === "product_not_received", your backend can query GET https://api.lastscanned.com/v1/track/{trackingNumber} synchronously, resolve any cross-border last-mile handoff, and attach the complete carrier checkpoint log to the Stripe dispute in seconds:

import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function attachTrackingEvidenceToStripeDispute(
  disputeId: string,
  trackingNumber: string,
  shippingAddress: string,
  customerName: string,
) {
  // 1. Fetch live normalized tracking history from lastscanned
  const res = await fetch(
    `https://api.lastscanned.com/v1/track/${encodeURIComponent(trackingNumber)}`,
    { headers: { Authorization: `Bearer ${process.env.LASTSCANNED_API_KEY}` } },
  );
  if (!res.ok) throw new Error(`Tracking lookup failed: ${res.status}`);
  const pkg = await res.json();

  // 2. If cross-border parcel handed off to a domestic last-mile courier, fetch the doorstep leg too
  let lastMilePkg = null;
  const handoffMatch = pkg.message?.match(/Handed to .+ as ([A-Z0-9]{8,34})/i);
  if (handoffMatch) {
    const lmRes = await fetch(
      `https://api.lastscanned.com/v1/track/${encodeURIComponent(handoffMatch[1])}`,
      { headers: { Authorization: `Bearer ${process.env.LASTSCANNED_API_KEY}` } },
    );
    if (lmRes.ok) lastMilePkg = await lmRes.json();
  }

  const activePkg = lastMilePkg ?? pkg;
  const timelineLines = activePkg.events.map(
    (e: { timestamp: string | null; localTime: string | null; status: string; location: string | null; description: string }) =>
      `${e.timestamp ?? e.localTime ?? "N/A"} | [${e.status.toUpperCase()}] ${e.location ?? "Carrier Network"} — ${e.description}`,
  );

  const evidenceSummary = [
    `CARRIER PROOF OF DELIVERY AUDIT LOG`,
    `Primary Tracking Number: ${pkg.trackingNumber} (${pkg.carrier.toUpperCase()})`,
    lastMilePkg ? `Final-Mile Tracking Number: ${lastMilePkg.trackingNumber} (${lastMilePkg.carrier.toUpperCase()})` : null,
    `Terminal Status: ${activePkg.status.toUpperCase()} (${activePkg.statusDescription ?? ""})`,
    `Delivered Timestamp (UTC): ${activePkg.deliveredAt ?? "See scan log"}`,
    `Delivered Timestamp (Local): ${activePkg.deliveredAtLocal ?? "See scan log"}`,
    activePkg.signedBy ? `Signed By: ${activePkg.signedBy}` : null,
    `Chronological Carrier Scans (${activePkg.events.length} checkpoints):`,
    ...timelineLines,
  ]
    .filter(Boolean)
    .join("\n");

  // 3. Submit structured fields + chronological checkpoint log to Stripe Dispute API
  await stripe.disputes.update(disputeId, {
    evidence: {
      customer_name: customerName,
      shipping_address: shippingAddress,
      shipping_carrier: activePkg.carrier.toUpperCase(),
      shipping_tracking_number: activePkg.trackingNumber,
      shipping_date: activePkg.events.at(-1)?.localTime?.slice(0, 10) ?? undefined,
      uncategorized_text: evidenceSummary,
    },
    submit: false, // Set true to auto-submit when activePkg.status === "delivered"
  });
}

2. Archiving Delivery Proof Automatically with Watches (app.lastscanned.com/watches & POST /v1/watches)

Because carriers periodically purge tracking records after 45–90 days—while cardholders have up to 120 days to initiate a chargeback—the safest practice is to archive the terminal delivered payload the day the package arrives:

  1. Via the Web UI Dashboard (app.lastscanned.com/watches): Paste up to 100 tracking numbers at a time in the browser, monitor Active vs. Ended (Delivered) shipments, and configure your webhook endpoint on Webhooks (app.lastscanned.com/webhooks).
  2. Via POST /v1/watches: Register each tracking number when an order ships. When the carrier records the final delivery scan, lastscanned sends a signed tracking.updated webhook with result.status === "delivered", result.deliveredAt, result.signedBy, and the complete result.events array so you can store an immutable copy alongside the order record in your database.

On this page