Payment lifecycle¶
A payment intent (
pi_…) is one request for payment and its state. This page: every state, who triggers each transition, which webhook arrives, and the windows of holds and captures.
1. State diagram¶
create — POST /payment-intents
│
▼
┌──────────▶ requires_confirmation ── expires_at passed ────────────────▶ expired
│ │ │ (claimless: claim → the person is known; a claim not confirmed in 90 s lapses)
│ │ └────────── cancel (you or the person) ──────▶ canceled
│ confirm (the person: account + Face ID)
│ ▼
│ processing live: a ledger call is in flight — never charge again, read again
│ ┌─────────┼──────────────────────────┐
│ ledger automatic: manual:
│ refused hold + capture hold
│ ▼ ▼ ▼
└──── failed succeeded ◀── capture ── requires_capture ── cancel ──▶ canceled (the hold goes back)
confirm again, │ (you) │
or expired / │ capture_before passed ──────────▶ expired (authorization_expired)
canceled ▼ refund (you, 90 days)
partially_refunded ── refund ──▶ refunded
In test mode processing is skipped: confirm resolves at once by the amount — …01 → failed (test_declined),
…02 → canceled, manual → requires_capture, anything else → succeeded.
2. Each state¶
| State | Meaning | Next | Final? |
|---|---|---|---|
requires_confirmation |
Created, not paid. A claimless one has nobody until claimed (user_ref: null) |
processing, canceled, expired (test: the outcome at once) |
— |
processing |
Live: Gerege is talking to the ledger (core banking) — a hold or a capture. An answer may be 202 |
requires_capture, succeeded, failed, expired |
— |
requires_capture |
Manual: the money is held on the person's account until capture_before |
succeeded (capture), canceled (cancel), expired |
— |
succeeded |
Paid; the money is on your payout account or in clearing | partially_refunded, refunded |
almost (refunds only) |
partially_refunded |
Partly refunded (refunded_amount < amount) |
refunded |
almost |
refunded |
Fully refunded | — | ✓ |
failed |
The ledger refused (failure_code: insufficient_funds, limit_exceeded, test_declined, …). Still payable — the person tries another account or again |
processing, canceled, expired |
— |
canceled |
You or the person canceled; for manual, the hold was released (not a refund, no fee) | — | ✓ |
expired |
Time ran out: not paid (expires_at) or not captured (capture_before, failure_code: "authorization_expired") |
— | ✓ |
When the person's chosen account cannot pay (account_unavailable) the intent does not become failed — it quietly
returns to requires_confirmation (the person picked the wrong account; the payment did not fail), with no webhook.
3. Transitions and webhooks¶
| Transition | Who | Webhook |
|---|---|---|
| creation | your backend | — |
| claim (claimless) | the person (Gerege App) | payment_intent.claimed |
→ requires_capture |
the person confirms (manual) | payment_intent.requires_capture |
→ succeeded |
the person confirms (automatic) or you capture (manual) | payment_intent.succeeded |
→ failed |
the ledger | payment_intent.failed |
→ canceled |
you (POST …/cancel) or the person |
payment_intent.canceled |
→ expired |
the platform (a job) | claimless: payment_intent.expired; app: payment_intent.canceled + status: "expired" (OMAS compatibility) |
| refund | you (POST …/refunds) |
refund.succeeded · refund.failed |
Webhooks are at least once and unordered: deduplicate on id, decide on status, and read again with
GET /payment-intents/{id} before an important step. Every event's schema: Webhooks.
4. Holds and capture¶
Every Gerege payment goes through a hold: on confirm the money is locked on the person's Gerege Wallet account, then captured (taken) or released (given back).
capture_method |
What happens | When |
|---|---|---|
automatic (default) |
confirm → hold → capture at once → succeeded |
goods or service handed over at once (online shop, till) |
manual |
confirm → hold → requires_capture; you capture or cancel |
take the money only after handing over (vending, preparing an order, rental) |
Why vending uses manual. A machine learns whether it could dispense only after the payment (jammed, empty, the network dropped). With automatic capture every failed dispense needs a refund — the person's money moves for minutes and a refund entry stays behind. With manual:
- dispensed →
capture→succeeded; - not dispensed →
cancel→ the hold is released at once — the money never left the person's account, it is not a refund; - partly dispensed → full
capture+ a refund of the rest (partial capture by amount — «удахгүй»); - the machine never answers → when
capture_beforepasses the hold is released to the person by itself (payment_intent.expired).
5. Windows¶
| Window | Meaning | Limits | Default |
|---|---|---|---|
expires_in → expires_at |
time for the person to pay (confirm) | 60–1800 s | 600 s |
| claim lapse | an unconfirmed claim passes to someone else | 90 s (fixed) | — |
capture_within → capture_before |
manual: time from confirm to capture | 60–604,800 s (7 days) | 604,800 s |
| the hold's safety margin | the ledger hold's own expiry = capture_before + half the window (≥ 1 min, ≤ 15 min); automatic: expires_at + 15 min |
— | — |
| refund window | how long after the payment a refund is possible | 90 days | — |
- Choose
capture_withinby how reliable the machine's network is: the machine must get «dispensed» to your backend beforecapture_before. After it a capture is impossible (409 expired) — the goods are gone and the money was not taken. 60–300 s is usual for vending. - The safety margin is only for when the platform could not release the hold itself (a network fault, an outage) and
the ledger releases it on its own; normally the job releases it at
capture_before. The person's money is never locked without an end. - A
failedintent may be paid again untilexpires_at.
6. processing and retries¶
When the ledger's answer is unknown (a network fault, a timeout) the intent stays processing and the answer may be
202 Accepted. This is not an error: the call is recorded on the intent with its key and the platform asks again
and finishes it. You:
- never create a new intent to «charge again»;
- wait for the final state with
GET /payment-intents/{id}(or the webhook); - may repeat capture, cancel and refund with the same request — they are idempotent (Idempotency).