> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polygon.technology/llms.txt
> Use this file to discover all available pages before exploring further.

# Cash-in

> How to let a customer deposit physical cash at a retail location and receive USDC in their wallet.

> [!NOTE]
>
> **Before you start:** the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. [Request access](https://polygon.technology/get-access?utm_source=docs\&utm_medium=card\&utm_campaign=oms_access) before you begin. Cash-in is enabled per project; contact us to turn it on.
>
> Authenticate by exchanging your API key and secret for a bearer token at `POST /auth/token`, then send it as `Authorization: Bearer {accessToken}` on every request. Every `POST` request also requires an `Idempotency-Key` header. See [Get started](https://docs.polygon.technology/payments/get-started.md) for the full flow.

This guide shows you how to issue a cash-in code, let the customer deposit cash at a retail location, and confirm the USDC arrives in their wallet. For how cash-in compares with other ways to fund a wallet, see [Cash-ins and cash-outs](https://docs.polygon.technology/payments/cash-ins-and-cash-outs.md).

Cash-in flow

1
App
→
OMS
POST /cash-ins (source, destination, cash)

2
OMS
→
App
\{depositInstructions: \{cashInCode}, expiresAt}

3
App
→
Customer
Show deposit code + nearby location

4
Customer
→
Retail
Present code, hand over cash

5
Retail
→
OMS
Confirm deposit + actual amount

6
OMS
Create the cashToCrypto transaction

7
OMS
→
App
Webhooks: cashIn.statusChanged, transaction.statusChanged

## Prerequisites

1. **A customer** (`cst_`) that has completed onboarding for your project. If the customer is not yet verified, `POST /cash-ins` returns `403` with `KYCPending` or `KYCRequired`.
2. **A destination wallet**: an OMS wallet (`wlt_`) owned by the customer, or an external wallet given by on-chain address or registered external account (`ext_`).
3. **A webhook endpoint** subscribed to `cashIn.*` and `transaction.statusChanged`, registered with `POST /webhooks` or in the OMS Dashboard. See [Receive webhooks](https://docs.polygon.technology/payments/guides/receiving-webhooks.md).

## Step 1: Find a cash location

Look up retail locations near the customer with `GET /cash-locations`. Pass `flow=cash_in`, the search coordinates, and the cash network identifier your project was enabled with in `provider`. Add `customerId` to return only locations the customer is eligible to use (their KYC country and, in the US, their state).

```bash theme={null}
curl "https://sandbox-api.polygon.technology/v0.13/cash-locations?provider={provider}&flow=cash_in&latitude=39.7392&longitude=-104.9903&radius=10&customerId=cst_01H9Xa..." \
  -H "Authorization: Bearer {accessToken}"
```

Each location has an `id` (`loc_`) and a `cashLocationReference`. Keep both: you pass them on the cash-in as `cash.locationId` and `cash.locationReference`. Only offer locations where `buyAllowed` is `true`. The response also reports the network's `maxTransaction` and `maxDailyPerCustomer` limits; show them to the customer before they set out.

## Step 2: Create the cash-in

```bash theme={null}
curl -X POST https://sandbox-api.polygon.technology/v0.13/cash-ins \
  -H "Authorization: Bearer {accessToken}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ci-order-789" \
  -d '{
    "customerId": "cst_01H9Xa...",
    "source": {
      "asset": "usd",
      "indicatedAmount": "200.00"
    },
    "destination": {
      "asset": "usdc",
      "network": "polygon",
      "wallet": { "id": "wlt_01H9Xb..." }
    },
    "cash": {
      "locationId": "loc_01H9Xd...",
      "locationReference": "R1JFRU5ET1QtMjQzNA=="
    },
    "metadata": { "orderId": "ord_789" }
  }'
```

* **`source`** (required): `asset` is `usd`. `indicatedAmount` is optional: the amount the customer expects to deposit, used for the estimate in `pricing`. The final amount is whatever cash the customer hands over.
* **`destination`** (required): the `asset` and `network` to deliver, plus exactly one way to identify the wallet: `wallet.id` (an OMS wallet), `wallet.blockchainAddress`, or `wallet.externalAccount` (an `ext_` ID). An OMS wallet must belong to the customer and hold `destination.asset` on `destination.network`; otherwise the request returns `422` with `DestinationWalletMismatch`.
* **`cash`** (required): `locationId` and `locationReference` from step 1.
* **`sponsorGas`** (optional): only `true` is supported; OMS covers the on-chain gas for delivery.

### Response, `201 Created`

```json theme={null}
{
  "id": "ci_01H9Xy...",
  "status": "pending",
  "customer": { "id": "cst_01H9Xa..." },
  "source": {
    "type": "cash",
    "asset": "usd",
    "details": {
      "locationId": "loc_01H9Xd...",
      "locationName": "Main Street Pharmacy #4521",
      "locationAddress": {
        "line1": "123 Main St",
        "city": "Denver",
        "state": "CO",
        "zipCode": "80202",
        "country": "US"
      }
    }
  },
  "destination": {
    "type": "walletCrypto",
    "id": "wlt_01H9Xb...",
    "asset": "usdc",
    "network": "polygon",
    "details": { "address": "0x7b3a9f2c4d1ea8bf6390ce5d2b7fa104c8e3d9b1" }
  },
  "depositInstructions": { "cashInCode": "483291" },
  "pricing": {
    "source": { "asset": "usd", "amount": "200.00" },
    "destination": { "asset": "usdc", "amount": "198.000000" },
    "fees": [
      { "type": "oms", "side": "source", "asset": "usd", "amount": "2.00" }
    ],
    "exchangeRate": "1.0000",
    "fixedAmountSide": "source",
    "sponsorGas": true
  },
  "metadata": { "orderId": "ord_789" },
  "expiresAt": "2026-03-16T19:30:00.000Z",
  "createdAt": "2026-03-16T18:30:00.000Z",
  "updatedAt": "2026-03-16T18:30:00.000Z"
}
```

Show the customer:

* `depositInstructions.cashInCode`: the code they present at the counter.
* `source.details.locationName` and `source.details.locationAddress`: where to go.
* `expiresAt`: the deadline to deposit.
* The estimate in `pricing`: `pricing.source.amount` is the cash amount, `pricing.destination.amount` is what reaches the wallet, and `pricing.fees` lists every fee. Until the deposit, these are based on `indicatedAmount`.

`transactionId` is absent until the deposit is confirmed. OMS sends `cashIn.created` when the code is issued.

## Step 3: The customer deposits cash

The customer presents the code at the location and hands over the cash. This happens outside OMS. When the location confirms the deposit, the cash-in moves to `processing` and OMS sends `cashIn.statusChanged`. In sandbox, simulate this step with the [sandbox endpoints](#test-in-sandbox).

If the customer needs a new code before `expiresAt` (for example, a lost or mistyped code), [refresh it](#refresh-the-code).

## Step 4: Confirm completion

OMS recalculates `pricing` on the cash actually deposited, converts it, and delivers the crypto. The cash-in moves to `completed`, `completedAt` and `transactionId` are set, and OMS sends `cashIn.statusChanged`. Read the final state with `GET /cash-ins/{cashInId}`:

```json theme={null}
{
  "id": "ci_01H9Xy...",
  "status": "completed",
  "customer": { "id": "cst_01H9Xa..." },
  "depositInstructions": { "cashInCode": "483291" },
  "pricing": {
    "source": { "asset": "usd", "amount": "200.00" },
    "destination": { "asset": "usdc", "amount": "198.000000" },
    "fees": [
      { "type": "oms", "side": "source", "asset": "usd", "amount": "2.00" }
    ],
    "exchangeRate": "1.0000",
    "fixedAmountSide": "source",
    "sponsorGas": true
  },
  "transactionId": "txn_01H9Xz...",
  "metadata": { "orderId": "ord_789" },
  "expiresAt": "2026-03-16T19:30:00.000Z",
  "createdAt": "2026-03-16T18:30:00.000Z",
  "updatedAt": "2026-03-16T18:47:00.000Z",
  "completedAt": "2026-03-16T18:47:00.000Z"
}
```

`source` and `destination` are unchanged from step 2 and omitted here.

### The cash-in's transaction

The deposit creates a transaction (`transactionId`) whose `precursor` is `{ "type": "cashIn", "id": "ci_..." }`. Its `source` is a `cash` instrument and its `destination` is the wallet. OMS sends `transaction.statusChanged` on each change, with the transaction in `data.resource`:

```json theme={null}
{
  "id": "txn_01H9Xz...",
  "status": "completed",
  "subStatus": "completed.settled",
  "customer": { "id": "cst_01H9Xa..." },
  "precursor": { "type": "cashIn", "id": "ci_01H9Xy..." },
  "source": {
    "type": "cash",
    "asset": "usd",
    "details": { "locationId": "loc_01H9Xd...", "locationName": "Main Street Pharmacy #4521" }
  },
  "destination": {
    "type": "walletCrypto",
    "id": "wlt_01H9Xb...",
    "asset": "usdc",
    "network": "polygon",
    "details": { "address": "0x7b3a9f2c4d1ea8bf6390ce5d2b7fa104c8e3d9b1" }
  },
  "pricing": {
    "source": { "asset": "usd", "amount": "200.00" },
    "destination": { "asset": "usdc", "amount": "198.000000" },
    "fees": [
      { "type": "oms", "side": "source", "asset": "usd", "amount": "2.00" }
    ],
    "exchangeRate": "1.0000",
    "fixedAmountSide": "source",
    "sponsorGas": true
  },
  "transfers": [
    { "id": "trf_01H9Xp...", "role": "payout", "rail": "blockchain", "attempt": 1 }
  ],
  "metadata": { "orderId": "ord_789" },
  "createdAt": "2026-03-16T18:47:05.000Z",
  "updatedAt": "2026-03-16T18:48:10.000Z"
}
```

While delivery is underway the transaction is `processing` (for example `processing.fundsPulled` once the cash is collected). For the on-chain hash, fetch the payout transfer with `GET /transfers/{transferId}` and read `tracking.txHash`.

## Refresh the code

To replace the code before it expires, call refresh. The response is the full cash-in with a new `depositInstructions.cashInCode`; the old code stops working.

```bash theme={null}
curl -X POST https://sandbox-api.polygon.technology/v0.13/cash-ins/ci_01H9Xy.../refresh \
  -H "Authorization: Bearer {accessToken}" \
  -H "Idempotency-Key: ci-refresh-789-1"
```

Refresh does not extend `expiresAt`. OMS sends `cashIn.updated` with `changedFields: ["depositInstructions"]`.

| Response | Meaning |
| - | - |
| `422` `RefreshExpired` | `expiresAt` has passed. Create a new cash-in. |
| `429` `RefreshTooFrequent` | Refreshed too recently. Wait for the `Retry-After` header. |
| `429` `RefreshLimitReached` | This cash-in has used all its refreshes. Create a new cash-in. |

## Expiry and failures

* **The code expires unused.** The cash-in moves to `expired` and OMS sends `cashIn.statusChanged`. No transaction is created and no money moved. Create a new cash-in if the customer still wants to deposit.
* **The cash-in fails.** The cash-in moves to `failed` with a `failure` object (`code`, `message`, `retryable`).
* **Delivery fails after the deposit.** The cash-in's transaction moves to `failed` (for example `failed.outboundFailed`) and its `failure` says why. OMS sends `transaction.statusChanged`.
* **Review.** A transaction can be held for review. It stays `processing` with `subStatus` `processing.underReview`, then continues to `completed` or `failed`. You do not need to act, and you should not surface the review to the customer.

Each cash-in is single use. Once it completes, fails, or expires, create a new one for the next deposit.

## Track cash-ins

Read one cash-in with `GET /cash-ins/{cashInId}`, or list them with `GET /cash-ins`, filtered by `customerId`, `status`, `createdAfter`, `createdBefore`, or `search`. Results are newest first.

| Event | When |
| - | - |
| `cashIn.created` | The code was issued (`status: pending`) |
| `cashIn.statusChanged` | The cash-in moved to `processing`, `completed`, `failed`, or `expired`; `data.previousStatus` has the old status |
| `cashIn.updated` | The code was refreshed |
| `transaction.statusChanged` | The cash-in's transaction changed status or sub-status |

Each event carries the resource under `data.resource`. See [Webhook events](https://docs.polygon.technology/api-reference/webhook-events.md) for the envelope.

## Test in sandbox

In sandbox, three endpoints stand in for the retail counter. Each takes the current `cashInCode` and the cash `amount`. They return `404` outside sandbox.

1. **Present the code:** `POST /cash-ins/{cashInId}/simulate/present-code` with `{ "cashInCode", "amount" }`. The response includes a `confirmationId`.
2. **Hand over the cash:** `POST /cash-ins/{cashInId}/simulate/deposit-cash` with `{ "cashInCode", "amount", "originalConfirmationId" }`, passing the `confirmationId` from step 1. The cash-in proceeds to `processing` and then `completed`.
3. **Or cancel instead:** `POST /cash-ins/{cashInId}/simulate/cancel` with the same body as `deposit-cash` to cancel the authorization before the cash is handed over.

```bash theme={null}
curl -X POST https://sandbox-api.polygon.technology/v0.13/cash-ins/ci_01H9Xy.../simulate/present-code \
  -H "Authorization: Bearer {accessToken}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sim-present-789" \
  -d '{ "cashInCode": "483291", "amount": "200.00" }'

curl -X POST https://sandbox-api.polygon.technology/v0.13/cash-ins/ci_01H9Xy.../simulate/deposit-cash \
  -H "Authorization: Bearer {accessToken}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sim-deposit-789" \
  -d '{ "cashInCode": "483291", "amount": "200.00", "originalConfirmationId": "{confirmationId}" }'
```

## Key points

* **Find the location first.** `GET /cash-locations?flow=cash_in` gives the `id` and `cashLocationReference` the cash-in needs, plus the network's limits.
* **The amount is set at the counter.** `indicatedAmount` only drives the estimate; `pricing` is recalculated on the cash actually deposited.
* **One code, one deadline.** Refresh replaces the code but never extends `expiresAt`.
* **Two resources to follow.** The cash-in (`cashIn.*` events) and the transaction it creates (`transaction.statusChanged`).
* **The on-chain hash is on the transfer.** Read `tracking.txHash` from the transaction's payout transfer.
