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

# Server Integration

> Capture an Apple Pay or Google Pay token in the browser and submit it to the CrissCross API from your own server, for full control of the checkout flow.

### Overview

If you want full control of the payment flow — your own retry logic, your own transaction bookkeeping, your own 3-D Secure presentation — you can submit the wallet token to the CrissCross API yourself instead of letting the SDK do it.

You still use the [wallets SDK](/wallets-react-integration) to render the button and obtain the token. Only the wallet running on the customer's device can produce one, and it cannot be produced server-side. What changes is that the SDK hands the token to you rather than submitting it.

> **Warning**
>
> This is the most involved wallet integration and it carries the most ways to get a payment stuck. Unless you need control that [React Integration](/wallets-react-integration) does not give you, use that instead — it makes the same calls described here.

### Pre-requisites

* Apple Pay and Google Pay enabled on your account.
* Your payment domains registered and verified. See [Wallet Configuration](/wallets-configuration).
* A checkout session created with `integrationType: "direct"`.

### The flow

#### Render the button and capture the token

Initialise the SDK in `manual` mode, which suppresses submission and hands you the token — and, for Apple Pay, makes the merchant validation yours to route:

```jsx
<CrissCrossWallets sessionId={sessionId} mode="manual">
  <ApplePayButton
    // Apple Pay only. Return the merchantSession from step 2, unaltered.
    onValidateMerchant={({ validationUrl }) =>
      fetch('/api/wallet-merchant-session', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ validationUrl }),
      }).then((response) => response.json())
    }
    onToken={({ provider, token, deviceFingerprint }) =>
      postToYourServer({ provider, token, deviceFingerprint })
    }
  />
  <GooglePayButton
    // No onValidateMerchant — Google Pay has no merchant validation step.
    onToken={({ provider, token, deviceFingerprint }) =>
      postToYourServer({ provider, token, deviceFingerprint })
    }
  />
</CrissCrossWallets>
```

`onValidateMerchant` must resolve with the `merchantSession` object exactly as CrissCross returned it — the wallet sheet will not release a token until it does. Step 2 covers the endpoint behind `/api/wallet-merchant-session`.

`token` is already base64-encoded and safe to transport as a string. `deviceFingerprint` is required for fraud screening — pass it through unaltered.

#### Validate the merchant session (Apple Pay only)

Apple requires a server-side merchant validation before the wallet sheet will release a token. In `manual` mode this is yours to make.

`onValidateMerchant` gives you a `validationUrl`. Forward it to CrissCross from your server — never from the browser, since this call uses your OAuth credentials:

```bash
curl -X POST https://api.crisscross.money/v1/payment/wallet/merchant-session \
     -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
  "sessionId": "01951c8a-7c3d-7e1f-9d4a-2b3c4d5e6f70",
  "provider": "apple-pay",
  "validationUrl": "https://apple-pay-gateway.apple.com/paymentservices/paymentSession"
}'
```

Response:

```json
{
  "merchantSession": { }
}
```

Return `merchantSession` to the browser **unaltered**, as JSON. Do not re-serialise its fields, reorder them, or log it — Apple validates it as an opaque whole and it is short-lived.

Note what you do *not* send: no merchant identifier, no domain, no certificate. CrissCross resolves all of it from `sessionId`. Google Pay has no equivalent step.

Pass `validationUrl` through exactly as the wallet issued it. CrissCross accepts it only if it is an HTTPS URL on an Apple Pay validation host and rejects anything else with `422`, so a rewritten or constructed URL will fail rather than be requested.

#### Submit the payment

```bash
curl -X POST https://api.crisscross.money/v1/payment \
     -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
  "paymentMethodId": "digitalwallet",
  "sessionId": "01951c8a-7c3d-7e1f-9d4a-2b3c4d5e6f70",
  "paymentDetails": {
    "type": "digitalwallet",
    "provider": "apple-pay",
    "encryptedWalletToken": "eyJ2ZXJzaW9uIjoiRUNfdjEiLCJkYXRhIjoi...",
    "deviceFingerprint": "eyJmcCI6IjhkM2YxZTJhOWM0YjdkNmU...",
    "payerEmail": "siyanda.mthembu@example.com"
  }
}'
```

| Field                                 | Required | Notes                                                                                               |
| ------------------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `paymentMethodId`                     | yes      | Always the literal string `"digitalwallet"`. The wallet goes in `paymentDetails.provider`, not here |
| `paymentDetails.provider`             | yes      | `apple-pay` or `google-pay`                                                                         |
| `paymentDetails.encryptedWalletToken` | yes      | The base64 token from the SDK, passed through verbatim                                              |
| `paymentDetails.deviceFingerprint`    | yes      | From the SDK. Omitting it will cause fraud screening to reject the payment                          |
| `paymentDetails.payerEmail`           | yes      | The payer's email address                                                                           |

#### Handle the response

```json
{
  "transactionId": "9f3e4b2c-1a6d-4e88-9d3a-ff1234567890",
  "status": "SUCCESSFUL",
  "message": "Payment authorised"
}
```

A payment needing 3-D Secure returns `PENDING` with an `authState`:

```json
{
  "transactionId": "9f3e4b2c-1a6d-4e88-9d3a-ff1234567890",
  "status": "PENDING",
  "message": "Authorization required",
  "authState": {
    "type": "redirect",
    "redirectUrl": "https://secure.crisscross.money/3ds/9f3e4b2c"
  }
}
```

Send the customer to `redirectUrl`. They return to the session's `redirectUrl` when the challenge resolves. Most wallet payments are exempt from 3-D Secure, because the device authentication already satisfies strong customer authentication — so treat this as a path you must support, not the common case.

#### Confirm on the webhook

The webhook is the authoritative outcome. Match it on `sessionId` or `merchantReference`, verify its signature, and only then fulfil. See [Webhook Events](/webhook-events).

### Things that will bite you

* **A wallet token is single-use and short-lived.** You cannot store it, retry with it after a decline, or submit it twice. A retry means a new tap on the wallet button and a new token.
* **The merchant session is also single-use.** Fetch a fresh one per wallet sheet; do not cache it.
* **Never log tokens or merchant sessions.** They are payment credentials for as long as they are valid.
* **Do not transform the token.** It arrives base64-encoded from the SDK and must reach CrissCross exactly as it left the wallet.

### Additional resources

* **[React Integration](/wallets-react-integration)** — the same flow with these calls handled for you.
* **[Wallet Configuration](/wallets-configuration)** — registering your payment domains.
* **[Digital Wallets](/payment-method-digital-wallets)** — supported workflows, refunds and reconciliation.
* **[Full and Partial Refunds](/full-and-partial-refunds)** — refunding a wallet payment.