> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.crisscross.money/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.crisscross.money/_mcp/server.

# Payment Status & Recovery

> How sessions relate to transactions, how to look payments up by merchantReference, and how to recover after a timeout.

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.

```mermaid
sequenceDiagram
    participant Merchant
    participant CrissCross
    participant Payer

    Merchant->>CrissCross: POST /v1/checkout/session
    CrissCross->>Merchant: sessionId + paymentLink (no transaction yet)
    Merchant->>Payer: Redirect to paymentLink
    Payer->>CrissCross: Submits payment method
    Note over CrissCross: transactionId is created here
    CrissCross->>Merchant: Webhook with transactionId + sessionId
```

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](/webhooks).
2. **Get Checkout Session** — `GET /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:

```bash
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:

```json
[
  {
    "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](/payout-tracking) for those.
* At most 100 matching transactions are returned.

### merchantReference semantics for collections

`merchantReference` is **unique per merchant**, as on [payouts](/payout-single#idempotency): creating a session with a reference that has already been used is rejected with `409 Conflict`:

```json
{
  "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:

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

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

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