How to integrate a stablecoin payin API: a developer quickstart
Stablecoin payin API integration in one afternoon: authenticate, verify a customer, link a wallet, take a deposit, and handle webhooks, with cURL.
To integrate a stablecoin payin API, create an API key, create and verify a customer, and link a destination wallet. Then create a virtual account, or a payin quote and a payin, to show the payer where to send money. Handle webhooks as the deposit becomes stablecoins. BlindPay lets you run all of it on a development instance first.
A payin is the "money in" direction: a payer sends fiat, and the API delivers stablecoins to a wallet. If the term is new, start with what a stablecoin payin API is. This guide is the hands-on version, with the exact calls from BlindPay's docs.
Key takeaways
- Five objects carry the flow: an API key, a customer, a wallet, a payin (or a virtual account), and a webhook endpoint.
- Amounts are integers in minor units.
10000means $100.00. - A payin quote expires after 5 minutes, and a created payin cannot be canceled.
- Development instances approve customers automatically and complete payins about 30 seconds after creation.
- Verify webhook signatures on the raw body, and test a failed and a refunded payin before launch.
What do I need before I start?
You need a BlindPay account, a development instance, and an API key for it. You also need an HTTPS endpoint that can receive webhooks. Everything else is created through the API.
Keep these definitions handy:
- Customer: the person or business receiving the stablecoins. Every payin belongs to a customer who passed KYC (know your customer) checks.
- Wallet: the address that receives the stablecoins. A stablecoin is a token pegged to a currency, such as USDC for the US dollar.
- Payin: one deposit, created from a quote.
- Virtual account: a dedicated bank account whose deposits settle to a wallet on their own.
The stablecoin delivered on a development instance is USDB, BlindPay's test token. In production you use USDC or USDT. The pieces of the flow are covered in more depth in how a stablecoin payin works step by step.
How do I authenticate?
Send your API key as a Bearer token on every request. Keys are scoped to one instance, so a development key will not work on production, and the dashboard shows a key only once, at creation.
The base URL is the same for development and production. Which environment you hit depends only on which instance the key belongs to. Keep the key on your server and out of client code. Development instances are capped at 40 requests per minute per instance, so a loop that hammers the API will return 429 with development_instance_rate_limit_exceeded.
How do I create and verify a customer?
Two calls: generate a terms-of-service URL, then create the customer with the tos_id it produces.
Open the returned URL and accept. The browser lands on your redirect_url with ?tos_id=to_... in the address bar. In production, the customer accepts the terms themselves.
Save the id (re_...). On a development instance the customer is approved automatically, and placeholder document URLs are fine. In production, standard KYC for an individual takes about 60 seconds, while business verification (KYB) is a manual review of 3 hours to 1 business day. The status moves from verifying to approved, rejected, or compliance_request, and a customer.update webhook announces each change. Do not request quotes until the customer is approved.
Where do the stablecoins land?
Register the destination wallet before you quote, because the quote needs its ID. BlindPay offers two destinations for payins.
| Destination | ID prefix | Who holds the keys | Notes |
|---|---|---|---|
| External blockchain wallet | bw_ | The customer | Pass blockchain_wallet_id on the quote |
| Managed wallet (beta) | bl_ | BlindPay custodies the balance | Pass wallet_id on the quote |
Here is a direct-address registration for an external wallet on a testnet. On a development instance only testnet networks are accepted.
Double-check the address. BlindPay cannot recover stablecoins delivered to a wrong one. For Brazilian customers, also send is_self_custody (true or false); omitting it returns self_custody_required. The network is read from the wallet, so you never pass a network on a payin quote. For help choosing a chain, see the guide to which blockchain network to use for payins.
How do I create a virtual account for payins?
Create a virtual account when a customer will receive repeated deposits and you want one permanent account number instead of fresh instructions each time. The customer must already be approved and have a wallet.
banking_partner takes one of the values listed in the create a virtual account docs, and which one is available depends on your instance and the customer. On a development instance use USDB as the token. Business customers must have fields such as account_purpose and source_of_wealth filled in first, or the call fails with missing_required_fields.
On a development instance the account is approved immediately, with a fake routing number. In production it goes through compliance review (pending_review) and then bank review (verifying) before it becomes approved, and approval is not guaranteed at either stage. A virtualAccount.complete webhook tells you when the account number is live. Each deposit then creates a payin and fires payin.new. The model is explained in virtual accounts for stablecoin payins.
How do I create a one-time payin instead?
Quote first, then create the payin. The quote locks the amount, fees, and destination for 5 minutes.
currency_type: "sender" means request_amount is in the fiat the payer sends. With cover_fees: true the sender pays the fee on top, and the wallet receives the full amount. Save the quote id (qu_...), then create the payin from it, with an Idempotency-Key header so a network retry cannot create two:
The path says evm for every payment method and network. The name is historical. For ach the response carries memo_code and blindpay_bank_details, which you show to the payer. Other rails return a pix_code, a clabe, or an instruction object, listed in the payins docs. The same flow for other rails is covered in how to integrate a crypto on-ramp API.
How do I listen for payin events?
Register an HTTPS endpoint, then fetch its signing secret.
An empty events array subscribes to everything. The response returns the endpoint ID (we_...), and GET .../webhook-endpoints/{id}/secret returns the whsec_... signing secret. Every call is signed with svix-id, svix-timestamp, and svix-signature headers. Verify on the raw body before parsing, and deduplicate on svix-id. The details are in stablecoin API webhooks and reconciliation.
| Event or status | What it means | What your system should do |
|---|---|---|
payin.new | Payin created, or a virtual account deposit arrived | Create your payment record in a pending state |
payin.update | An intermediate step, such as an arrival check or review | Update tracking detail, no customer message needed |
payin.complete with completed | Stablecoins delivered to the wallet | Credit the customer, once |
payin.complete with refunded | Deposit returned to the sender | Close the payment, notify the customer |
payin.complete with failed | The payin did not go through | Close the payment, inspect before messaging |
status: on_hold | Held for manual review | Keep pending, answer any information request quickly |
How do I test in the sandbox?
A development instance runs the whole flow without real money. Every payin completes about 30 seconds after creation and delivers USDB. To force a different outcome, set the quote's request_amount:
request_amount | Result |
|---|---|
66600 ($666.00) | failed |
77700 ($777.00) | refunded |
| Any other amount | completed after about 30 seconds |
Also note that on development the memo_code and pix_code are the literal placeholder <development>, so you cannot pay them for real. The full pre-launch list is in how to test a virtual accounts integration.
What mistakes should I avoid?
These are the five errors that cost the most time on a first build.
| Symptom | Likely cause | Fix |
|---|---|---|
missing_required_fields on virtual account create | Business customer is missing fields such as account_purpose | Update the customer, then retry |
self_custody_required on wallet create | Brazilian customer, no is_self_custody | Send true or false |
request_amount_must_be_a_whole_currency_unit | MXN, COP, or ARS amount not a multiple of 100 | Round to a whole currency unit |
429 with development_instance_rate_limit_exceeded | More than 40 requests per minute | Back off using the Retry-After header |
| Payin create is rejected | Quote older than 5 minutes | Create a new quote and retry |
Two further habits prevent most production incidents. Send an Idempotency-Key on every create, and read status from each payload instead of guessing from the event name. For the idempotency rules, see the BlindPay idempotency docs.
What changes in production?
Production adds real checks and real timing. A production instance can take up to 3 business days to provision, and you need a new API key, because keys are scoped to one instance.
- Submit real KYC documents for every customer.
- Change
tokenfromUSDBtoUSDCorUSDT, and use mainnet networks. - Remove any logic that relies on the 666.00 and 777.00 test amounts.
- Create a new webhook endpoint on the production instance. Development webhook settings do not carry over.
- Expect real bank timing: ACH and wire payins wait up to 5 business days, while Pix, SPEI, Transfers, and PSE payins wait up to 30 minutes before they fail.
Where does BlindPay fit?
BlindPay offers the API calls above plus tooling around them. The REST API has an OpenAPI 3.1 spec you can download from https://api.blindpay.com/doc to generate a typed client. Official SDKs exist for Node.js, Python, Go, PHP, and Swift; this guide shows cURL because the SDK docs do not include a payin example, so check each SDK's README for method names. For scripting, the BlindPay CLI covers the same flow, for example blindpay payin_quotes create and blindpay payins create --quote-id qu_..., with a --json flag for structured output (see the CLI announcement).
BlindPay's pricing is a monthly plan plus per-transaction fees that vary by rail and network and appear in the quote before you create the payin. See /pricing. The same pattern of customer, virtual account, and webhook appears across payin providers, so the structure here transfers even if you compare vendors. Agents can run this flow too: see building with AI agents, or copy the payins integration prompt.
Sources
- BlindPay docs, Bank transfer to stablecoins quickstart (checked on 2026-10-26)
- BlindPay docs, Payins (checked on 2026-10-26)
- BlindPay docs, Create a virtual account (checked on 2026-10-26)
This article is for general information only and is not legal, tax, or financial advice.
FAQ
More payments articles
See all payments articlesIntegrating a stablecoin API: a developer's guide to cross-border payments with BlindPay
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.
Payin webhooks, idempotency, and reconciliation for stablecoin payins
Payin webhooks reconciliation: verify signatures, dedupe events, model a payin state machine, and match each virtual account deposit to your ledger.
Neobank launch checklist: a 30, 60, and 90 day plan for an MVP
A 90-day neobank MVP plan: team roles, regulatory path, vendors, build milestones, pilot users, launch criteria, and the metrics to watch after day 90.