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:

  1. Webhooks (recommended) — every transaction event carries transactionId, sessionId, and merchantReference. See Webhooks.
  2. Get Checkout SessionGET /v1/checkout/session?sessionId=... returns the session and, once a payment has been initiated, an activeTransaction object with the transactionId and current status.
  3. 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:

curl --request GET 'https://api.crisscross.money/v1/payment/search?merchantIds=YOUR_MERCHANT_ID&merchantReference=ORDER-2026-001' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'

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:

[
{
"transactionId": "01a02352-ac7b-799f-9bd0-79ba982c24f8",
"merchantReference": "ORDER-2026-001",
"currentState": "AUTH_REQUIRED",
"previousStates": ["RECEIVED", "PENDING", "ROUTED", "AUTH_REQUIRED"],
"processor": "Example Processor",
"merchantName": "Example Merchant",
"amount": 10,
"currency": "KES",
"transactionType": "PAYMENT",
"paymentMethodId": "mobilemoney",
"transactionStates": [ ... ],
"identifiers": {
"sessionId": "01a02352-53b8-7000-82b3-380f374c7219",
"payerId": "01994e10-78b0-7aa8-bf9a-80a9d571706c"
},
"paymentAttributes": {}
}
]

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-123 will not find order-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:

{
"code": "DUPLICATE_REFERENCE",
"message": "A session with this merchantReference already exists for this merchant. Use a new reference per order.",
"existingSessionId": "01a02352-53b8-7000-82b3-380f374c7219"
}

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:

1

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 paymentLink to hand to the payer; direct integrations receive no paymentLink — initiate the payment against the new sessionId with POST /v1/payment as usual.
  • 409 DUPLICATE_REFERENCE — the reference is already owned by a session, normally the one your timed-out request created. The response’s existingSessionId is the most recent session owning the reference; fetch its state with GET /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 its amount and expiresAt) 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.)

2

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.

3

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=....

Payment links are returned once

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.)