Problem
The authorization expired before anyone captured it
Orders shipped against a card hold that lapsed: why manual capture fails, the exposure formula, a six-step check, and what AXIOTRA can and cannot see.
What it is
A card payment can be taken in two steps. The authorization asks the issuer to hold the amount on the cardholder's account; the capture, sent later, is what actually moves the money. Stores separate the two on purpose: to capture only when the order ships, to review an order for fraud first, to confirm stock on a made-to-order or pre-order item, or to adjust the amount after a split shipment.
The hold does not last. For an online card payment the authorization is typically valid for seven days, and shorter for some merchant-initiated charges; Stripe lists the window per card brand and exposes the exact deadline on the charge as capture_before. When the deadline passes without a capture, the funds are released back to the cardholder. On Stripe the PaymentIntent moves to canceled; on Shopify the order's payment status becomes Expired. An expired authorization is a sale with no money behind it, and the problem is the orders that shipped anyway.
How it happens
Capture tied to an event that never fires. The rule is capture on fulfillment, but fulfillment is recorded by a 3PL app or a warehouse system that marks the order shipped without triggering the capture, or the integration that should call capture fails quietly. The parcel leaves; the hold runs out.
Lead time longer than the window. Pre-orders, backorders, made-to-order and custom items routinely ship after seven days. If payment was authorized at checkout and capture waits for shipment, the authorization is gone before the item is ready, on every such order, by design.
A review queue that stalls. Orders held for manual fraud review or address verification sit in a queue over a weekend or a holiday. The reviewer approves on day eight, the order goes to the warehouse, and nobody notices that the approval came after the hold lapsed.
Partial capture that releases the rest. On most card payments only one capture is allowed per authorization. Capturing the first shipment of a split order captures that amount and releases the remainder automatically, so the second shipment has nothing left to capture against unless the payment was set up for multiple captures.
A setting changed by accident. Switching a store from automatic to manual capture, often during a test or a fraud scare, turns every later order into one that needs a human to press capture. Nothing fails loudly; the payment list simply fills with uncaptured rows.
Why it matters
While the hold is live the exposure is the uncaptured part of the order, with a deadline: potential_exposure = order.total − sum(captured amounts), due by capture_before. That is recoverable in one click, which is why the deadline, not the amount, sets the priority.
After the deadline the same figure changes class. If the goods have not shipped, nothing is lost yet: do not ship, and ask the customer to pay again. If the goods have shipped, loss = order.total − sum(captured amounts) unless the customer pays a new request, and the unit cost and outbound shipping are already spent. A late capture attempt against an expired authorization is not backed by the hold and may simply be declined.
The pattern also distorts reconciliation. The order platform reports the sale in revenue while the processor never settles it, so the payout comes in lower than sales suggest and the gap looks like a fee or payout problem until someone lists the uncaptured charges.
How to diagnose
1. List the orders whose payment status is authorized, partially paid or expired, with order id, total, placed_at and fulfillment status. On Shopify, filter the orders list by payment status; on a custom checkout, use the order's own payment state.
2. List the processor side for the same period: charges or PaymentIntents that are uncaptured or canceled, with amount, amount captured, created and capture_before. On a Stripe export the order reference is in metadata or the description, as it is for every other charge.
3. Join on the order reference and add the shipment rows: shipped_at and tracking. Four buckets fall out. Shipped with the authorization expired: loss candidates. Shipped with the authorization still live: capture today. Not shipped with the authorization expired: hold the order and request payment again. Partially captured with every shipment sent: the remainder is the gap.
4. Sort the still-live rows by capture_before, earliest first. Anything due within a day is the working list for today, ahead of any larger but older loss.
5. For the shipped-and-expired rows, confirm nothing else collected the money: a second charge on the same order, a manual invoice, or a payment link. Then contact the customer with a new payment request. Do not charge a saved card again without the customer's agreement to that charge.
6. Find the cause before closing the list. Group the rows by fulfillment source, by product, and by the day the order was reviewed. One 3PL or one pre-order product on every row points at the capture trigger, not at the customers; fix the trigger, or capture at checkout for items that ship later than the window.
What AXIOTRA does with it today
There is no detector for expired authorizations, and the payment detector can hide one. The order_payment_mismatch detector pairs payments with orders on the order reference and does not read the status of either side. A charge that was authorized and never captured still counts as the order's payment, so the order does not appear as unpaid; it raises a finding only if its amount differs from the order total.
What reaches the ledger depends on the door. A Stripe connection stores an uncaptured charge with status authorized and with the charge's amount, which is the authorized amount rather than the captured amount, so a partial capture does not show as an amount difference either. Each sync re-reads charges created in the last 30 days, so a charge captured later is updated to succeeded; one that expired stays authorized. A Shopify connection brings the order's payment status (authorized, partially_paid, expired, voided) onto the order and no payment rows. From CSV, the payments file's status column is stored as given.
Two places do use it. The Stripe payout comparison counts only Stripe payments whose status is captured or succeeded, so authorized rows are left out of the expected payout, which is correct for this problem. And the Orders screen shows each order's status, which makes step 1 above visible for the first 40 orders; for a longer list, filter the export instead. The join to shipments and the capture_before deadline are not computed anywhere in the product today.