Skip to content
Commerce Exception
COMMERCE EXCEPTION RESOLUTION API

The retry is what refunds the customer twice.

A worker dies mid-batch, a webhook is delivered again, two agents open the same ticket: and the same broken order is resolved twice. Commerce Exception takes an order's payment, fulfilment, shipping and stock state, classifies every exception on it, and returns one action per exception with an idempotency key derived from the remedy rather than from the request. Replay the same exception and you get the same key. No proposed payout can exceed what was actually captured.

Free tier: 250 exceptions/month Paid from $299/month No card to run the demo
Live resolution Ready

Runs the same engine the paid endpoint uses. Nothing is stored and no key is needed.

Press “Resolve this exception” to call the live API.
The problem

Three systems hold the truth. None of them agrees.

The processor knows what was captured. The WMS knows what shipped. The catalogue knows what is on the shelf. An exception is what happens in the gaps between them: and the gap is worked by hand, by whoever is on the queue that morning.

Nobody computes what is actually owed

Captured 394.00, delivered 19.00 of goods, one parcel lost, one capture duplicated. The number to refund is arithmetic across three systems, and it gets done in a spreadsheet, or in someone's head, under a ticket SLA.

Retries are indistinguishable from new problems

Queues retry. Webhooks redeliver. Two agents open the same ticket. Without a key that identifies the remedy, the second attempt looks like a fresh refund to the processor and it goes out.

The decision leaves no trace

A refund lands in the processor with a note field. Which rule fired, which evidence it rested on, what else was considered and rejected: none of it survives, so a chargeback six weeks later is defended from memory.

What it costs

Every one of these is a silent, permanent loss.

Nothing errors. The refund succeeds, the ticket closes, and the money is gone. These are found at month end, if they are found at all.

  • Double refund
    The same remedy applied twice

    A batch failed halfway and was re-run. Both passes computed the same refund, and both were accepted, because the second call carried a key generated at request time rather than one derived from the order and the amount. The processor did exactly what it was asked.

  • Over-refund
    More paid back than was ever taken

    One order, several claims: a duplicate capture, a lost parcel, a return already in the warehouse. Each is real, each is handled by a different person or rule, and together they exceed the capture. Nothing in the stack is holding a ceiling.

  • Under-collection
    Goods out of the door that were never billed

    A partial shipment went out against a partial capture, the remainder shipped later, and nobody captured the difference. It is the same arithmetic as an over-refund with the sign flipped, and it is checked far less often.

What it returns

A typed exception, a bounded action, a receipt.

Send the state your systems already hold. Back comes every exception on the order (not just the first) each with a severity, the money it puts at issue in integer minor units, and exactly one action to resolve it.

ExceptionDetected whenDecided action
duplicate_chargeThe same amount captured twice inside the duplicate window.refund: bounded by what is actually overheld.
payment_captured_not_fulfilledMoney taken, nothing shipped, fulfilment SLA passed.reship if stock is proven, otherwise refund.
shipment_lost_in_transitDeclared lost, or no carrier scan for the stale threshold.reship if stock is proven, otherwise refund.
return_received_not_refundedGoods came back; the money did not follow.refund of the returned value not yet returned.
refund_owed_not_issuedHolding more than the goods kept are worth, with no specific cause.refund of the residual balance.
partial_shipment_unbilled_remainderGoods worth more than was captured have shipped.recapture, bounded by uncaptured authorization.
inventory_oversoldMore units unshipped than exist on hand.escalate with the short units.
return_not_restockedReturned units are not back in sellable stock.restock, in units rather than money.
delivery_overduePast the promised date, still moving.escalate. No money moves.

Order state Ledger Findings Bounded actions Idempotency keys Receipt

  • 01

    One ledger, three systems

    Ordered, shipped, delivered, returned and restocked units are valued at the line price, in integers. From those comes entitledToHoldMinor: the value of goods that left, stayed gone and were not aboard a parcel that will never arrive.

  • 02

    Every exception, not the first

    An order can be charged twice and have a lost parcel and hold stock that was never booked back in. All three come back, each with its own severity and evidence. A queue needs the whole reason.

  • 03

    Claims allocated, not summed

    Several findings can claim the same money. They are granted in priority order out of the overheld balance until it runs out: duplicate capture first, residual last. The strongest claim is paid in full and the weakest is the one clipped.

  • 04

    A refund can never exceed the capture

    Everything payable is bounded by capturedMinor − refundedMinor, at one named function. The engine asserts it again before returning: a plan that would move more money than was taken raises an error instead of reaching a processor.

  • 05

    Keys derived from the remedy

    The key hashes the order, exception type, action, currency, amount and subject. It does not hash the observation time, the ticket id or the request. Replay the same exception and you get the same key: which is the whole point of one.

  • 06

    A receipt you can store

    What was decided, on what evidence, with a SHA-256 digest over the canonical decision: including the instant it was taken. A clean verdict gets one too, because "we looked and nothing was wrong" is worth being able to prove.

Example output

Charged twice, and the parcel never arrived.

$394.00 captured across two identical charges three minutes apart. A $178.00 keyboard shipment declared lost with no stock behind it; a $19.00 cable delivered. This is what POST /v1/exceptions returns, trimmed to the parts that decide the money.

{
  "severity": "critical",
  "ageDays": 19,
  "ledger": {
    "orderedValueMinor": 19700,
    "inTransitValueMinor": 17800,
    "entitledToHoldMinor": 1900,
    "capturedMinor": 39400,
    "netCapturedMinor": 39400,
    "duplicateCaptureMinor": 19700,
    "overholdMinor": 37500,
    "refundCeilingMinor": 39400
  },
  "findings": [
    { "type": "duplicate_charge",         "severity": "critical", "amountMinor": 19700 },
    { "type": "shipment_lost_in_transit", "severity": "critical", "amountMinor": 17800, "agedUp": true }
  ],
  "actions": [
    { "kind": "refund", "amountMinor": 19700, "reason": "duplicate_capture_returned",
      "idempotencyKey": "cxr_bdb5ff5fc060e3aa35f8c5d163170934" },
    { "kind": "refund", "amountMinor": 17800, "reason": "shipment_declared_lost",
      "idempotencyKey": "cxr_c698a66459a3e8f94efdd57c2b3d4941" }
  ],
  "totals": { "recoverableMinor": 37500, "refundMinor": 37500 },
  "receipt": {
    "moneyOutMinor": 37500,
    "refundCeilingMinor": 39400,
    "digest": "705a97fa4217025bce2b2e5426976ff3…"
  }
}

$375.00, not $394.00

The cable was delivered and the customer kept it, so $19.00 of the capture was earned. The refund is the overheld balance, not the whole charge. Refunding the capture would have handed back money for goods the customer still has.

Two claims, one capture

The duplicate charge and the lost parcel both want refunding. They are granted in priority order out of the $375.00 that is actually overheld: so the two amounts sum to exactly that, and never to more.

Submit it again tomorrow

Different ticket id, different observedAt, different request: the same two keys. That is what makes a retry safe, and it is the property this API is built around rather than one it mentions.

This service is newly launched. We publish no uptime percentage, customer count, logo wall or certification claim until we have the operating history to support one. The API decides and returns a plan; executing it against your processor, WMS and ledger is your side of the integration.

How it works

Three steps to a queue that cannot double-pay

  • Post the order state

    Lines with ordered, shipped, delivered and returned quantities; the payment; the shipment; optionally stock on hand. One unit per exception, up to 100 per call.

  • Execute the actions in order

    They come back in execution order, which is also the order money was allocated. Pass each idempotencyKey to your processor as its idempotency key and the retry problem is gone.

  • Store the receipt

    One digest per decision, over the ledger, the findings, the actions and the instant. It is what you produce when someone asks in November why $375.00 went back in March.

Rules the engine never bends

Integer money, UTC days, bounded payouts

  • Money is integer minor units. A fractional unitPriceMinor is a 400 naming the exact field, not a rounding. Every value is whole units times a whole price, so nothing can be a cent out.
  • A refund never exceeds the capture. Bounded at one named function, then asserted again before the response is built.
  • Currencies are never compared. A charge denominated in anything but the case currency is refused. There is no exchange rate here and there should not be.
  • Deadlines are whole UTC days. A refund window that shifts an hour across a DST boundary is a window that closed a day early. Duplicate detection is the deliberate exception: it is minute-granular, because two identical captures in one day is ordinary and two in four minutes is a double-submit.
  • observedAt is required. Never defaulted to now, because every deadline is measured against it: so a replay months later reproduces the same verdict.
  • Nothing is stored. Every endpoint is stateless. You send the state with each request; no order, payment or customer data is retained here.
Developer integration

Small surface, versioned, OpenAPI-first

Authenticate with Authorization: Bearer <key>. Every error returns a stable code and a requestId you can quote, and validation failures name the exact field in details.path.

POST/v1/keysPublic. Emails a one-time claim token to the address you give it. No key is returned on this call, proving the mailbox is what stops anyone minting a key on your account.
POST/v1/keys/claimPublic. Spends the emailed token and returns the key, shown once: 250 exceptions a month, no card.
POST/v1/exceptionsResolve one exception or up to 100. Billed one unit per exception.
POST/v1/demo/resolvePublic. Real engine, capped at 10 lines and 10 charges. No key required.
GET/v1/exception-typesPublic. Every exception type, action kind, reason code and policy default.
curl -X POST https://commerceexception-api.com/v1/exceptions \
  -H "Authorization: Bearer $COMMERCE_EXCEPTION_KEY" \
  -H "content-type: application/json" \
  -d '{"exception":{"exceptionId":"case_88213","orderId":"SO-90417",
        "currency":"USD","placedAt":"2026-03-02","observedAt":"2026-03-21",
        "lines":[{"sku":"KB-88","orderedQty":2,"shippedQty":2,
                  "deliveredQty":0,"returnedQty":0,
                  "unitPriceMinor":8900,"onHandQty":0}],
        "payment":{"capturedMinor":17800,"refundedMinor":0},
        "shipment":{"status":"lost","trackingId":"1Z-4471"}}}'
Pricing

Priced per exception, capped so a backlog import can’t surprise you

One unit is one exception submitted for resolution, however many findings and actions it produces. Every plan includes an allowance, then a per-unit overage that is hard-capped per billing period: your worst-case bill is the base price plus the cap, never more.

Developer
$299 /month
  • 10,000 exceptions included
  • $0.06 per additional exception
  • Overage capped at $600 per period
  • Full API access, Python and TypeScript SDKs
Start with this plan
Scale
$2,499 /month
  • 250,000 exceptions included
  • $0.02 per additional exception
  • Overage capped at $5,000 per period
  • Full API access, Python and TypeScript SDKs
Start with this plan
Enterprise
$5,000+ /month
  • 1,000,000 exceptions included
  • $0.01 per additional exception
  • Overage capped at $10,000 per period
  • Full API access, Python and TypeScript SDKs
Contact sales

Free tier: 250 exceptions per month, no card. Prices in USD, billed monthly. Payment is processed by Square through our shared billing platform; card details never touch this site.

What counts as an exception?
One order state submitted in one call. A batch of 50 bills 50 units. The number of findings and actions it produces makes no difference: an order with four things wrong with it costs the same as a clean one, because charging per finding would reward us for finding more.
Does it move the money itself?
No. It decides and returns a plan; you execute it against your processor, WMS and ledger. That is deliberate: this API never holds a payment credential, and an integration that goes wrong cannot go wrong with your money in it.
Why is the idempotency key derived rather than generated?
Because a generated key changes between retries, which is exactly when you need it not to. The key hashes the order, exception type, action kind, currency, amount and subject: and deliberately not the ticket id, the observation time or the request. Submit the same exception again and the processor recognises the second call as the same operation.
Can a plan ever refund more than was captured?
No. Every payable amount is bounded by capturedMinor − refundedMinor at one named function, competing claims are allocated out of the overheld balance rather than summed, and the engine asserts the total again before returning. A plan that violated it would raise an error rather than reach a processor.
What if I do not know my stock levels?
Leave onHandQty off. Oversells then cannot be detected and a refund cannot be upgraded to a reship, and both are said plainly in warnings rather than guessed. Refunding money the ledger says you should not be holding is always defensible; picking stock that turns out not to exist is a second exception on top of the first.
Can I stop it paying out large amounts automatically?
Set policy.autoRefundCapMinor. Above it the action becomes escalate carrying the proposed amount for a human to approve: and the money stays reserved meanwhile, so no other action on the same order can be granted it too.
Can I evaluate it before paying?
Yes. The demo at the top of this page is the real engine with no key at all, and the free tier gives you 250 exceptions a month against the full authenticated surface.

Resolve it once, however many times you try.

The free tier is 250 exceptions a month, no card. Create a key, post one broken order, and submit it a second time: the keys come back identical.

One email address, no password, no card. Prefer the terminal? The same key, in two calls:

curl -X POST https://commerceexception-api.com/v1/keys \
  -H "content-type: application/json" \
  -d '{"email":"you@company.com","name":"Evaluation key","source":{"source":"web","medium":"landing"}}'

# 202 Accepted. A one-time token is emailed to that address — spend it,
# and the response carries the key, shown once.

curl -X POST https://commerceexception-api.com/v1/keys/claim \
  -H "content-type: application/json" \
  -d '{"token":"<token from the email>"}'