Skip to content

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_before passes 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_within by how reliable the machine's network is: the machine must get «dispensed» to your backend before capture_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 failed intent may be paid again until expires_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).