Enterprise feature. Contact us for access.

Introduction

This guide will show you how to accept credit card payments using Crossmint’s Headless Checkout API for memecoin sales with Checkout.com as the payment provider. You’ll learn how to:
  • Set up credit card payments for Solana memecoin purchases in JavaScript
  • Implement a checkout UI using Checkout.com’s Flow component
  • Track order status and delivery
For a faster, embedded checkout solution with minimal setup time, see our embedded memecoin quickstart.

Important Notes

Compliance Checks

Crossmint runs compliance checks on all tokens to ensure they do not qualify as securities or currencies under applicable regulations. Transactions for tokens that are determined to be too similar to securities or currencies will fail.

Supported Tokens

Currently, memecoin checkout only supports Solana network. You can check which tokens are supported by using the fungibleCheckoutAvailable endpoint. A more in depth guide on token support is here.

Delivery to External Wallets Only

Memecoin checkout only delivers memecoins to EOAs (Externally Owned Accounts), not Crossmint supported delivery solutions, such as on-the-fly wallet creation (both Crossmint custodial wallets and smart wallet), delivery to Twitter handle, etc.

Merchant of Record

Crossmint remains the merchant of record for all transactions. Your buyers will still receive delivery receipts and transaction confirmations from Crossmint.

Prerequisites

1

Solana Wallet

Have a Solana wallet address ready to receive purchased memecoins
2

Get API Keys

Get your API keys from the Crossmint Console
Navigate to the "Integrate" section on the left navigation bar, and ensure you're on the "API Keys" tab.Within the Client-side keys section, click the "Create new key" button in the top right.On the authorized origins section, enter http://localhost:3000 and click "Add origin".Next, check the scopes labeled orders.create, orders.read, orders.update.Finally, create your key and save it for subsequent steps.

Fungible Token Specification

To define which fungible token you'd like to purchase, you'll need to specify the tokenLocator in the format:
  • For Solana: solana:${tokenMintHash}
    • Example: solana:6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN
    • tokenMintHash: The token mint hash (commonly known as contract address, CA, or mint hash)
  • For EVM chains (Ethereum, Polygon, Arbitrum, Base, etc.): <blockchain>:<contractAddress>:<tokenId>
    • Example: ethereum:0x1234567890123456789012345678901234567890:1
    • blockchain: The chain name (ethereum, polygon, arbitrum, base, etc.)
    • contractAddress: The token contract address (40 hexadecimal characters)
    • tokenId: For fungible tokens, use "1" as a placeholder value (required by validation schema)
EVM Fungible Token Format: While fungible tokens don't conceptually have individual token IDs, Crossmint's validation schema requires the 3-part format <blockchain>:<contractAddress>:<tokenId> for all EVM tokens. For fungible tokens (memecoins), use "1" as the tokenId value.

Headless Memecoin Checkout

The headless checkout API allows complete control over your checkout experience, including:
  • Custom UI components and styling
  • Custom payment flow sequences
  • Integrated analytics and tracking
  • Custom error handling and retry logic
  • Branded confirmation pages

Create an Order

The first step in the headless checkout process is to create an order. An order is an object datastructure that represents an intent to purchase in Crossmint’s systems. This guide will create a basic order, and then update it with required info step-by-step.
You can also create the entire order in one API call if the necessary information is available at the time of order creation. This can be used for custom “one-click-checkout” experiences, should you wish to make them.
Endpoint: POST https://www.crossmint.com/api/2022-06-09/orders Refer to the complete create order API reference here.
Memecoins are now testable in staging using the xmeme token (7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu). All other token purchases will fail in staging. For production launch with other tokens, contact our sales team.
Use the JavaScript code snippet below to create a starting point for your order. Alternatively, use the API playground to explore and create your own order.
const apiKey = 'your-server-api-key';                                    // CHANGE THIS TO YOUR SERVER API KEY
const tokenId = 'solana:7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu'; // xmeme token for staging
const deliveryAddress = 'your-solana-wallet-address';                   // CHANGE THIS TO YOUR RECEIVING SOLANA WALLET ADDRESS
const receiptEmail = 'your-email@example.com';                          // CHANGE THIS TO YOUR EMAIL

const options = {
method: 'POST',
headers: {
'X-API-KEY': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
lineItems: {
tokenLocator: tokenId, // Token address in format solana:tokenAddress (e.g., solana:7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu for xmeme token)
executionParameters: {
mode: "exact-in", // The execution method for the order. It tells Crossmint to operate in buying fungibles mode
amount: "1", // default currency USD
maxSlippageBps: "500" // Optional, or else default autogenerated slippage will be applied
}
},
payment: {
method: "checkoutcom-flow", // Using Checkout.com as the payment processor
receiptEmail: receiptEmail
},
recipient: {
walletAddress: deliveryAddress
}
})
};

fetch('https://staging.crossmint.com/api/2022-06-09/orders', options)
.then(response => response.json())
.then(response => console.log(JSON.stringify(response, null, 2)))
.catch(err => console.error(err));

Note the following parameters in the request body:
  • maxSlippageBps: Optional, or else default autogenerated slippage will be applied
  • receiptEmail: Required for credit card payments to deliver receipt
  • executionParameters.mode: The execution method for the order. “exact-out” is for NFTs, “exact-in” is for fungible tokens

Render the Checkout.com Flow Component

After creating an order, you’ll need to render the Checkout.com Flow component to collect payment information. The Flow component is a pre-built UI that handles the payment collection process. Reference Documentation:
import { useEffect, useState } from 'react';
import Script from 'next/script';
import Image from 'next/image';
import { Spinner } from '@/components/ui/spinner';

export function CheckoutComEmbedded({ embeddedCheckoutParameters }) {
  const { createOrder, order } = useOrder();
  const [isCheckoutReady, setIsCheckoutReady] = useState(false);
  const [isScriptLoaded, setIsScriptLoaded] = useState(false);

  useEffect(() => {
    async function initiateOrder() {
      try {
        await createOrder(embeddedCheckoutParameters);
      } catch (error) {
        console.error("Failed to create order:", error);
      }
    }

    initiateOrder();
  }, [embeddedCheckoutParameters, createOrder]);

  useEffect(() => {
    if (order == null) {
      return;
    }

    console.log("order", order);
  }, [order]);

  useEffect(() => {
    if (order == null) {
      return;
    }

    const initializeCheckout = async () => {
      try {
        if (typeof window.CheckoutWebComponents !== 'function') {
          console.error('CheckoutWebComponents not loaded properly');
          return;
        }

        const checkout = await window.CheckoutWebComponents({
          appearance: {
            colorBorder: "#FFFFFF",
            colorAction: '#060735',
            borderRadius: ["8px", "50px"],
          },
          publicKey: order.payment.preparation.checkoutcomPublicKey,
          environment: "sandbox", // Change to "live" for production
          locale: "en-US",
          paymentSession: order.payment.preparation.checkoutcomPaymentSession,
          cors: {
            mode: 'no-cors',
            credentials: 'same-origin'
          },
          onReady: () => {
            console.log("Flow is ready");
            setIsCheckoutReady(true);
          }, // checkout.com takes a second to load, so need to wait for it to render
          onPaymentCompleted: (component, paymentResponse) => {
            console.log("Payment completed with ID:", paymentResponse.id);
          },
          onChange: (component) => {
            console.log(`Component ${component.type} validity changed:`, component.isValid());
          },
          onError: (component, error) => {
            console.error("Payment error:", error, "Component:", component.type);
          },
        });

        const flowComponent = checkout.create("flow");
        const container = document.getElementById("flow-container");
        if (container) {
          flowComponent.mount(container);
        }
      } catch (error) {
        console.error("Error initializing checkout:", error);
      }
    };

    // Initialize checkout when the script is loaded and payment session exists
    const scriptElement = document.querySelector('script[src*="checkout-web-components"]');
    if (scriptElement) {
      initializeCheckout();
    }
  }, [order]);

  if (!order) {
    return (
      <div className="w-full max-w-[400px] sm:max-w-[600px] md:max-w-[800px] mx-auto px-6 md:px-10">
        <div className="bg-white p-6 md:p-10 rounded-lg">
          <div className="flex flex-col items-center justify-center py-10">
            <Spinner size="large" />
            <p className="text-muted-foreground mt-4">Loading checkout...</p>
          </div>
        </div>
      </div>
    );
  }

  return (
    <div className="w-full max-w-[400px] sm:max-w-[600px] md:max-w-[800px] mx-auto px-6 md:px-10">
      <div className="bg-white p-6 md:p-10 rounded-lg">
        <Script
          src="https://checkout-web-components.checkout.com/index.js"
          strategy="afterInteractive"
          onLoad={() => {
            console.log("Checkout.com script loaded");
            setIsScriptLoaded(true);
          }}
          onError={(e) => {
            console.error("Error loading Checkout.com script:", e);
          }}
        />
        <div className="flex-grow">
          <div id="flow-container" className="w-full"></div>
        </div>
        {isCheckoutReady && (
          <div className="text-center mt-4 text-sm" style={{ color: 'rgb(102, 102, 102)' }}>
            <p>
              By continuing, you accept{' '}
              <Image
                src="/crossmint-logo.svg"
                alt="Crossmint"
                width={16}
                height={16}
                className="inline-block mx-1"
              />
              <a
                href="https://www.crossmint.com/legal/terms-of-service"
                target="_blank"
                rel="noopener noreferrer"
                className="underline hover:text-gray-800"
              >
                Crossmint's terms
              </a>
            </p>
          </div>
        )}
      </div>
    </div>
  );
}

// Add this to your global declarations
declare global {
  interface Window {
    CheckoutWebComponents: any;
  }
}

Submit Payment

The Checkout.com Flow component handles the payment submission process automatically. When a user completes the payment form and clicks the payment button, the onPaymentCompleted callback will be triggered with the payment response. Unlike with Stripe, you don’t need to manually submit the payment form. The Checkout.com Flow component takes care of the entire payment process, including validation, submission, and handling the response. Here’s how the payment flow works with Checkout.com:
  1. The user fills out the payment form rendered by the Flow component
  2. The user clicks the payment button in the Flow component
  3. The Flow component validates the payment information
  4. If valid, the Flow component submits the payment to Checkout.com
  5. The onPaymentCompleted callback is triggered with the payment response
  6. You can use the payment response to update your UI and proceed to the next step
Handling Payment Success:
// Example of handling the payment completion
onPaymentCompleted: (component, paymentResponse) => {
  console.log("Payment completed with ID:", paymentResponse.id);
  // Update your UI to show payment success
  setPaymentStatus('success');
  // Proceed to the next step (e.g., order confirmation)
  navigateToOrderConfirmation(orderId);
},
If there’s an error during the payment process, the onError callback will be triggered:
onError: (component, error) => {
  console.error("Payment error:", error, "Component:", component.type);
  // Update your UI to show payment failure
  setPaymentStatus('error');
  // Display error message to the user
  setErrorMessage(error.message || 'Payment failed. Please try again.');
},

Poll for Status Updates

After making the payment, you’ll need to poll the Get Order API to check on the delivery status and present this information to your user. Endpoint: GET https://staging.crossmint.com/api/2022-06-09/orders/<orderId> Refer to the complete get order API reference here. Example Response:
{
    "id": "order_xyz",
    "status": "completed",
    "phases": {
        "quote": { "status": "completed" },
        "payment": { "status": "completed" },
        "delivery": { "status": "completed", "details": "Memecoins delivered to specified wallet" }
    }
}

Handling Refunded Payments

When polling for order status, you may encounter a situation where payment.status is completed but the order also contains a payment.refunded property. This indicates that the payment was initially successful but has since been refunded.
{
    "order": {
        "payment": {
            "status": "completed",
            "refunded": {
                "amount": "1.00",
                "currency": "usd",
                "txId": "0x1234abcd...",
                "chain": "ethereum"
            }
        }
    }
}
The payment.refunded object includes the following fields:
  • amount: The amount that was refunded
  • currency: The currency of the refund
  • txId: The on-chain transaction ID the refund was sent in
  • chain: The blockchain where the refund transaction occurred
When you encounter this state, your application should:
  1. Display an appropriate message to the user indicating that their payment was refunded
  2. Provide the transaction ID (txId) so users can verify the refund on-chain
  3. Prevent any further actions related to the order (such as delivery expectations)
  4. Provide options for the user to place a new order if desired
This state typically occurs when there was an issue with processing the order after payment was received, such as insufficient liquidity for memecoin purchases or compliance issues. 🎉 Congratulations! You’ve successfully set up your headless memecoin checkout. Check out the Next Steps section below to learn how to customize your integration.

Understanding the Code

Quote Expiration

Price quotes are valid for 30 seconds. After expiration, you'll need to request a new quote from the embedded checkout component

Slippage

Crossmint applies the slippage specified in your executionParameters.maxSlippageBps. If not provided, Crossmint will use the default slippage configuration (typically 500 BPS or 5%) from Crossmint's provider

Next Steps

Order Lifecycle

The order goes through several phases: Learn more about order phases in the headless checkout guide or embedded checkout guide A summary of the phases is below:
  1. Quote Phase (30-second validity)
    • Initial price quote generated
    • Requires recipient information to proceed
  2. Payment Phase
    • Collect payment information (via Crossmint's embedded UI, or your own Checkout.com Flow component if using headless checkout)
    • Process credit card payment
    • Handle payment completion and errors
  3. Delivery Phase
    • Purchase memecoin with USDC
    • Apply specified slippage tolerance
    • Send transfer transaction to recipient wallet
  4. Completion
    • Order marked as completed
    • Receipt email sent to recipient
    • Memecoins have been delivered to the recipient wallet
If the quote expires (after 30 seconds), you’ll need to create a new order to get updated pricing. You can choose the UX flow to handle this.

Refreshing Orders with Checkout.com

If an order expires before payment is completed (e.g., the 30-second quote validity period ends), you can either create a new order or use the refresh quote API:
async refreshOrder(orderId, clientSecret) {
  try {
    const ancestorOrigins = typeof window !== 'undefined' && window.location?.ancestorOrigins
      ? Array.from(window.location.ancestorOrigins)
      : [];

    const response = await this.callApi(`2022-06-09/orders/${orderId}/refresh`, "POST", {}, {
      "authorization": `${clientSecret}`,
      "x-ancestor-origins": JSON.stringify(ancestorOrigins)
    });

    const parsed = await response.json();
    return parsed;
  } catch (error) {
    console.error("Error refreshing quote:", error);
    throw error;
  }
}

FAQ