Skip to main content
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 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 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.
Cash-in flow
1App→OMSPOST /cash-ins (source, destination, cash)
2OMS→App{depositInstructions: {cashInCode}, expiresAt}
3App→CustomerShow deposit code + nearby location
4Customer→RetailPresent code, hand over cash
5Retail→OMSConfirm deposit + actual amount
6OMSCreate the cashToCrypto transaction
7OMS→AppWebhooks: 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.

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

  • 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

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. If the customer needs a new code before expiresAt (for example, a lost or mistyped code), refresh it.

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}:
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:
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.
Refresh does not extend expiresAt. OMS sends cashIn.updated with changedFields: ["depositInstructions"].

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. Each event carries the resource under data.resource. See Webhook events 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.

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.