Skip to content

PayPal sandbox only. No real money moves.

LearnHow it works

Retries and unknown outcomes

What the saga does when a capture is refused, rate limited, times out or never gets an answer.

Code-checkedread against the code it describes; nothing was run

Verified againstRead against the retry and review code listed at the foot of the page (PR #1954). Fake PayPal and simulation only.

Page last changed 7 Nov 2026 (from the git history)

A capture call can end in four very different ways, and charging the wrong person depends on telling them apart.

Taken

PayPal answers COMPLETED. Money moved. The settlement goes on.

Refused

PayPal says no and says why (declined, expired). Nothing moved. Safe to try the runner-up, once the truth confirms it.

Not run

HTTP 429. PayPal refused to run the request. Nothing moved and nothing is in doubt. Wait and repeat the same call.

Unknown

A timeout, a network failure or a 5xx. The capture may or may not have happened. Never charge the runner-up on a maybe.

Pick an answer in the diagram to follow its path.

A capture call fails: what the saga does next

Fake PayPalReal limits UNVERIFIED
A capture call fails: what the saga does nextA decision tree for the answer to the capture call. The transport first retries network errors, 429 and 500 to 504 up to four times with the same request id. Then the capture phase sorts the failure. A kill switch pauses. A missing token stalls with no clock. A 429 stalls with no unknown clock and retries with the same id after reading the truth. A timeout or 5xx starts a ten minute unknown clock and goes to review after it, never to the next bidder. A pending capture waits up to three days. A 400 or an id conflict goes to review as an invariant. A refusal that names the hold, including a capture past the honor period, reads the truth and then falls back to the runner-up. Any other refusal starts the unknown clock.The callWhat the answer isWhat the saga doescapturerequest id gv1-...intent saved firstTransportretries on network error,429, 500, 502, 503, 504up to 4 attempts, 10 s eachfull-jitter backoff 250 ms to 4 sRetry-After honored up to 30 ssame request id every attemptthen returns or throws thelast answer2xx, status COMPLETED,amount as askedCAPTUREDcapture_ok is saved, then the void phase releases the others.kill switch engagednothing is sentPAUSEDThe unknown clock stands still while the switch is on.A capture already at PayPal is still adopted.no OAuth token (401 after refresh,auth unavailable): nothing sentSTALLED: auth_unavailableNo clock starts; the intent stays, nothing was sent.One operator alert after 10 minutes of stall.HTTP 429, after the transporttried 4 times: PayPal refused itSTALLED: rate_limited (PR #1938)No unknown clock starts, so a restart cannot age it into review.Next pass reads the truth first, then repeats the same request id.timeout, network error, 5xx,408, 409, 425: outcome unknownUNKNOWN CLOCK: 10 minutesNext pass: truth first, then the same request id again.After 10 minutes: FAILED_NEEDS_REVIEW. Never the next bidder.PayPal took the capture butlists it PENDINGWAITS FOR PAYPAL: up to 3 daysCOMPLETED: adopted. Declined: the runner-up.Alert after 10 minutes; review only after 3 days.400, request id reused with otherparameters, ceiling out of rangeFAILED_NEEDS_REVIEW (invariant)A bug or a configuration fault: the next bidder would hit it too.refusal that names this hold: declined,expired, voided, already captured, 404,or 422 past the honor period (PR #1947)TRUTH FIRST, THEN THE RUNNER-UPA capture already there is adopted; hold gone or still open:definite fail, then the runner-up at its own price.Truth unreadable: waits. A different capture: review.any other refusal: 403, other 4xx,a 422 issue the code does not knowUNKNOWN CLOCK: 10 minutesMay repeat for every bidder, so never the next bidder:unknown clock starts, review after 10 minutes.waits, nothing is lostmoney takenfalls back to the runner-upa person is needed

Scroll the figure sideways, or open it full size.

Pick an answer, or click a row, to follow its path. Pick it again to show all rows.

Read from packages/payments/src/saga/capture.ts (afterFailure), errorMap.ts, transport.ts and saga/unknown.ts; PR #1938 and PR #1947 are on main. The behaviour is tested against the fake PayPal (test/saga/rateLimited.test.ts) and in the simulated storm lots of S8. PayPal's real rate limits are UNVERIFIED (G1 measurement), and so is the exact honor-period issue name (G3).
Text version of this diagram

The transport retries network errors and the statuses 429, 500, 502, 503 and 504, up to 4 attempts of 10 seconds each, with the same request id; a Retry-After wait is honored up to 30 seconds. What it returns or throws is sorted by the capture phase:

  • 2xx COMPLETED: 2xx, status COMPLETED, amount as asked leads to CAPTURED. capture_ok is saved, then the void phase releases the others.
  • Kill switch: kill switch engaged nothing is sent leads to PAUSED. The unknown clock stands still while the switch is on. A capture already at PayPal is still adopted.
  • No token: no OAuth token (401 after refresh, auth unavailable): nothing sent leads to STALLED: auth_unavailable. No clock starts; the intent stays, nothing was sent. One operator alert after 10 minutes of stall.
  • 429: HTTP 429, after the transport tried 4 times: PayPal refused it leads to STALLED: rate_limited (PR #1938). No unknown clock starts, so a restart cannot age it into review. Next pass reads the truth first, then repeats the same request id.
  • Timeout or 5xx: timeout, network error, 5xx, 408, 409, 425: outcome unknown leads to UNKNOWN CLOCK: 10 minutes. Next pass: truth first, then the same request id again. After 10 minutes: FAILED_NEEDS_REVIEW. Never the next bidder.
  • Pending at PayPal: PayPal took the capture but lists it PENDING leads to WAITS FOR PAYPAL: up to 3 days. COMPLETED: adopted. Declined: the runner-up. Alert after 10 minutes; review only after 3 days.
  • 400 or id conflict: 400, request id reused with other parameters, ceiling out of range leads to FAILED_NEEDS_REVIEW (invariant). A bug or a configuration fault: the next bidder would hit it too.
  • Declined or honor period: refusal that names this hold: declined, expired, voided, already captured, 404, or 422 past the honor period (PR #1947) leads to TRUTH FIRST, THEN THE RUNNER-UP. A capture already there is adopted; hold gone or still open: definite fail, then the runner-up at its own price. Truth unreadable: waits. A different capture: review.
  • 403 or unknown refusal: any other refusal: 403, other 4xx, a 422 issue the code does not know leads to UNKNOWN CLOCK: 10 minutes. May repeat for every bidder, so never the next bidder: unknown clock starts, review after 10 minutes.

The order of the checks

The transport retries first

Network errors and the statuses 429, 500, 502, 503 and 504 are retried inside one request: at most four attempts of ten seconds each, full-jitter backoff from 250 ms up to four seconds, and a Retry-After header is honored up to 30 seconds. Every attempt has the same request id (see idempotency). When the attempts run out the last failure is raised as a typed error, never a raw network error.

The error map labels the answer

Each status gets a kind and an outcome, either definite (PayPal said so) or unknown (the effect cannot be known from here).

The capture phase decides

In this order: kill switch, no token, 429, other unknown outcomes, invariant faults, then refusals that name the hold, and finally every other refusal.

How statuses are labelled

AnswerKindOutcomeWhy
401auth expired or auth faileddefiniteThe request was not accepted at all
404not founddefinitePayPal does not know the resource
400invalid requestdefiniteA bug or a bad configuration
429rate limitedunknown in the map, handled as not run by the sagaSee below
5xxserver errorunknownThe capture may have been taken
408, 409, 425rejectedunknownA timeout or an in-flight conflict proves nothing
422 with a known issuedeclined, expired, already voided, already captureddefiniteNames this hold
422 with an unknown issue, other 4xxrejecteddefiniteNot known to name this hold
network failure, timeoutnetworkunknownNo answer exists

The 422 issue names other than PREVIOUSLY_CAPTURED are UNVERIFIED on the sandbox (gate G1). The saga is built so a wrong name is harmless: it always reads the truth before it falls back.

What changed on 7 Nov 2026

The clocks

Prop

Type

While the kill switch is engaged the unknown clock stands still; the time it held the clock is not counted afterwards.

Why never the next bidder on a maybe

If the first capture did happen and the runner-up is charged as well, two people paid for one lot. That is the one mistake the saga must not make, so every maybe waits or goes to a person.

What is proven

Where this lives in the code

Edit on GitHub

Last updated on