Skip to content

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. 10000 means $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.

Bash
curl https://api.blindpay.com/v1/instances/in_000000000000/customers \
  --header 'Authorization: Bearer YOUR_API_KEY'

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.

Bash
curl --request POST \
  --url https://api.blindpay.com/v1/e/instances/in_000000000000/tos \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "idempotency_key": "<your_uuid>",
    "redirect_url": "https://example.com/tos-accepted"
  }'

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.

Bash
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/customers \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "tos_id": "to_000000000000",
    "type": "individual",
    "kyc_type": "standard",
    "email": "email@example.com",
    "tax_id": "12345678",
    "address_line_1": "8 The Green",
    "address_line_2": "#12345",
    "city": "Dover",
    "state_province_region": "DE",
    "country": "US",
    "postal_code": "02050",
    "ip_address": "127.0.0.1",
    "phone_number": "+13022006100",
    "proof_of_address_doc_type": "UTILITY_BILL",
    "proof_of_address_doc_file": "https://example.com/proof-of-address.jpg",
    "first_name": "John",
    "last_name": "Doe",
    "date_of_birth": "1998-01-01T00:00:00Z",
    "id_doc_country": "US",
    "id_doc_type": "PASSPORT",
    "id_doc_front_file": "https://example.com/passport-front.jpg",
    "selfie_file": "https://example.com/selfie.jpg"
  }'

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.

DestinationID prefixWho holds the keysNotes
External blockchain walletbw_The customerPass blockchain_wallet_id on the quote
Managed wallet (beta)bl_BlindPay custodies the balancePass 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.

Bash
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/customers/re_000000000000/blockchain-wallets \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "John personal wallet",
    "network": "base_sepolia",
    "is_account_abstraction": true,
    "address": "0x..."
  }'

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.

Bash
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/customers/re_000000000000/virtual-accounts \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "banking_partner": "<banking_partner>",
  "token": "USDC",
  "blockchain_wallet_id": "bw_000000000000"
}'

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.

Bash
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/payin-quotes \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "blockchain_wallet_id": "bw_000000000000",
    "currency_type": "sender",
    "cover_fees": true,
    "request_amount": 10000,
    "payment_method": "ach",
    "token": "USDB"
  }'

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:

Bash
curl https://api.blindpay.com/v1/instances/in_000000000000/payins/evm \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Idempotency-Key: <your_uuid>' \
  --data '{
  "payin_quote_id": "qu_000000000000"
}'

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.

Bash
curl --request POST \
  --url https://api.blindpay.com/v1/instances/in_000000000000/webhook-endpoints \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "url": "https://example.com/webhook",
    "events": []
  }'

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 statusWhat it meansWhat your system should do
payin.newPayin created, or a virtual account deposit arrivedCreate your payment record in a pending state
payin.updateAn intermediate step, such as an arrival check or reviewUpdate tracking detail, no customer message needed
payin.complete with completedStablecoins delivered to the walletCredit the customer, once
payin.complete with refundedDeposit returned to the senderClose the payment, notify the customer
payin.complete with failedThe payin did not go throughClose the payment, inspect before messaging
status: on_holdHeld for manual reviewKeep 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_amountResult
66600 ($666.00)failed
77700 ($777.00)refunded
Any other amountcompleted 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.

SymptomLikely causeFix
missing_required_fields on virtual account createBusiness customer is missing fields such as account_purposeUpdate the customer, then retry
self_custody_required on wallet createBrazilian customer, no is_self_custodySend true or false
request_amount_must_be_a_whole_currency_unitMXN, COP, or ARS amount not a multiple of 100Round to a whole currency unit
429 with development_instance_rate_limit_exceededMore than 40 requests per minuteBack off using the Retry-After header
Payin create is rejectedQuote older than 5 minutesCreate 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.

  1. Submit real KYC documents for every customer.
  2. Change token from USDB to USDC or USDT, and use mainnet networks.
  3. Remove any logic that relies on the 666.00 and 777.00 test amounts.
  4. Create a new webhook endpoint on the production instance. Development webhook settings do not carry over.
  5. 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

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

FAQ

One API.
Multiple payment rails.

Blind Pay, Inc. (“BlindPay”) is a United States-based financial technology and payments infrastructure company, incorporated in the State of Delaware on May 2, 2024. BlindPay is duly registered as a Money Services Business (“MSB”) with the Financial Crimes Enforcement Network (“FinCEN”) under registration number 31000277566153. BlindPay is not a state-licensed money transmitter and does not, in its own capacity, engage in regulated money transmission services. All such regulated activities are conducted exclusively through, and under the licenses of, duly authorized financial institution partners. BlindPay's role is limited to providing financial technology, virtual account management, payment processing, and related payments infrastructure services that facilitate payment flows and account functionalities. These services are provided in coordination with its licensed partners and in compliance with applicable federal MSB requirements and relevant state regulatory frameworks.

Blind Pay, LLC (“BlindPay”) is a Delaware limited liability company formed on June 24, 2025. BlindPay is duly registered as a Money Services Business (“MSB”) with the Financial Crimes Enforcement Network (“FinCEN”) under registration number 31000318334247 and is listed in the Nationwide Multistate Licensing System (“NMLS”) under NMLS ID #2745309.

BLIND PAY SOCIEDADE PRESTADORA DE SERVIÇOS DE ATIVOS VIRTUAIS LTDA. (“BlindPay”) is a Virtual Asset Service Provider (“VASP”) operating in Brazil. The company is presently completing the regulatory adaptation process required of Virtual Asset Service Provider Companies (SPSAV) under Central Bank of Brazil Resolution No. 520/2025, and remains subject to the transitional regime set forth in Article 88 of that Resolution. Registered address: Avenida Princesa Isabel, nº 15, Sala 1405, Edifício Martinho de Freitas, Centro, Vitória/ES, 29010-361.

BlindPay is a stablecoin API that enables individuals and businesses to move money worldwide.