You can start testing Onramp in staging. Once you are ready to go live, reach out to
sales to enable the feature in production
Crossmint’s Headless Checkout API lets you build a seamless onramp so users can buy crypto with a credit card.
In this guide, you’ll learn how to:
Create a Crossmint order for a user authenticated in your app
Run the KYC process when required
Integrate the payment platform to accept credit‑card payments
Track the order status at each step
1. Prerequisites
Create a server-side API key with the orders.create
and orders.read
scopes enabled
2. Create Order
Use the Create Order API to initiate the purchase process.
Use the following token addresses for USDC:
Chain Staging Production Solana 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU
EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
Base 0x036CbD53842c5426634e7929541eC2318f3dCF7e
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
🚨 IMPORTANT: The user email you send as a parameter (on payment.receiptEmail
) must correspond to a user authenticated
by your application. This email is used by Crossmint to determine whether KYC is required for the order.
Here’s an example API call:
const apiKey = "YOUR_API_KEY" ; // CHANGE
const tokenLocator = "solana:example-token-address" ; // Token address, e.g., USDC in staging: "solana:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
const receiptEmail = "user@example.com" ; // Replace with actual recipient email
const walletAddress = "example-wallet-address" , // Replace with actual recipient wallet address
const response = await fetch ( "https://staging.crossmint.com/api/2022-06-09/orders" , {
method: "POST" ,
headers: {
"Content-Type" : "application/json" ,
"x-api-key" : apiKey ,
},
body: JSON . stringify ({
lineItems: [
{
tokenLocator ,
executionParameters: {
mode: "exact-in" ,
amount: "2" ,
},
},
],
payment: {
method: "checkoutcom-flow" ,
receiptEmail
},
recipient: {
walletAddress
},
}),
});
const data = await response . json ();
{
"clientSecret" : "example-client-secret" ,
"order" : {
"orderId" : "example-order-id" ,
"phase" : "payment" ,
"locale" : "en-US" ,
"lineItems" : [
{
"chain" : "solana" ,
"metadata" : {
"name" : "USDC" ,
"description" : "USDC Token" ,
"imageUrl" : "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=040"
},
"quote" : {
"status" : "valid" ,
"charges" : {
"unit" : {
"amount" : "1.47" ,
"currency" : "usd"
}
},
"totalPrice" : {
"amount" : "2" ,
"currency" : "usd"
},
"quantityRange" : {
"lowerBound" : "1.36" ,
"upperBound" : "1.36"
}
},
"delivery" : {
"status" : "awaiting-payment" ,
"recipient" : {
"locator" : "solana:example-wallet-address" ,
"walletAddress" : "example-wallet-address"
}
},
"executionMode" : "exact-in" ,
"maxSlippageBps" : "0" ,
"executionParams" : {
"mintHash" : "example-mint-hash" ,
"mode" : "exact-in" ,
"amount" : "2"
}
}
],
"quote" : {
"status" : "valid" ,
"quotedAt" : "2025-03-07T23:04:11.996Z" ,
"expiresAt" : "2025-03-07T23:14:11.996Z" ,
"totalPrice" : {
"amount" : "2" ,
"currency" : "usd"
}
},
"payment" : {
"method" : "stripe-payment-element" ,
"currency" : "usd" ,
"status" : "requires-kyc" ,
"preparation" : {
"kyc" : {
"provider" : "persona" ,
"templateId" : "example-template-id" ,
"referenceId" : "example-reference-id"
}
}
}
}
}
3. Check KYC Requirements
After creating an order, check the response for KYC requirements:
If payment.status
is requires-kyc
, direct your user through the KYC flow
If payment.status
is awaiting-payment
, the user has already completed KYC and can proceed
4. Complete KYC Process
Crossmint uses Persona for KYC (Know Your Customer) verification. The Persona SDK allows you to embed the verification flow directly into your application.
Embedded experience
Send the parameters from order.payment.preparation.kyc
in the corresponding Persona parameters (e.g., templateId
, referenceId
, environmentId
).
📝 Note: Customization of the KYC flow’s look-and-feel is an enterprise feature. Please contact Crossmint support to enable this feature.
import Persona from "persona" ;
const personaConfig = response . order . payment . preparation . kyc ;
const client = new Persona . Client ({
// From previous response
templateId: personaConfig . templateId ,
referenceId: personaConfig . referenceId ,
environmentId: personaConfig . environmentId ,
onReady : () => client . open (),
onComplete : ({ inquiryId , status , fields }) => {
console . log ( `Finished inquiry ${ inquiryId } ` );
// Here we should start polling for order status updates
},
onCancel : ({ inquiryId , sessionToken }) => console . log ( "onCancel" ),
onError : ( error ) => console . log ( error ),
});
⚠️ Warning: snippet code bellow is only intended for frontend apps (i.e React or Nextjs)
For a mobile integration, check Persona’s React Native guide .
Hosted experience
If you want to redirect users to a URL, opening it in a WebView, you can construct the proper Persona URL given the IDs above.
Here is an URL example:
https://inquiry.withpersona.com/verify?inquiry-template-id =itmpl_eq9Za7FkEeqTiGwWUDRAihafYiku & reference-id = %7B%22referenceEntity%22%3A%7B%22type%22%3A%22userId%22%2C%22id%22%3A%22688a6cacdbb38888231282c2%22%7D%2C%22trigger%22%3A%7B%22type%22%3A%22order%22%2C%22orderId%22%3A%220454dfb6-29c1-419f-a96c-c32324f48e05%22%7D%7D & environment-id = env_YxXyTmeMbZrh6AaYSp79bbEm
5. Poll KYC Status
Poll Crossmint’s Get Order API to check the KYC verification status. The possible KYC status values are:
If payment.status
is "awaiting-payment"
, user has successfully completed KYC, proceed to payment
If payment.status
is "rejected-kyc"
, verification was rejected and the user can’t onramp
If payment.status
is "manual-kyc"
, verification requires manual review and the user will be notified via email about its ultimate success or rejection
const pollKYCStatus = async ( orderId ) => {
const response = await fetch ( `https://staging.crossmint.com/api/2022-06-09/orders/ ${ orderId } ` , {
method: "GET" ,
headers: {
"Content-Type" : "application/json" ,
"x-api-key" : "YOUR_API_KEY" ,
},
});
const order = await response . json ();
const paymentStatus = order . payment ?. status ;
// Handle different KYC statuses
if ( paymentStatus === "awaiting-payment" ) {
console . log ( "KYC completed successfully! Proceeding to payment." );
return { status: "success" , proceed: true };
} else if ( paymentStatus === "rejected-kyc" ) {
console . log ( "KYC was rejected. User cannot proceed with onramp." );
return { status: "rejected" , proceed: false };
} else if ( paymentStatus === "manual-kyc" ) {
console . log ( "KYC requires manual review. User will be notified via email." );
return { status: "manual" , proceed: false };
}
// Still processing
return { status: "processing" , proceed: false };
};
{
"orderId" : "example-order-id" ,
"payment" : {
"status" : "awaiting-payment" ,
"preparation" : {
"checkoutcomPaymentSession" : "example-session" ,
"checkoutcomPublicKey" : "example-key"
}
}
}
6. Execute Transactions
On awaiting-payment
status, initialize Checkout.com’s Flow component for Desktop or Mobile to render an embedded payment form using the payment session and public key obtained in the prior response.
// Example implementation using Checkout.com Flow component
import { loadCheckoutWebComponents } from "@checkout.com/checkout-web-components" ;
const initializePayment = ( checkoutcomPaymentSession , checkoutcomPublicKey ) => {
const checkout = await loadCheckoutWebComponents ({
publicKey: checkoutcomPublicKey ,
paymentSession: checkoutcomPaymentSession ,
environment: "sandbox" , // For staging only, remove for production
onPaymentCompleted : () => {
console . log ( "Payment successful" , event );
},
onError : ( _component : { type : string }, _error : Error ) => {
console . log ( "Payment failed" , event );
},
});
checkout . create ( "flow" ). mount ( "#payment-container" );
};
7. Poll Order for Completion
After payment is processed, use the same polling pattern from step 5 to check for order completion. The order is complete when both payment.status
is "completed"
and delivery.status
is "completed"
.
const pollOrderCompletion = async ( orderId ) => {
const response = await fetch ( `https://staging.crossmint.com/api/2022-06-09/orders/ ${ orderId } ` , {
method: "GET" ,
headers: {
"Content-Type" : "application/json" ,
"x-api-key" : "YOUR_API_KEY" ,
},
});
const order = await response . json ();
// Check if order is completed
const isPaymentCompleted = order . payment ?. status === "completed" ;
const isDeliveryCompleted = order . lineItems ?.[ 0 ]?. delivery ?. status === "completed" ;
if ( isPaymentCompleted && isDeliveryCompleted ) {
console . log ( "Order completed successfully!" );
console . log ( "Transaction ID:" , order . lineItems [ 0 ]. delivery . txId );
return true ;
}
return false ;
};
Example Completed Order Response
{
"orderId" : "ed89e9ae-3589-4d2c-81e4-ebab5d76bb86" ,
"phase" : "completed" ,
"locale" : "en-US" ,
"lineItems" : [
{
"chain" : "solana" ,
"metadata" : {
"name" : "USDC" ,
"description" : "USDC Token" ,
"imageUrl" : "https://cdn.mcauto-images-production.sendgrid.net/60dce2e0bed4c8e7/b87aae79-3583-4a07-83d1-32f9d99818c2/300x300.png"
},
"quote" : {
"status" : "valid" ,
"charges" : {
"unit" : {
"amount" : "1.2" ,
"currency" : "usd"
}
},
"totalPrice" : {
"amount" : "5" ,
"currency" : "usd"
},
"quantityRange" : {
"lowerBound" : "4.15" ,
"upperBound" : "4.15"
}
},
"delivery" : {
"status" : "completed" ,
"recipient" : {
"locator" : "solana:x4zyf8T6n6NVN3kBW6fmzBvNVAGuDE8mzmzqkSUUh3U" ,
"walletAddress" : "x4zyf8T6n6NVN3kBW6fmzBvNVAGuDE8mzmzqkSUUh3U"
},
"txId" : "4hHMknm7ZbpzgBhDVwccHft8zEhPJcCvKMDi77Uk1Mc7wUYCsb53co4QkMLnkN68xUAUQLt8F914p2CWWozJ1Sfq" ,
"tokens" : [
{
"mintHash" : "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" ,
"locator" : "solana:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" ,
"quantity" : "4.15"
}
]
},
"executionMode" : "exact-in" ,
"maxSlippageBps" : "0" ,
"executionParams" : {
"mintHash" : "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU" ,
"mode" : "exact-in" ,
"amount" : "5.00" ,
"effectiveAmount" : 4.15
}
}
],
"quote" : {
"status" : "valid" ,
"quotedAt" : "2025-09-19T10:21:49.710Z" ,
"expiresAt" : "2025-09-19T10:51:49.710Z" ,
"totalPrice" : {
"amount" : "5" ,
"currency" : "usd"
}
},
"payment" : {
"status" : "completed" ,
"method" : "checkoutcom-flow" ,
"currency" : "usd" ,
"preparation" : {
"checkoutcomPaymentSession" : {
"id" : "ps_32ujNRDEOG5FhrTwehYN7sNx3Km" ,
"payment_session_secret" : "pss_9e5c1ac4-b667-4040-a0cc-61dd0fcc1b20" ,
"payment_session_token": "YmFzZTY0:eyJpZCI6InBzXzMydWpOUkRFT0c1RmhyVHdlaFlON3NOeDNLbSIsImVudGl0eV9pZCI6ImVudF81cGxtcmxrMmpyb3JyZWRwcGw2cWI1ZG0ybSIsImV4cGVyaW1lbnRzIjp7fSwicHJvY2Vzc2luZ19jaGFubmVsX2lkIjoicGNfb3RnNndrY21yYWN1M2VkZGFlbDJ6NG4yZXUiLCJhbW91bnQiOjUwMCwibG9jYWxlIjoiZW4tVVMiLCJjdXJyZW5jeSI6IlVTRCIsInBheW1lbnRfbWV0aG9kcyI6W3sidHlwZSI6ImNhcmQiLCJjYXJkX3NjaGVtZXMiOlsiVmlzYSIsIk1hc3RlcmNhcmQiXSwic2NoZW1lX2Nob2ljZV9lbmFibGVkIjpmYWxzZSwic3RvcmVfcGF5bWVudF9kZXRhaWxzIjoiZGlzYWJsZWQiLCJiaWxsaW5nX2FkZHJlc3MiOnsiY291bnRyeSI6IlVTIn19XSwiZmVhdHVyZV9mbGFncyI6WyJhbmFseXRpY3Nfb2JzZXJ2YWJpbGl0eV9lbmFibGVkIiwiY2FyZF9maWVsZHNfZW5hYmxlZCIsImdldF93aXRoX3B1YmxpY19rZXlfZW5hYmxlZCIsImxvZ3Nfb2JzZXJ2YWJpbGl0eV9lbmFibGVkIiwicmlza19qc19lbmFibGVkIiwidXNlX25vbl9iaWNfaWRlYWxfaW50ZWdyYXRpb24iXSwicmlzayI6eyJlbmFibGVkIjpmYWxzZX0sIm1lcmNoYW50X25hbWUiOiJDcm9zc21pbnQgKFBhZWxsYSBFdXJvcGUgUy5MLikiLCJwYXltZW50X3Nlc3Npb25fc2VjcmV0IjoicHNzXzllNWMxYWM0LWI2NjctNDA0MC1hMGNjLTYxZGQwZmNjMWIyMCIsInBheW1lbnRfdHlwZSI6IlJlZ3VsYXIiLCJpbnRlZ3JhdGlvbl9kb21haW4iOiJhcGkuc2FuZGJveC5jaGVja291dC5jb20ifQ==",
"_links" : {
"self" : {
"href" : "https://api.sandbox.checkout.com/payment-sessions/ps_32ujNRDEOG5FhrTwehYN7sNx3Km"
}
}
},
"checkoutcomPublicKey" : "pk_sbox_da5vrweqqatedeoyrymxt5vb645"
},
"received" : {
"amount" : "5" ,
"currency" : "usd"
},
"receiptEmail" : "demos+onramp-existing-user@crossmint.com"
}
}
Upon successful order completition:
The purchased tokens (minus fees) are sent directly to the user’s wallet
User receives an email receipt from hello@crossmint.io