---
title: "Stablecoin API objects explained: customers, bank accounts, wallets, quotes, payouts, and webhooks"
seoTitle: "Stablecoin API objects explained: the data model"
description: "The core objects behind a stablecoin API, how they relate, which IDs to store in your ledger, and 8 data-model mistakes that break payout integrations."
date: "2026-09-28"
updated: "2026-09-28"
category: "payments"
author: "BlindPay Team"
faq:
  - q: "What are the main objects in a stablecoin API?"
    a: "Most stablecoin APIs share the same core objects: a customer (the verified person or business), bank accounts and blockchain wallets that hold or receive funds, a quote that locks the rate and fees, a payout or payin that executes the quote, an optional virtual account for repeat deposits, and webhook events that report every state change. Names vary by provider, but the roles stay the same."
  - q: "What is the difference between a customer and a receiver in a stablecoin API?"
    a: "A customer is the verified legal party on whose behalf money moves, and it passes KYC or KYB. A receiver, beneficiary, or bank account is a payment destination attached to that customer. Some providers use receiver to mean the customer itself, so read the docs. At BlindPay, customer IDs start with re_ and bank accounts start with ba_."
  - q: "Why do stablecoin API quotes expire so quickly?"
    a: "A quote locks an exchange rate and fees that the provider has to honor, while FX and liquidity keep moving. Short windows, often five minutes, limit that exposure. Treat a quote as single use: create it when the sender confirms, execute the payout right away, and request a new one if it expires. Never cache a quote for a later payment."
  - q: "Which provider IDs should I store in my database?"
    a: "Store the customer ID next to your user or business record, the bank account and wallet IDs next to each payee, and the quote ID, payout or payin ID, and your own idempotency key on every payment. Keep the rail reference too, such as the SWIFT UETR, plus the webhook message ID you used for deduplication. Store amounts as integers in minor units."
  - q: "Should my app update payment status from webhooks or by polling?"
    a: "Use webhooks as the primary signal and polling as the safety net. Webhooks arrive at least once, can repeat, and can arrive out of order, so deduplicate by message ID and only move a payment forward in its state machine. Run a scheduled job that fetches any payment still open past its expected arrival time and reconciles it against the API."
  - q: "How many virtual accounts can one customer have?"
    a: "It depends on the provider. Many issue one account per customer per banking partner, and some allow several for separate purposes. Model the virtual account as its own object linked to the customer and to the wallet where converted stablecoins land, rather than as a field on the customer, so a second account later does not force a schema change."
---

A stablecoin API is built from about eight objects. A customer is the verified person or business. Bank accounts and wallets are where money goes or sits. A quote locks the rate and fees. A payout or payin executes that quote, a virtual account collects repeat deposits, and webhook events report every change. Get those relationships right and the integration code stays small.

**Key takeaways**

- The customer is a verified legal party, not your login user. Model it that way from day one.
- A quote is single use and lives for minutes. A payout or payin is the durable record you reconcile.
- Store the provider's IDs, your idempotency key, and the rail reference on every payment, with amounts as integers in minor units.
- Webhooks are delivered at least once and can arrive out of order. Your state machine should only move forward.
- Names vary by provider (receiver, beneficiary, counterparty), but the roles below show up in nearly every [type of stablecoin API](/resources/more/types-of-stablecoin-apis).

The [integration guide](/resources/more/how-to-integrate-a-stablecoin-api) walks through the calls in order. This page covers the layer under the calls: what each object means, how they link, and what your own database should keep.

## What are the core objects in a stablecoin API?

The core objects are the environment, customer, bank account, wallet, quote, payout, payin, virtual account, and webhook event.

| Object | What it represents | How long it lives | Example ID at BlindPay |
| --- | --- | --- | --- |
| Instance (environment) | An isolated sandbox or production space. Nothing crosses between them | Permanent | `in_...` |
| Customer | The verified individual or business that sends or receives money. Passes KYC or KYB | Permanent | `re_...` |
| Bank account | A fiat destination for payouts: rail, account details, holder name. Can belong to a third party | Until deleted | `ba_...` |
| Blockchain wallet | An external address the customer controls, on a specific network | Until deleted | `bw_...` |
| Managed wallet | A stablecoin balance the provider holds for the customer | Permanent | `bl_...` |
| Quote | A locked rate, fee split, and amount for one payment | Minutes | `qu_...` (payout), `pq_...` (payin) |
| Payout | Stablecoins out, local currency into a bank account | Permanent record | `po_...` |
| Payin | Local currency in, stablecoins out to a wallet | Permanent record | `pi_...` |
| Virtual account | A reusable bank account in the customer's name for repeat deposits | Until closed | `va_...` |
| Webhook endpoint and event | Your URL, and the messages sent to it on each state change | Endpoint permanent, events replayable | `we_...` |

Less common objects follow the same pattern: transfers between wallets, bills to pay (payables), off-ramp wallets that pay out on deposit, and partner fees. If you understand the ten above, you can read any of them.

## How do the objects relate to each other?

Every object hangs off a customer inside one environment, and every money movement points back to a quote.

Picture a tree. The instance is the root. Customers sit under it. Each customer owns its destinations and sources: many bank accounts, many wallets, and usually one or a few virtual accounts. A virtual account points at the wallet where converted stablecoins land.

Money movements sit beside the tree, not inside it. A payout quote references a customer's bank account plus the network and token that fund it. The payout references that quote and the wallet the stablecoins come from. A payin references its payin quote and the destination wallet. Webhook events reference whichever object changed.

Three cardinality rules save the most bugs:

- **One quote backs one payout.** A second payout with the same quote ID should be rejected.
- **A bank account belongs to the paying customer, not the payee.** A customer named John can pay a bank account held by Jack.
- **Funds sit in exactly one place at a time.** An external wallet is under the customer's control, a managed wallet is held by the provider, and a payout in flight is in the provider's hands until it completes or returns. [Payout statuses](/resources/more/stablecoin-payout-statuses-explained) shows each step.

## Which IDs should you store in your own database?

Store every provider ID next to the record it maps to, plus the references your finance team will ask for later.

| Your record | What to store | Why |
| --- | --- | --- |
| User or business | Customer ID, KYC status | Every payment needs the customer; status gates what they can do |
| Payee | Bank account ID, rail, last four digits | Lets you reuse the destination without resending details |
| Wallet | Wallet ID, address, network | The same address on two networks is two different wallets |
| Payment | Quote ID, payout or payin ID, your idempotency key, status, amounts in minor units, token, network | The core of reconciliation |
| Rail reference | SWIFT UETR, Fedwire IMAD, or bank reference when the rail returns one | What the recipient's bank asks for when a payment goes missing |
| Onchain reference | Transaction hash | Proves the stablecoin leg happened |
| Webhook log | Message ID, event type, received time, processed time | Deduplication and audit |

Amounts deserve their own rule. Store integers in minor units, the way the API sends them. A `request_amount` of `66600` in USD means $666.00. A float will eventually round a cent the wrong way, and finance will find it at month end.

## What does a quote and payout request look like?

A quote request names the destination, amount, and funding token; the payout request names the quote. The example below is generic and illustrative. Field names differ by provider.

```json
// POST /quotes  (illustrative)
{
  "bank_account_id": "ba_123",
  "request_amount": 100000,
  "currency_type": "sender",
  "token": "USDC",
  "network": "base"
}

// 200 OK
{
  "id": "qu_456",
  "sender_amount": 100000,
  "receiver_amount": 532150,
  "commercial_rate": 5.35,
  "flat_fee": 300,
  "expires_at": 1790000000000
}
```

```json
// POST /payouts  (illustrative)
// Header: Idempotency-Key: 7b0c4c2e-payroll-2026-09-30-row-118
{
  "quote_id": "qu_456",
  "sender_wallet_address": "0xabc..."
}

// 200 OK
{ "id": "po_789", "status": "processing" }
```

Three details in that exchange matter. `expires_at` is a timestamp you read, not a window you assume. The `Idempotency-Key` header, described in an [IETF draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/), lets you retry the POST without paying twice; our [idempotency guide](/resources/more/stablecoin-api-idempotency-keys) covers the edge cases. And the payout comes back as `processing`, not `completed`. The final answer arrives by webhook.

## Which states does each object move through?

Each object has a small state machine, and your code should mirror it rather than invent its own.

| Object | Common states | Terminal states | Watch for |
| --- | --- | --- | --- |
| Customer | verifying, pending review | approved, rejected | Approved with an open information request |
| Virtual account | pending review, bank review | approved, rejected | Rail details are empty until approval |
| Quote | active | expired, used | Expiry before the sender confirms |
| Payout | processing, on hold | completed, failed, refunded | A failed payout is not automatically refunded |
| Payin | processing, on hold | completed, failed, refunded | A refunded payin means the deposit went back to the sender |

Model "on hold" as pending, not as an error. SWIFT and USD payouts often pass through a review hold as a standard step, so most users will see it at least once.

## How should you build the data model, step by step?

Build it in the same order money moves. Seven steps:

1. **Create one environment per stage.** Keep sandbox and production IDs in separate databases or clearly tagged columns. An ID from one never works in the other.
2. **Map your users to customers.** One customer per legal party that sends or receives money. A company with five admins is one business customer, not five.
3. **Attach destinations to the customer.** Bank accounts and wallets get their own tables with a foreign key to the customer.
4. **Write the payment row before you call the API.** Generate the idempotency key, store it with status `created`, then request the quote and the payout.
5. **Record every provider ID as it comes back.** Quote ID, then payout ID, then the transaction hash and rail reference.
6. **Apply webhooks through a forward-only state machine.** Deduplicate by message ID. The [Standard Webhooks spec](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md) recommends using the message ID as an idempotency key for this.
7. **Reconcile on a schedule.** Fetch anything still open past its expected arrival and compare it with your ledger. The [webhooks and reconciliation guide](/resources/more/stablecoin-api-webhooks-reconciliation) has the matching logic.

## What are the 8 most common data-model mistakes?

These eight show up in nearly every first integration, and each has a one-line fix.

| Mistake | What breaks | Fix |
| --- | --- | --- |
| Treating the customer as your login user | Duplicate KYC, payments split across "customers" for one company | One customer per legal party; many logins can act for it |
| Registering only your direct clients when you pay on behalf of others | Compliance gaps in nested flows | Register each end customer as its own customer when the provider requires it |
| Storing the bank account on the payee profile only | Third-party payouts fail to map | Bank account belongs to the paying customer, with holder name stored separately |
| Storing amounts as floats | Cent-level drift at reconciliation | Integers in minor units, converted only for display |
| Caching or reusing quotes | Expired or rejected payouts | New quote per payment, executed immediately |
| Generating a new idempotency key on each retry | Duplicate payouts after a timeout | One key per intended payment, saved before the first call |
| Applying webhooks in arrival order | A late event moves a completed payout back to processing | Forward-only transitions, dedupe by message ID |
| Counting linked events twice | One bill payment shows as two payments | Correlate linked records (for example a payable and its payout) by ID and count once |

## Where does BlindPay fit?

BlindPay's API uses these same objects with prefixed IDs, so a log line tells you what you are looking at. The docs come in two flavors: Abstracted, for teams that think in bank rails (virtual accounts, payins, payouts), and Advanced, for teams that work with wallets, approvals, and chains directly. Both describe the same API. Official [SDKs](https://blindpay.com/docs/sdks) for Node.js, Python, Go, PHP, and Swift are generated from one [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) spec, so the object shapes match across languages. The [customers reference](https://blindpay.com/docs/learn/customers) and the [webhook event list](https://blindpay.com/docs/learn/webhooks-events) are good first reads.

## What to do next

Draw your current schema next to the table in this article. For each payment row, check that you store the quote ID, the payout or payin ID, your idempotency key, and the amount in minor units. If any of those is missing, add the column before your first production payout, not after the first reconciliation break.
