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

# Your First Payment

> Step-by-step walkthrough of a first successful sandbox payment on CrissCross — token, checkout session, payment initiation, and polling to a completed state.

> **Info**
>
> **Relevant if you use Collect** to take payments from customers. If you only send payouts or use Exchange, you don't need this subsection. See [Which parts you need](/testing-overview#which-parts-you-need).

This is the one test to run before any other. It takes a single mobile money payment all the way to `COMPLETED` and proves four things at once: your credentials work, you can reach the API, your request shape is accepted, and you can read a terminal state back.

Everything afterwards is a variation on these calls. Get this working first — a failure here is almost always credentials or request shape, and is far easier to diagnose on the happy path than inside a scenario you are also trying to trigger.

The example uses Kenya as the country and M-Pesa as the mobile network operator, which appear in the requests as currency `KES` and provider `mpesa`. Substitute your own market from the [market reference](/payment-method-mobile-money#supported-countries) if you prefer; the steps do not change.

> **Note**
>
> This page is a test procedure. For field-by-field reference — every optional field, every response shape, the full operator list — see [Direct API: Mobile Money](/direct-api-integration-mobile-money).

### Step 1 — Get an access token

`POST /v1/auth/oauth2/token` — [API reference](/api-reference/authentication/request-access-token)

```bash
curl -X POST 'https://api.crisscross.money/v1/auth/oauth2/token' \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "client_id": "YOUR_SANDBOX_CLIENT_ID",
    "client_secret": "YOUR_SANDBOX_CLIENT_SECRET"
  }'
```

```json
{
  "access_token": "eyJhbGci...",
  "token_type": "Bearer",
  "expires_in": 86400
}
```

**Pass:** a `200` with an `access_token`. Tokens last 24 hours and there is no refresh token — request a new one when it expires.

**If this fails:** a `401` with `{"error": "Invalid client credentials"}` means the `client_id` or `client_secret` is wrong, and the two are deliberately indistinguishable so you cannot tell which. A `400` usually means the body was form-encoded; it must be JSON. See [Authentication](/authentication).

### Step 2 — Create a checkout session

`POST /v1/checkout/session` — [API reference](/api-reference/collect/payments/payment-initiation/create-checkout-session)

```bash
curl -X POST 'https://api.crisscross.money/v1/checkout/session' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "merchantId": "YOUR_SANDBOX_MERCHANT_ID",
    "merchantReference": "SANDBOX-FIRST-TEST-001",
    "amount": 250000,
    "currency": "KES",
    "integrationType": "direct",
    "payerDetails": {
      "emailAddress": "test.payer@example.com",
      "fullName": "Test Payer",
      "location": "KEN",
      "phoneNumber": "+254712345678"
    }
  }'
```

```json
{
  "sessionId": "0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a",
  "payerId": "2a1c4f1a-0d44-4f16-8c8e-9a3b4c5d6e7f"
}
```

**Pass:** a `201` with a `sessionId`. Keep it — the next call needs it.

Two things worth getting right the first time, because they cause most early failures:

* **`amount` is in minor units.** `250000` KES is KSh 2,500.00, but `UGX`, `XAF` and `XOF` have no minor unit, so the same `250000` is 250,000 whole units.
* **`payerDetails.location` is ISO 3166 alpha-3**: `KEN`, not `KE`. It decides which operators are valid and normalises the mobile number.

### Step 3 — Initiate the payment

`POST /v1/payment` — [API reference](/api-reference/collect/payments/payment-initiation/initiate-transaction)

```bash
curl -X POST 'https://api.crisscross.money/v1/payment' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "sessionId": "0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a",
    "paymentMethodId": "mobilemoney",
    "paymentDetails": {
      "type": "mobilemoney",
      "provider": "mpesa",
      "payerMobileNumber": "0712345678",
      "payerFullName": "Test Payer"
    }
  }'
```

```json
{
  "transactionId": "9f3e4b2c-1a6d-4e88-9d3a-ff1234567890",
  "sessionId": "0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a",
  "merchantReference": "SANDBOX-FIRST-TEST-001",
  "paymentMethodId": "mobilemoney",
  "status": "PENDING",
  "message": "Transaction pending"
}
```

**Pass:** a `transactionId` and a `status` of `PENDING`.

`0712345678` is the number from step 2, written in local format. Local format and E.164 both work, and the trigger numbers use local format too.

It isn't a [trigger number](/testing-trigger-numbers), so this payment will succeed. That's what you want from a first test.

**If this fails:** check that `paymentMethodId` is `"mobilemoney"`; the operator goes in `paymentDetails.provider`. A `409 Conflict` means the session already has an active transaction, so create a new session.

### Step 4 — Poll to a terminal state

`GET /v1/payment/{transactionId}` — [API reference](/api-reference/collect/payments/manage-payments/get-transaction)

```bash
curl -G 'https://api.crisscross.money/v1/payment/9f3e4b2c-1a6d-4e88-9d3a-ff1234567890' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

The first response is usually `AUTH_REQUIRED`. For a real customer, this is while the prompt is on their phone:

```json
{
  "transactionId": "9f3e4b2c-1a6d-4e88-9d3a-ff1234567890",
  "status": "AUTH_REQUIRED",
  "message": "Transaction pending",
  "authState": { "authMethodType": "pendingApproval" }
}
```

Keep polling. The status stays `AUTH_REQUIRED` for about 10 seconds while CrissCross checks the payment with the operator. Poll every 2 seconds for the first 30 seconds, then every 5–10 seconds, and stop after about 3 minutes. Once it completes:

```json
{
  "transactionId": "9f3e4b2c-1a6d-4e88-9d3a-ff1234567890",
  "status": "COMPLETED",
  "message": "Transaction completed",
  "processorReference": "PROC-9f3e4b2c",
  "authState": {
    "state": "completed",
    "transitionedAt": "2026-06-02T10:30:44Z",
    "message": "Transaction completed"
  }
}
```

**Pass:** `status` reaches `COMPLETED` and a `processorReference` is present.

`AUTH_REQUIRED` is the normal waiting state for mobile money, not an error. Show it in your UI as "check your phone".

Stop polling at `COMPLETED`, `FAILED`, `CANCELLED` or `EXPIRED`. For `ERRORED`, check `authState.canPoll`: if it's `true`, only the status check failed and the payment may still complete, so keep polling. Otherwise, stop.

### What you have just proved

|                |                                                                              |
| -------------- | ---------------------------------------------------------------------------- |
| Credentials    | Your sandbox `client_id` and `client_secret` issue a working token           |
| Connectivity   | Nothing between you and `api.crisscross.money` is blocking the calls         |
| Request shape  | Session and payment bodies are accepted as written                           |
| State handling | You can read a transaction through `PENDING` → `AUTH_REQUIRED` → `COMPLETED` |

You've now proved the happy path. The rest of this section covers failures and the other approval flows.

### If you are using Hosted Checkout

Steps 1 and 2 are the same, except you set `integrationType` to `"hosted"` on the session. The response includes a `paymentLink`. Open it and pay on the hosted page instead of calling `POST /v1/payment` in step 3.

Because you didn't create the payment yourself, you don't have its `transactionId`. Get it from the session once the payment has started:

`GET /v1/checkout/session` — [API reference](/api-reference/collect/payments/payment-initiation/get-checkout-session)

```bash
curl -G 'https://api.crisscross.money/v1/checkout/session' \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --data-urlencode 'sessionId=0d3f1d6c-1c49-4b89-9db6-1fdc06f24c8a'
```

Take `activeTransaction.transactionId` from the response and poll it as in step 4. In production, you'd use your webhook for the final status instead of polling.

### Next

* [Set up a webhook endpoint](/core-concepts-webhooks#creating-a-webhook-subscription) and re-run this test. Webhooks are the source of truth for a payment's final status; polling just keeps your UI up to date. See [Testing Webhooks](/core-concepts-webhooks#testing-webhooks) for how to inspect and resend deliveries.
* Work through the approval flow your markets use. Most mobile money operators use the handset prompt you just saw, but some use an in-checkout OTP and some redirect the customer to the operator's page.
* Then vary the number to produce failures, using the [trigger tables](/testing-trigger-numbers).