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.
This guide shows you how to move USDC out of an OMS custodial wallet using the two-step quote and transaction flow. The crypto-to-crypto path described first is fully supported today. Paying out to a bank account is covered at the end and is early access.
Send from a wallet flow
1AppOMSPOST /quotes (USDC/polygon → USDC/polygon)
2OMSAppQuote with locked pricing
3AppOMSPOST /transactions {quoteId}
4OMSPolygonPull USDC and send onchain
5OMSAppWebhook: cryptoToCrypto.processing
6OMSAppWebhook: cryptoToCrypto.completed

Prerequisites

Before sending USDC from a wallet, you need:
  1. A customer with a cst_ ID.
  2. A funded OMS wallet with a wlt_ ID containing enough USDC to cover the source amount plus gas (if not sponsoring gas).
  3. A destination: another OMS wallet (its wlt_ ID) or an external onchain address (a 0x... address on the same network).
  4. Webhook subscriptions configured in the OMS Dashboard for transaction.cryptoToCrypto.* events.

Step 1: Create a quote

Create a quote to lock in the pricing. OMS infers the transaction type as cryptoToCrypto from the source and destination asset pair (USDC to USDC). The quote source must be an OMS crypto wallet: a walletId, asset, and network. The destination describes where the funds land. To send to an external onchain address, set destination.wallet.blockchainAddress. To send to another OMS wallet, set destination.wallet.id instead.

Request

POST /v0.9/quotes
Authorization: Bearer {api_key}
Idempotency-Key: qt-alice-usdc-send-001
Content-Type: application/json
{
  "customerId": "cst_01H9Xa...",
  "source": {
    "walletId": "wlt_01H9Xb...",
    "asset": "usdc",
    "network": "polygon",
    "amount": "2000.00"
  },
  "destination": {
    "wallet": {
      "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456"
    },
    "asset": "usdc",
    "network": "polygon"
  },
  "sponsorGas": true
}
Required fields:
  • customerId: The customer making this transaction.
  • source.walletId: The OMS custodial wallet holding the USDC to send (string, wlt_ prefix).
  • source.asset: The crypto asset to send (usdc or usdt).
  • source.network: The blockchain the wallet is on (polygon or ethereum).
  • source.amount or destination.amount: Exactly one is required. Here we specify the source amount (send 2,000 USDC). OMS calculates the destination amount after fees. You could alternatively specify destination.amount to target an exact received amount.
  • destination.wallet: Where the funds land. Provide one of id (another OMS wallet), blockchainAddress (an external onchain address), or externalAccount (a saved bank account, early access). This example uses blockchainAddress.
  • destination.asset: usdc.
  • destination.network: The network the funds are delivered on. For a crypto-to-crypto send this matches the source network (polygon).
Optional fields:
  • sponsorGas: When true, the developer absorbs gas fees. The customer sees fees on the source side as zero and the actual gas cost appears in sponsorGasCost.
  • developerFees: Array of fee entries. Omitted here, so no developer fee is charged.
  • metadata: Up to 20 arbitrary key-value pairs.

Response, 201 Created

{
  "id": "qt_01H9Xt...",
  "object": "quote",
  "type": "cryptoToCrypto",
  "status": "open",
  "customerId": "cst_01H9Xa...",
  "source": {
    "walletId": "wlt_01H9Xb...",
    "asset": "usdc",
    "network": "polygon",
    "amountGross": "2000.00",
    "amountNet": "2000.00",
    "feesDeducted": {
      "total": "0.00",
      "developer": "0.00",
      "oms": "0.00",
      "gas": "0.00"
    }
  },
  "destination": {
    "wallet": {
      "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456"
    },
    "asset": "usdc",
    "network": "polygon",
    "amountGross": "1998.50",
    "amountNet": "1998.50",
    "feesDeducted": {
      "total": "1.50",
      "developer": "0.00",
      "oms": "1.50",
      "gas": "0.00"
    }
  },
  "fixedAmountSide": "source",
  "sponsorGasCost": "0.38",
  "sponsorGas": true,
  "developerFees": [],
  "rates": {
    "pair": "usdc/usdc",
    "exchangeRate": "1.0000",
    "effectiveRate": "0.9993"
  },
  "metadata": null,
  "expiresAt": "2024-01-15T10:05:00Z",
  "createdAt": "2024-01-15T10:00:00Z"
}
Key fields in the response:
  • id: The quote ID (prefix qt_). You’ll use this to create the transaction.
  • status: "open": Pricing is locked, awaiting acceptance.
  • type: "cryptoToCrypto": Inferred from a USDC source and USDC destination.
  • destination.amountNet: "1998.50": What the destination address receives. Calculated as: 2,000.00 USDC − 1.50OMSfee1.50 OMS fee − 0.00 developer fee = 1,998.50 USDC.
  • sponsorGasCost: "0.38": The estimated gas cost the developer will absorb. This is an out-of-band cost, not deducted from the destination amount.
  • destination.feesDeducted.gas: "0.00": Shows as zero to the customer because gas is sponsored.
  • expiresAt: Trails-routed quotes have a ~5-minute TTL.
If the customer accepts the pricing, proceed to Step 2. If the quote expires, create a new one.

Step 2: Create the transaction

Accept the quote by creating a transaction that references the quoteId. This is the point of no return: USDC is pulled from the wallet and the onchain send begins.

Request

POST /v0.9/transactions
Authorization: Bearer {api_key}
Idempotency-Key: txn-alice-usdc-send-001
Content-Type: application/json
{
  "quoteId": "qt_01H9Xt..."
}
The request body is intentionally thin: the quote is the contract. All source, destination, and fee details were locked in Step 1. The transaction ID prefix is txn_.

Response, 201 Created

{
  "id": "txn_01H9Xd...",
  "object": "transaction",
  "type": "cryptoToCrypto",
  "status": "processing",
  "subStatus": "processing.fundsPulled",
  "customerId": "cst_01H9Xa...",
  "quoteId": "qt_01H9Xt...",
  "depositAddress": null,
  "virtualAccount": null,
  "cashIn": null,
  "source": {
    "walletId": "wlt_01H9Xb...",
    "asset": "usdc",
    "network": "polygon",
    "amountGross": "2000.00",
    "amountNet": "2000.00",
    "feesDeducted": {
      "total": "0.00",
      "developer": "0.00",
      "oms": "0.00",
      "gas": "0.00"
    },
    "txHash": "0x7f2a9b...c3d4e5"
  },
  "destination": {
    "wallet": {
      "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456"
    },
    "asset": "usdc",
    "network": "polygon",
    "amountGross": "1998.50",
    "amountNet": "1998.50",
    "feesDeducted": {
      "total": "1.50",
      "developer": "0.00",
      "oms": "1.50",
      "gas": "0.00"
    }
  },
  "fixedAmountSide": "source",
  "sponsorGasCost": "0.42",
  "sponsorGas": true,
  "rates": {
    "pair": "usdc/usdc",
    "exchangeRate": "1.0000",
    "effectiveRate": "0.9993"
  },
  "error": null,
  "metadata": null,
  "createdAt": "2024-01-15T10:01:00Z",
  "updatedAt": "2024-01-15T10:01:00Z"
}
What to notice:
  • status: "processing": USDC has been pulled from the wallet and the onchain send is underway.
  • subStatus: "processing.fundsPulled": Granular sub-status using dot notation, indicating the USDC has been pulled. The next sub-status reflects send progress.
  • source.txHash: "0x7f2a9b...c3d4e5": The onchain transaction hash for the send.
  • sponsorGasCost: "0.42": Final gas cost (may differ slightly from the quote estimate of $0.38).

Webhook, transaction.cryptoToCrypto.processing

OMS fires this webhook when the transaction enters processing:
{
  "id": "whk_evt_01H9Xw...",
  "event": "transaction.cryptoToCrypto.processing",
  "createdAt": "2024-01-15T10:01:00Z",
  "data": {
    "id": "txn_01H9Xd...",
    "object": "transaction",
    "type": "cryptoToCrypto",
    "status": "processing",
    "subStatus": "processing.fundsPulled",
    "customerId": "cst_01H9Xa...",
    "quoteId": "qt_01H9Xt...",
    "depositAddress": null,
    "virtualAccount": null,
    "cashIn": null,
    "source": {
      "walletId": "wlt_01H9Xb...",
      "asset": "usdc",
      "network": "polygon",
      "amountGross": "2000.00",
      "amountNet": "2000.00",
      "feesDeducted": {
        "total": "0.00",
        "developer": "0.00",
        "oms": "0.00",
        "gas": "0.00"
      },
      "txHash": "0x7f2a9b...c3d4e5"
    },
    "destination": {
      "wallet": {
        "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456"
      },
      "asset": "usdc",
      "network": "polygon",
      "amountGross": "1998.50",
      "amountNet": "1998.50",
      "feesDeducted": {
        "total": "1.50",
        "developer": "0.00",
        "oms": "1.50",
        "gas": "0.00"
      }
    },
    "fixedAmountSide": "source",
    "sponsorGasCost": "0.42",
    "sponsorGas": true,
    "rates": {
      "pair": "usdc/usdc",
      "exchangeRate": "1.0000",
      "effectiveRate": "0.9993"
    },
    "error": null,
    "metadata": null,
    "createdAt": "2024-01-15T10:01:00Z",
    "updatedAt": "2024-01-15T10:01:00Z"
  }
}

Step 3: Track the transaction

OMS pulls the USDC, broadcasts the onchain send, and the funds arrive at the destination address once the transaction confirms.

Webhook, transaction.cryptoToCrypto.completed

{
  "id": "whk_evt_01H9Xw2...",
  "event": "transaction.cryptoToCrypto.completed",
  "createdAt": "2024-01-15T10:02:30Z",
  "data": {
    "id": "txn_01H9Xd...",
    "object": "transaction",
    "type": "cryptoToCrypto",
    "status": "completed",
    "subStatus": null,
    "customerId": "cst_01H9Xa...",
    "quoteId": "qt_01H9Xt...",
    "depositAddress": null,
    "virtualAccount": null,
    "cashIn": null,
    "source": {
      "walletId": "wlt_01H9Xb...",
      "asset": "usdc",
      "network": "polygon",
      "amountGross": "2000.00",
      "amountNet": "2000.00",
      "feesDeducted": {
        "total": "0.00",
        "developer": "0.00",
        "oms": "0.00",
        "gas": "0.00"
      },
      "txHash": "0x7f2a9b...c3d4e5"
    },
    "destination": {
      "wallet": {
        "blockchainAddress": "0xAbC1230000000000000000000000000000DeF456"
      },
      "asset": "usdc",
      "network": "polygon",
      "amountGross": "1998.50",
      "amountNet": "1998.50",
      "feesDeducted": {
        "total": "1.50",
        "developer": "0.00",
        "oms": "1.50",
        "gas": "0.00"
      }
    },
    "fixedAmountSide": "source",
    "sponsorGasCost": "0.42",
    "sponsorGas": true,
    "rates": {
      "pair": "usdc/usdc",
      "exchangeRate": "1.0000",
      "effectiveRate": "0.9993"
    },
    "error": null,
    "metadata": null,
    "createdAt": "2024-01-15T10:01:00Z",
    "updatedAt": "2024-01-15T10:02:30Z"
  }
}
What changed from the processing webhook:
  • status is now completed and subStatus is null.
  • updatedAt reflects when the onchain send confirmed.
At this point the flow is done. 2,000 USDC was pulled from the custodial wallet, OMS took a 1.50platformfee,and1,998.50USDCarrivedatthedestinationaddress.Thedeveloperabsorbed1.50 platform fee, and 1,998.50 USDC arrived at the destination address. The developer absorbed 0.42 in gas costs.

Polling alternative

If you prefer polling over webhooks, retrieve the transaction directly:
GET /v0.9/transactions/txn_01H9Xd...
Authorization: Bearer {api_key}
Poll until status is completed or failed. Webhooks are preferred for production: they avoid unnecessary requests and notify you the moment status changes.

Failure handling

If the transaction fails after USDC has been pulled (for example, an onchain error or a compliance block), OMS fires a transaction.cryptoToCrypto.failed webhook:
{
  "id": "whk_evt_01H9Xw3...",
  "event": "transaction.cryptoToCrypto.failed",
  "createdAt": "2024-01-15T10:02:00Z",
  "data": {
    "id": "txn_01H9Xd...",
    "object": "transaction",
    "type": "cryptoToCrypto",
    "status": "failed",
    "subStatus": "failed.sendRejected",
    "error": {
      "code": "send_rejected",
      "message": "Onchain send could not be completed"
    },
    "...rest of transaction object..."
  }
}
If OMS successfully refunds the USDC back to the source wallet, you’ll receive a transaction.cryptoToCrypto.refundCompleted webhook.

Compliance review

In some cases, a transaction may be held for compliance review. This fires a developer-only webhook (not forwarded to customers):
{
  "id": "whk_evt_01H9Xw4...",
  "event": "transaction.cryptoToCrypto.underReview",
  "createdAt": "2024-01-15T10:01:05Z",
  "data": {
    "id": "txn_01H9Xd...",
    "object": "transaction",
    "type": "cryptoToCrypto",
    "status": "processing",
    "subStatus": "processing.underReview",
    "...rest of transaction object..."
  }
}
The transaction remains in processing during review. Once resolved, it proceeds to completed or failed.

Webhook events

EventWhen
transaction.cryptoToCrypto.processingUSDC pulled, onchain send underway
transaction.cryptoToCrypto.completedFunds delivered to the destination
transaction.cryptoToCrypto.failedSend failed (onchain error, compliance, etc.)
transaction.cryptoToCrypto.refundCompletedUSDC refund returned to source wallet
transaction.cryptoToCrypto.underReviewUnder compliance review (developer-only)

Key points

  • The quote is the contract. The transaction request body is just { "quoteId": "..." }: no overrides.
  • The source must be an OMS wallet. A quote source is always { "walletId", "asset", "network" }. Send to another OMS wallet with destination.wallet.id or to an external address with destination.wallet.blockchainAddress.
  • Set sponsorGas to absorb gas for the customer. Pulling and sending USDC has gas costs on the source side. With sponsorGas: true, the customer sees zero source-side fees and the developer absorbs the cost reported in sponsorGasCost.
  • source.txHash is populated because OMS executes an onchain transaction to send the USDC.
  • sponsorGasCost may differ between quote and transaction. The quote gives an estimate; the transaction shows the actual cost. Plan for minor variance.
  • No developer fee in this example. Omitting developerFees means the developer takes no cut. Add fee entries to the quote request to charge.
  • Idempotency keys are per request. Use a distinct Idempotency-Key for the quote and the transaction so retries are safe and don’t create duplicates.

Pay out to a bank account

Bank payout destinations are not yet available in the OMS API. To be notified when it launches, register your interest.

Register interest

Share your use case and we’ll reach out when bank payout destinations are available.
A fiat payout follows the same two-step quote and transaction flow. The quote source is still an OMS wallet holding USDC. The difference is the destination: instead of a crypto address, you target a saved external bank account using destination.wallet.externalAccount (a cpUsBank_ ID) with asset: "usd" and a fiat rail in network (ach or wire). OMS infers the transaction type as cryptoToFiat. External bank accounts have no endpoints yet, so this path is early access.
{
  "customerId": "cst_01H9Xa...",
  "source": {
    "walletId": "wlt_01H9Xb...",
    "asset": "usdc",
    "network": "polygon",
    "amount": "2000.00"
  },
  "destination": {
    "wallet": {
      "externalAccount": "cpUsBank_01H9Xs..."
    },
    "asset": "usd",
    "network": "ach"
  },
  "sponsorGas": true
}
Once the quote is open, create the transaction the same way, with { "quoteId": "..." }. Track it with transaction.cryptoToFiat.* webhooks or by polling GET /v0.9/transactions/{id}. ACH transfers typically settle in 1 to 3 business days; wire is faster. The same operational notes apply: the quote is the contract, sponsorGas covers the source-side gas reported in sponsorGasCost, and you should use a distinct idempotency key per request.