Integrating a stablecoin on-ramp API means verifying the customer, fixing the conversion with a quote or a floating rate, creating the on-ramp, showing the payer where to send fiat, and following webhooks until the stablecoin reaches the customer's custodial wallet. The quote sets the price, the rail decides what the payer sees, and webhooks carry every status change.
This post covers the fiat-in leg at endpoint and event level. For the generic build, see how to integrate a stablecoin API, for vocabulary the stablecoin on-ramp explainer, and a coding agent can take the on-ramp prompt with the endpoints and enums.
What are the steps of a stablecoin on-ramp integration?
An on-ramp has four parts: who pays, at what price, where the fiat goes, and how you learn it arrived.
The on-ramp sequence in the Lumx API, read from docs.lumx.io on November 1, 2026.
Step | Call | What you store | Event you wait for |
1. Create the customer | POST /customers, with the currencies you need in the accounts array | Customer id, then the wallets and accounts arrays once approved | customer.approved |
2. Quote (optional) | POST /exchange-rates | exchangeRateId and expiresAt | None |
3. Create the on-ramp | POST /transactions/on-ramp, with an Idempotency-Key | Transaction id, your own reference in metadata | onramp.awaiting_funds |
4. Show payment details | Render state.payment from the response | Nothing new | None |
5. Follow the lifecycle | Webhooks, then GET /transactions/{id} before you credit anyone | Each status with its eventId | onramp.transferring_fiat, onramp.trading, onramp.transferring_stablecoin, onramp.success |
6. Reconcile | List transactions and match your reference | Differences, if any | None |
Lumx runs this over REST and webhooks, as the on-ramp API reference shows. Sandbox is at api-sandbox.lumx.io and production at api.lumx.io, with a server-side bearer key.
Verify the customer before anything else
Every on-ramp belongs to an approved customer. Create the customer with the fiat currencies you will need in the accounts array, because each virtual account is created with the customer and starts provisioning only after verification. Request the account of each currency before you collect in it. A business customer needs its documents and associated parties uploaded before verification starts, while an individual starts automatically after the liveness check.
Do not quote early: a call for a customer that is not approved fails with KYC_NOT_APPROVED (HTTP 403). Listen to customer.approved, customer.rfi and customer.final_rejection instead of polling. An RFI (request for information, a compliance hold that asks for a document) pauses the customer until you send what was asked.
Account activation varies by currency. BRL, MXN, EUR and GBP accounts activate right after approval, and USD takes up to one business day while the banking partner reviews. The account.active event says when an account can receive funds. KYB (know your business, the verification of a company and its owners) targets one business day at Lumx, a target and not a promise.
Which quote type should you use?
The same call takes two shapes, and the choice is a product decision. A floating rate is a real-time reference with no lock: you send the rail, the source currency and amount, the target stablecoin and the purpose, and the price applies at execution. A locked rate is a quote you request first, which returns an id and an expiry, and you then create the on-ramp from that id alone.
Floating and locked rates for an on-ramp, per the Lumx exchange rates and transactions docs, November 1, 2026.
Floating | Locked | |
Quote call needed | No | Yes, POST /exchange-rates |
Price | Reference rate, may move slightly by execution | Fixed for the lock window |
Lock window | Not applicable | 30 seconds, 1 minute or 5 minutes |
Fee for the lock | None | None at 30 seconds, a fee at 1 and 5 minutes |
Amount you give | Source amount | Source or target amount |
Payer window to send fiat | 1 day on Pix and SPEI, 7 days on other rails | Equals the quote expiry |
The last row is where the two diverge. A floating rate has no lock, so its payment window is a fixed number of days. A locked rate dies with its quote, and the docs say the on-ramp payment window matches the quote expiration, so expiresAt works as both deadlines: you must create the on-ramp before it, and the payer must pay before it. If the lock expires before you create the on-ramp, the call fails with EXCHANGE_RATE_EXPIRED (HTTP 422) and you request a new quote. Never retry the old id.
Use floating when the amount can wander a little, locked when the payer must see the exact stablecoin amount first. The 30-second lock has no fee and suits a payer already inside your checkout. A payer who has to open a bank app or place a wire cannot pay in 30 seconds, so give that payer a longer lock, which carries a fee, or a floating rate. A partner fee (basis points, a flat amount or both) applies only when you send its partnerFeeId in the request, it has no cap and does not vary by corridor, and it sits on top of what the payer funds: on a BRL to USDC on-ramp it raises the BRL to send for the same USDC credited. A locked quote returns a fees object with separate Lumx, partner and total lines, and finalTargetAmount is already all-in, so you can show the payer the final amount before they pay.
What does the payer see on each rail?
The on-ramp response returns state.payment, and its shape follows the rail. A Pix (Brazil's instant payment system, run by the Central Bank) code is a string to copy, a SPEI (Mexico's interbank transfer system, run by Banxico) transfer needs a CLABE (the 18-digit Mexican bank account number), and a wire is a block of details typed into another app, so keep a screen state for each.
On-ramp rails and what state.payment carries, from the Lumx API reference and cut-off page, November 1, 2026.
Rail | Currency | What the payer is shown | Operating days and cut-off |
Pix | BRL | A brCode, as copyable text or a QR code | 24/7/365, no cut-off |
SPEI | MXN | A CLABE | 24/7/365, no cut-off |
ACH | USD | Account and routing numbers, plus holder and bank | US business days, 2:00 PM ET cut-off |
FEDWIRE | USD | Bank details with a memo | US business days, 3:00 PM ET cut-off |
SWIFT | USD | IBAN and BIC, the holder and bank, and a memo | Subject to intermediary bank hours, 3:00 PM ET cut-off |
SEPA | EUR | IBAN, BIC and holder | Lumx routes between SEPA Instant and SEPA Credit Transfer, you always send rail SEPA |
FPS (Faster Payments, the UK's real-time bank transfer network) | GBP | Sort code and account number | 24/7/365, up to GBP 1M per payment |
Treat the fields as a free-form object per rail. Store the memo or reference with your own order, because the bank statement is where a payer's question starts. For wire rails the response carries the memo in state.payment.memo.
Colombian pesos do not go through this call. COP is not one of the rail values on POST /transactions/on-ramp. It is collected into the customer's COP account, by Bre-B (Colombia's instant payment system), where the payer transfers to a Bre-B key on that account, or by PSE (Colombia's online bank-transfer network), where the payer authorizes the debit. The deposit converts to stablecoin by autoconversion rule. Bre-B carries a cap of 1,000 UVB per transaction, set by the Banco de la República (Circular Reglamentaria Externa DSP-465, Asunto 18, numeral 3.2, version of March 31, 2026), and it applies to every provider. A COP account is available to any customer and is named only when the customer is Colombian.
For a payer who sends often, a named virtual account (a local account number issued in the customer's own name) and a standing rule beat one on-ramp per payment. The named virtual account explainer and the guide on B2B collections cover when that fits. Lumx issues named accounts in BRL and MXN to foreign companies with no local entity (see named virtual accounts). It delivers to Ethereum, Polygon, Base and Tron, the project's default network when you omit blockchain, and the coverage page shows which stablecoin runs where.
Drive the lifecycle from webhooks
Do not poll. Every on-ramp state change arrives as a signed HTTP POST with an eventId, an eventType and a data object holding the transaction. Each status has its own event, in a fixed order.
On-ramp events from the Lumx webhooks docs, November 1, 2026.
Event | Triggered when | What your system does |
onramp.awaiting_funds | Waiting for the payer to send funds | Show payment details, start your own timeout |
onramp.transferring_fiat | Fiat transfer in progress | Record that the transfer is in progress, release no value |
onramp.trading | Conversion is executing | Nothing, unless you display progress |
onramp.transferring_stablecoin | Stablecoin on its way to the wallet | Nothing, unless you display progress |
onramp.success | Completed | Read the transaction back, then credit |
onramp.failed | Failed | Open a case, do not retry blindly |
onramp.expired | Expired before payment was received | Close the order, offer a new one |
Verify the signature on the raw body. Lumx signs the webhook-id, the webhook-timestamp and the body, joined by dots, with HMAC-SHA256, and the docs reject anything older than five minutes. Accept any valid signature in the header, because for 24 hours after a secret rotation Lumx signs with both secrets. Answer with a 2xx fast, process afterwards, and deduplicate on webhook-id. Depth on signatures and retries: stablecoin API webhooks.
Retries are the reason for that last rule. If your endpoint does not return a 2xx, Lumx retries with exponential backoff over eight attempts: immediately, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours and twice at 10 hours. A handler that credits on every delivery credits twice. The same discipline applies to the create call: send an Idempotency-Key with a UUID v4, saved before the request leaves, so a retry returns the cached response with X-Idempotency-Cached set to true. Keys expire after 24 hours. The webhook prompt generates the signature code.
Reconcile before you credit a user
I tell teams to treat the webhook as a prompt to look, not as proof. When onramp.success arrives, read the transaction back and match it on your own reference before a user sees a balance. The dispute that costs the most time is the payer who says they paid when your system shows nothing, and a ledger settles it, not an event stream. I would rather a team write that comparison job in week one than explain a missing credit in week ten.
Put your order id in the metadata when you create the on-ramp: Lumx accepts any JSON object there and returns it on the transaction. Run a daily job that lists transactions and compares status and amounts against your ledger by that reference. Treat late and failed as different states. Each completed conversion has a receipt with the rate and amounts to compare against.
Late, held or never: what happens to the deposit
If the payer does not pay inside the payment window, the transaction expires and onramp.expired fires. A deposit that lands after expiry is not processed: Lumx refunds it to the original sender on the same rail, and a new on-ramp is needed if the customer still wants to convert.
A third party can fund the on-ramp when that fits the flow of funds you expect, and an odd payer can raise an RFI. Layered or nested structures need written approval from compliance@lumx.io first. A deposit can also be held. Transactions flagged by monitoring wait for review, and large transfers can add up to two business hours of risk review. A wrong purpose can cause the same hold: every on-ramp carries a purpose, the end-use category of the funds, used for AML screening. Pick the value that matches what the money is for.
Map MINIMUM_AMOUNT_NOT_MET and TRANSACTION_LIMIT_EXCEEDED (both HTTP 422) too, and match on the code, not the message.
Test the whole path in sandbox
Sandbox runs on testnets against mock banking partners, so no real money moves. The last digit of the tax ID drives the customer outcome: 1 leaves verification not started, 2 returns an RFI, 3 is a final rejection, and any other digit approves.
For the deposit leg, Lumx offers a sandbox-only simulated deposit on autoconversion rules. It runs a deposit through the conversion without a bank transfer and returns 404 in production, so keep it out of shared code. Then run one small on-ramp per rail in production and watch the Lumx changelog.
When an API integration is the wrong tool
Skip the API when a hosted flow or the dashboard already does the job. If you move money a few times a month, a person in the dashboard is cheaper than a maintained integration. If one country and one rail is the whole requirement, the Lumx dashboard needs no build, while the API route takes two to three weeks to go-live. If the payer is the same business every month, a standing conversion rule needs no per-payment call.
Lumx offers that rule: set it up once on an account, and every matching deposit converts to USDC or USDT with no API call.
What this looks like with Lumx
Lumx is stablecoin payments infrastructure for businesses that move money between Latin America and the rest of the world: one API to collect, hold, convert, and pay out in BRL, MXN, COP, USD, EUR, and GBP or in USDC and USDT, over local rails such as PIX, SPEI, PSE, Bre-B, ACH, FEDWIRE, SEPA, and Faster Payments, with SWIFT and on-behalf-of payments and collections (POBO and COBO) in USD, EUR, and GBP, plus named virtual accounts, custodial wallets, and KYB/KYC built in.
For the on-ramp that means the customer, its accounts and its custodial wallet come from one create call, and the stablecoin lands in that wallet. A Pix deposit is the cleanest case: the BRL to USDC corridor takes a brCode from the on-ramp response and returns USDC, and the MXN to USDC corridor does the same with a CLABE. The treasury view is in converting BRL to USDC, and the wider surface on the global payments page.
Pricing, in the words Lumx uses: monthly platform fee plus usage-based per-transaction fees, transparent FX on locked quotes, no pre-funding; no published plans. A docs MCP server lets a coding agent read real field names instead of inventing them.
Verified on November 1, 2026. Operational context, not legal, tax, or investment advice.
Cover photo: Joshua Kettle on Unsplash.
How do you integrate a stablecoin on-ramp API?
Create and verify the customer, quote or use a floating rate, create the on-ramp with an idempotency key, show the payer the payment details for the rail, and follow webhooks until success, then read the transaction back and reconcile daily. On Lumx that is POST /customers, POST /exchange-rates when you want a lock, POST /transactions/on-ramp and seven onramp events, from onramp.awaiting_funds to onramp.success plus failed and expired.
How long does an on-ramp quote stay valid?
It depends on the quote type. A locked rate holds for 30 seconds, 1 minute or 5 minutes, and a floating rate has no lock and is a reference at execution. After a lock expires you request a new one. On Lumx the 30-second lock has no fee, 1 and 5 minutes carry one, an expired id returns EXCHANGE_RATE_EXPIRED, and a locked on-ramp expires with its quote.
Why use webhooks instead of polling for on-ramp status?
Deposits arrive on the payer's schedule, so webhooks report the change when it happens, while polling wastes calls or reacts late. Keep a daily reconciliation job as the backstop. Lumx sends signed events for every on-ramp status, retries over eight attempts, and lets you resend a failed one from the Dashboard.
What happens if the payer sends money after the on-ramp expires?
The late deposit is not converted. It goes back to the original sender on the same rail, and a new on-ramp is needed if the customer still wants stablecoin. On Lumx the window is 1 day for floating Pix and SPEI, 7 days for floating on other rails, and the quote expiry for a locked rate.
Can you test an on-ramp integration without real money?
Yes, on a sandbox with mock banking partners and test stablecoins on testnets. Force the customer outcomes you cannot wait for, and run each through your handler. On Lumx the last digit of the tax ID picks the verification result, a simulated deposit runs a conversion on an autoconversion rule, and the simulation returns 404 in production.





