Skip to main content
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:
ChainStagingProduction
Solana4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDUEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
Base0x036CbD53842c5426634e7929541eC2318f3dCF7e0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
🚨 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"
        }
    }
}
alt text
alt text

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;
};
{
    "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
I