---
title: "Integrating a stablecoin API: a developer's guide to cross-border payments with BlindPay"
description: "Step-by-step integration of a stablecoin API: authenticate, onboard a customer, create a virtual USD account, quote and send a cross-border payout, and handle signed webhooks. Endpoints, statuses, and SDKs for BlindPay."
date: "2026-09-17"
category: "payments"
author: "BlindPay Team"
howto:
  name: "How to integrate a stablecoin API for cross-border payments"
  steps:
    - name: "Authenticate with an instance-scoped API key"
      text: "Create a development instance in the dashboard, generate an API key, and send it as a Bearer token. Every request goes to https://api.blindpay.com/v1/instances/{instance_id}/... and the key decides whether you hit development or production."
    - name: "Onboard a customer and pass KYC or KYB"
      text: "Create a customer with type individual or business. Standard individual KYC is automated and usually resolves in about a minute; business KYB goes through review. Wait for kyc_status approved before creating accounts."
    - name: "Create a virtual USD account or register a wallet"
      text: "Attach a blockchain wallet to the approved customer, then create a virtual account that accepts ACH, wire, or SWIFT deposits and converts them to USDC or USDT into that wallet. If you already hold stablecoins, register the wallet and skip the virtual account."
    - name: "Add the receiver's bank account and request a quote"
      text: "Register the destination bank account with its rail type, such as pix, spei_bitso, ach, or international_swift. Request a quote with the amount in minor units; the response locks the rate, itemizes fees, shows the receive amount, and expires in five minutes."
    - name: "Create the payout"
      text: "Approve the quoted stablecoin amount on-chain from your wallet, then create the payout with the quote id and sender wallet address. The response returns a payout id with per-step tracking and a status of processing, on_hold, completed, failed, or refunded."
    - name: "Handle webhooks for status updates"
      text: "Register an HTTPS webhook endpoint, verify each delivery with the svix-id, svix-timestamp, and svix-signature headers using the endpoint secret, and dedupe on svix-id. Listen for payout.update and payout.complete to drive your ledger."
faq:
  - q: "Does the BlindPay API have a sandbox environment?"
    a: "Yes. Every account can create development instances, and a development API key routes all requests to the development environment on the same hostname. Development instances skip banking-partner review, return fake bank account numbers, and accept sentinel amounts that force a payout into failed or refunded so you can test every path."
  - q: "Which currencies and countries does the stablecoin API support?"
    a: "Stablecoins USDC and USDT on Ethereum, Polygon, Base, Arbitrum, Solana, Stellar, and Tron, converting to BRL over Pix, PIX Safe, and TED in Brazil, MXN over SPEI in Mexico, COP over ACH in Colombia, ARS over Transfers 3.0 in Argentina, USD over ACH, RTP, and domestic wire in the United States, EUR over SEPA, and USD to 100+ countries over SWIFT (POBO/COBO). The live list is available from the public rails endpoint without an API key."
  - q: "How are compliance checks handled during onboarding?"
    a: "Each customer goes through KYC or KYB inside the API. Standard individual KYC is automated and typically resolves in about a minute. Enhanced KYC for high-risk countries and standard KYB for businesses go through review, usually within a few hours and up to one business day. Sanctions screening runs on customers and on payouts, and a hold shows up as an on_hold payout status rather than a silent delay."
  - q: "Are webhooks reliable, and how do I verify them?"
    a: "Deliveries are signed with HMAC-SHA256 and carry svix-id, svix-timestamp, and svix-signature headers. A non-2xx response is retried with backoff over the following hours, and svix-id stays the same across retries, so use it as your deduplication key. You can also replay any event from the dashboard."
  - q: "Do I need to hold crypto to send a cross-border payout?"
    a: "No. Deposit dollars into a virtual USD account over ACH, wire, or SWIFT and the deposit converts to USDC or USDT in the linked wallet. From there the payout flow is identical to sending from a wallet you funded yourself."
  - q: "Which SDKs are available?"
    a: "Official SDKs cover Node and TypeScript, Python, Go, PHP, and Swift, all generated from the OpenAPI specification. There is also an MCP server so AI coding tools can call the API directly, and the docs publish llms.txt for agent consumption."
---

Integrating a stablecoin API for cross-border payments takes five steps: authenticate with an instance-scoped API key, onboard a customer through KYC or KYB, fund a virtual USD account or register a stablecoin wallet, quote and create a payout to a local bank account, and handle signed webhooks for status changes. With BlindPay, a first payout in the development environment is typically a day of work using the REST API or one of the official SDKs.

This guide walks through that flow with the actual endpoints, statuses, and event names. It is written for a developer or technical founder who has never moved money over stablecoins before. For the category background, read [what a stablecoin API is](/resources/more/what-is-a-stablecoin-api).

## What you will need to get started

| Requirement | Where to get it |
| --- | --- |
| A BlindPay account and a development instance | Dashboard at app.blindpay.com; instances are created there, not through the API |
| An API key scoped to that instance | Instance settings, API Keys tab; shown once, so store it in a secrets manager |
| An SDK or an HTTP client | `@blindpay/node` on npm, `blindpay` on PyPI, `blindpay-go`, `blindpay-php`, or `blindpay-swift`; or generate a client from the [OpenAPI spec](https://api.blindpay.com/doc) |
| A blockchain wallet you control | Any EVM, Solana, or Stellar wallet on a supported network; testnets work on development instances |
| An HTTPS endpoint for webhooks | Public URL; local and private addresses are rejected, so use a tunnel in development |

Development instances are free, rate-limited to about 100 requests per minute, and skip banking-partner review so virtual accounts and payouts resolve immediately. Production access requires compliance onboarding and takes up to three business days. Read [sandbox vs production](/docs/learn/sandbox-vs-production) for the exact behavioral differences.

## Step 1: Authenticate

Every request carries the API key as a Bearer token and targets a specific instance:

```bash
GET /v1/instances/in_000000000000/customers
Host: api.blindpay.com
Authorization: Bearer YOUR_API_KEY
```

There is no separate sandbox hostname. Which environment you hit is decided entirely by which instance the key belongs to, so a development key fails against a production instance and vice versa. Keys are full read and write, optionally locked to an IPv4 allowlist.

Two headers are worth adopting from the first request. `Idempotency-Key` on any write makes a network retry safe: the same key with the same body returns the original response with `Idempotency-Replayed: true`, and a different body returns a 422. On development instances a 429 comes with `Retry-After`, so build the backoff in now rather than when production traffic arrives.

## Step 2: Onboard a customer

A customer is the person or business you will pay or collect from. Create one with `type` set to `individual` or `business` and the identity fields the docs list for that type. Compliance runs immediately.

| Onboarding path | Who | How it resolves | Typical time |
| --- | --- | --- | --- |
| KYC standard | Individuals in standard-risk countries | Automated | About 60 seconds |
| KYC enhanced | Individuals in high-risk countries | Review | 3 hours to 1 business day |
| KYB standard | Businesses | Review | 3 hours to 1 business day |

Poll or, better, listen for `customer.update` until `kyc_status` reads `approved`. Other statuses include `verifying`, `pending_review`, `compliance_request` when the team needs a document, and `rejected` with the reason in `kyc_warnings` or `fraud_warnings`. Customers in prohibited countries are blocked at creation and cannot be overridden, and KYC data on an existing customer cannot be edited, so treat a rejection as "create a corrected customer" rather than "patch this one."

This is the step that turns compliance from a project into an API call. Sanctions screening and risk scoring happen here and again on each transfer, which is why a payout can later sit in `on_hold` without anything being wrong on your side. Read [how to automate KYC and KYB for stablecoin payments](/resources/more/how-to-automate-kyc-kyb-stablecoin-payments) for the design behind it.

## Step 3: Create a virtual USD account or register a wallet

You need stablecoins in a wallet to send a payout. There are two ways to get there.

**If you already hold USDC or USDT**, register the wallet on the customer with `POST /v1/instances/{instance_id}/customers/{customer_id}/blockchain-wallets`, choosing the network and address. You will send the payout from this wallet.

**If you hold dollars in a bank**, create a [virtual USD account](/virtual-accounts). It is a dedicated US account number that accepts ACH, domestic wire, and SWIFT deposits (RTP for some account types) and converts every deposit to USDC or USDT in the linked wallet. First attach a blockchain wallet to the approved customer, then:

```bash
POST /v1/instances/{instance_id}/customers/{customer_id}/virtual-accounts
Content-Type: application/json

{
  "token": "USDC",
  "blockchain_wallet_id": "bw_000000000000"
}
```

The request also takes a `banking_partner` from the documented list, and eligibility depends on the customer's country and business type. The response includes `ach`, `wire`, and where applicable `rtp` objects with routing and account numbers, and SWIFT details once the account is `approved`. In development the account is approved instantly with fake numbers. Each incoming deposit creates a payin and fires `payin.new` and then `payin.complete`, which is your signal that stablecoins have landed. Business customers need a few extra fields before a virtual account can be created, such as business type, industry, and ownership, and the API returns `missing_required_fields` naming exactly which ones.

This is the "no pre-funding" model in practice: you fund each transfer when you need it, from a wallet you control, rather than parking a balance in each destination currency.

## Step 4: Add a bank account and request a quote

Register the receiver's bank account once, with the rail as its `type`: `pix`, `pix_safe`, or `ted` for Brazil, `spei_bitso` for Mexico, `ach_cop_bitso` for Colombia, `transfers_bitso` for Argentina, `ach`, `rtp`, or `wire` for the United States, `sepa` for Europe, and `international_swift` for everywhere else. The public rails endpoint returns the live list, with countries, and needs no API key.

Then request a quote:

```bash
POST /v1/instances/{instance_id}/quotes
Content-Type: application/json

{
  "bank_account_id": "ba_000000000000",
  "currency_type": "receiver",
  "request_amount": 500000,
  "cover_fees": false,
  "network": "base",
  "token": "USDC"
}
```

Amounts are integers in minor units, so the request above is 5,000.00 in the receiver's currency. `currency_type` decides whether you are fixing the stablecoin amount sent or the fiat amount received. The response returns `commercial_quotation`, `blindpay_quotation`, `sender_amount`, `receiver_amount`, and the fees as separate fields, plus `expires_at` in epoch milliseconds. A quote is valid for five minutes and backs exactly one payout. For EVM networks the response also includes a `contract` object with the address, ABI, and amount to approve.

Show the receiver amount to your user before they confirm. That one field is what makes stablecoin payouts comparable to any other rail: the receiver gets exactly this, and it was known before anything moved.

## Step 5: Create the payout

Approve the quoted stablecoin amount from your wallet on-chain using the `contract` details in the quote, then create the payout on the endpoint for your network: `/payouts/evm` for Ethereum, Polygon, Base, and Arbitrum, `/payouts/solana`, or `/payouts/stellar`.

```bash
POST /v1/instances/{instance_id}/payouts/evm
Content-Type: application/json
Idempotency-Key: 8f6a0c2e-...

{
  "quote_id": "qu_000000000000",
  "sender_wallet_address": "0x..."
}
```

The response is a payout with `id`, `status`, and five tracking objects covering the on-chain transaction, liquidity, partner fee, payment, and completion, each with its own step. The status values are:

| Status | Meaning | What your system should do |
| --- | --- | --- |
| `processing` | Funds pulled, conversion and delivery in progress | Show pending |
| `on_hold` | Compliance or banking review before release; every SWIFT payout starts here, ACH, wire, and RTP pass through it | Show pending, do not retry |
| `completed` | Local currency delivered over the rail | Mark paid, store the rail reference |
| `failed` | Delivery could not complete | Surface the reason, funds return to the sender wallet |
| `refunded` | Funds returned after a failure downstream | Reconcile the return |

Delivery time depends on the rail: minutes on Pix, SPEI, and Transfers 3.0, instant on RTP, one to two business days on ACH, TED, and SEPA, up to five on SWIFT, where each payout carries a UETR and an MT103 confirmation. SWIFT payouts have a 100 USD minimum. In development, a `request_amount` of `66600` forces `failed` and `77700` forces `refunded`, so you can test the unhappy paths without waiting for one to happen.

## Step 6: Handle webhooks

Polling works, but webhooks are how the integration is meant to run. Register an endpoint:

```bash
POST /v1/instances/{instance_id}/webhook-endpoints
Content-Type: application/json

{
  "url": "https://example.com/webhooks/blindpay",
  "events": []
}
```

An empty `events` array subscribes to everything. Fetch the endpoint's signing secret with a GET on its `/secret` path; it starts with `whsec_`. The events you will handle most:

| Event | Fires when |
| --- | --- |
| `customer.update` | KYC or KYB status changes |
| `payin.complete` | A virtual account deposit has converted and landed |
| `payout.update` | A payout changes status, including to `failed` |
| `payout.complete` | Local currency has been delivered |
| `virtualAccount.complete` | A virtual account has finished review |

Verify every delivery. The headers are `svix-id`, `svix-timestamp`, and `svix-signature`. Concatenate the id, the timestamp, and the raw body with dots, compute HMAC-SHA256 with the base64-decoded secret, and compare against each `v1,` entry in the signature header using a constant-time comparison. Reject anything older than five minutes.

Two details save debugging time. A `failed` payout fires `payout.update`, not `payout.complete`, so key your terminal-state logic on status rather than event name. And retries reuse the same `svix-id`, so store it and skip duplicates. Failed deliveries are retried with backoff over the following hours, and any event can be replayed from the dashboard without creating a new business event. Read the [webhook events reference](/docs/learn/webhooks-events) for the full list.

## How OpenAPI and SDKs shorten the integration

Every endpoint above is described in a single OpenAPI 3.1 document at `https://api.blindpay.com/doc`. The official SDKs for Node, Python, Go, PHP, and Swift are generated from it, so field names, enums, and error shapes match the docs exactly and a new API field shows up as a typed property rather than a surprise in a JSON blob.

The same spec is what makes AI-assisted integration work. The MCP server, `npx -y @blindpay/mcp`, lets a coding agent in Claude Code or Codex call the API against your development instance while you build. The docs publish llms.txt and a raw markdown twin of every page, so an agent can read the reference the same way you do. The practical effect is that the first payout in development is usually a day of work, and most of that day is your own ledger and UI, not the API.

## What to do next

Create a development instance, run the six steps above against testnet, and force a `failed` and a `refunded` payout with the sentinel amounts before you write a line of production code. Then read [how to choose a stablecoin API](/resources/more/how-to-choose-a-stablecoin-api) to check that the provider you are integrating clears the bar on pre-funding, compliance, and pricing, or go straight to the [payout quickstart](/docs/quickstart-payout).

*This article is for general information only and is not legal, tax, or financial advice.*
