Payment Status & Recovery
Payment Status & Recovery
Every payment involves two identifiers with different lifetimes. Understanding when each one exists is the key to querying payment status reliably — especially when a request times out on your side and you need to work out what actually happened.
Sessions and transactions
A checkout session (sessionId) is the payment intent. It is created by POST /v1/checkout/session and exists before the payer has done anything.
A transaction (transactionId) is a single attempt to collect funds within that session. It does not exist until a payment is initiated — for Hosted Checkout, that is the moment the payer submits a payment method on the checkout page; for direct integrations, it is your POST /v1/payment call. Creating a session, or the payer merely opening the payment link, does not create a transaction.
A session has at most one active transaction at a time — initiating a second payment against a session with a pending or completed transaction returns 409 Conflict. If an attempt fails terminally, a new transaction can be initiated on the same session (or on a fresh session if it has expired).
Getting the transactionId for a session
For Hosted Checkout you never initiate the transaction yourself, so you receive the transactionId in one of three ways:
- Webhooks (recommended) — every transaction event carries
transactionId,sessionId, andmerchantReference. See Webhooks. - Get Checkout Session —
GET /v1/checkout/session?sessionId=...returns the session and, once a payment has been initiated, anactiveTransactionobject with thetransactionIdand current status. - Search Payments — look the transaction up by your own
merchantReference(below).
Searching by merchantReference
GET /v1/payment/search finds transactions by the merchantReference you supplied at session creation:
Both query parameters are required — merchantIds (comma-separated; must be merchants your credentials are scoped to) and merchantReference. The response is an array of transactions in full-state form:
The sessionId the transaction belongs to is returned under identifiers.sessionId. Note that amount here is a decimal in major units (10 = 10.00 KES), unlike session creation which takes minor units.
Search semantics to be aware of:
- Matching is exact and case-sensitive.
ORDER-123will not findorder-123, and there is no partial or prefix matching. - Transactions in any state are returned — pending, in-flight, and terminal alike — and a transaction is searchable immediately after it is initiated; there is no indexing or consistency delay.
- Only transactions are searched, never sessions. A session on which no payment was ever initiated will not appear — see the recovery playbook below for what that means in practice.
- Collections only. Payouts are not returned; use the payout history endpoints for those.
- At most 100 matching transactions are returned.
merchantReference semantics for collections
merchantReference is unique per merchant, as on payouts: creating a session with a reference that has already been used is rejected with 409 Conflict:
The existingSessionId is the most recent session owning the reference — pass it to GET /v1/checkout/session?sessionId=... to recover that session’s state and any activeTransaction. Guidance:
- Use a unique reference per logical order (for example your own order or payment ID). A reference stays used even if its session expires unpaid, so retrying an abandoned order needs a fresh reference.
- Always supply a non-empty reference — the field is required, but an empty string passes validation, is exempt from the uniqueness check, and cannot be found by search at all.
- Prefer alphanumeric characters — some providers strip other characters, which can break downstream reconciliation.
- Sessions created before uniqueness was enforced may share a reference, so Search Payments still returns an array.
Recovering from a timeout
If your request to CrissCross times out, the outcome depends on where in the flow it happened. Work through these steps:
Timed out on session creation? Retry with the same reference
Retry POST /v1/checkout/session with the same merchantReference. The reference is unique per merchant, so the retry is a safe probe:
- 201 — the original request never created a session; you now have a fresh session. Hosted Checkout integrations get a new
paymentLinkto hand to the payer; direct integrations receive nopaymentLink— initiate the payment against the newsessionIdwithPOST /v1/paymentas usual. - 409
DUPLICATE_REFERENCE— the reference is already owned by a session, normally the one your timed-out request created. The response’sexistingSessionIdis the most recent session owning the reference; fetch its state withGET /v1/checkout/session?sessionId=.... If the reference might have been used before — in particular by sessions created before uniqueness was enforced — verify the returned session is the one you expect (check itsamountandexpiresAt) before treating the 409 as confirmation that your session creation succeeded.
(The Idempotency-Key header is not supported on this endpoint — the reference-uniqueness contract above is the recovery mechanism.)
Search for a transaction by merchantReference
If the session was created and the payer may have paid, call GET /v1/payment/search with your merchantReference. A returned transaction gives you the payment status (currentState), the full state history, and the parent session under identifiers.sessionId.
An empty result means no payment was initiated
If search returns [], no payer ever submitted a payment method against the session. There is nothing to reconcile — the money was not collected. If you still hold the sessionId, you can confirm the session’s state (and its expiresAt) via GET /v1/checkout/session?sessionId=....
The hosted checkout paymentLink (and its signature) is returned only in the POST /v1/checkout/session response — it cannot currently be re-fetched from GET /v1/checkout/session or reconstructed from the sessionId, so store it when you receive it. If you lose it, create a new session for the order with a fresh merchantReference — the old reference stays used, so re-creating with the same one returns the 409 above rather than a new link. (Sessions created with integrationType: direct have no paymentLink at all.)