Enterprise feature. Contact us for access.

Introduction

This guide will show you how to accept credit card payments using Crossmint’s Embedded Checkout or 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

You can jump to the sections on the right toolbar to get integrate with either:

  • Option 1, using Embedded Checkout, or
  • Option 2, using Headless Checkout.

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. If you’re an enterprise customer, you can check which tokens are supported by using the fungibleCheckoutAvailable endpoint, or reach out to support for an updated list of supported tokens.

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 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 Server-side keys section, click the "Create new key" button in the top right.

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 tokenLocator format: solana:${tokenMintHash} (tokenMintHash is commonly known as contract address, CA, or mint hash).

Option 1 - Embedded Memecoin Checkout

The fastest way to start selling memecoins is to adapt our embedded checkout solution to use fungibles.

Important Parameters

Before implementing the checkout, note these key parameters:

  • maxSlippageBps: Optional slippage tolerance (default provided if not specified)
  • receiptEmail: Required for delivering payment receipts
  • executionParameters.mode: Set to “exact-in” for memecoin purchases (specifies exact USD amount to spend). Exact-out is for NFT’s, exact-in is for fungibles.

    Integration Steps

    This guide will start from scratch with an empty Next.js application. You'll install the required @crossmint/client-sdk-react-ui dependency and add the embedded checkout component. To get started:

    Set up the Project

    1

    Create a new Next.js application

    npx create-next-app@latest
    

    If you see this message, type y and press Enter to proceed:

    Need to install the following packages:
      create-next-app@latest
    Ok to proceed? (y)
    
    2

    Name your app `crossmint-embedded-checkout-demo` and accept the default options

    What is your project named? crossmint-embedded-checkout-demo
    Would you like to use TypeScript? Yes
    Would you like to use ESLint? Yes
    Would you like to use Tailwind CSS? Yes
    Would you like to use `src/` directory? No
    Would you like to use App Router? (recommended) Yes
    Would you like to customize the default import alias? No
    
    3

    Change into the directory created in previous steps

    cd crossmint-embedded-checkout-demo
    
    4

    Install @crossmint/client-sdk-react-ui

    npm i @crossmint/client-sdk-react-ui
    
    5

    Open the project in your preferred code editor

Memecoin Embedded Integration

Next, we will set up a project file with Crossmint’s embedded checkout to accept memecoin purchases.

1

Add environment variables

Create .env.local in your project root:

NEXT_PUBLIC_CLIENT_API_KEY="_YOUR_CLIENT_API_KEY_"    # From API Keys page
NEXT_PUBLIC_TOKEN_ADDRESS="6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN" # Instead of a collectionId, we will use a token address. In this case, $trumps contract address
NEXT_PUBLIC_RECIPIENT_WALLET_ADDRESS="YOUR_SOLANA_WALLET_ADDRESS" # Add desired recipient wallet
NEXT_PUBLIC_RECEIPT_EMAIL="YOUR_EMAIL" # Add desired recipient email
2

Create the checkout page

Create /src/app/page.tsx with:

"use client";
import { CrossmintProvider, CrossmintEmbeddedCheckout } from "@crossmint/client-sdk-react-ui";
export default function Home() {
    const clientApiKey = process.env.NEXT_PUBLIC_CLIENT_API_KEY as string;
    const tokenAddress = process.env.NEXT_PUBLIC_TOKEN_ADDRESS as string;
    const recipientWalletAddress = process.env.NEXT_PUBLIC_RECIPIENT_WALLET_ADDRESS as string;
    return (
        <div className="flex flex-col items-center justify-start h-screen p-6 bg-white">
            <CrossmintProvider apiKey={clientApiKey}>
                <div className="max-w-[450px] w-full">
                    <CrossmintEmbeddedCheckout
                        recipient={{
                            walletAddress: recipientWalletAddress, // Wallet address to receive the memecoins
                        }}
                        lineItems={{
                            tokenLocator: `solana:${tokenAddress}`, // Token address in format solana:tokenAddress (e.g., solana:6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN for TRUMP token)
                            executionParameters: {
                                mode: "exact-in", // The execution method for the order. It tells Crossmint to operate in buying fungibles mode
                                amount: "5", // Amount in USD
                                maxSlippageBps: "500" // Optional - default slippage will be applied if not specified
                            }
                        }}
                        payment={{
                            receiptEmail: process.env.NEXT_PUBLIC_RECEIPT_EMAIL as string, // Email address to receive the receipt
                            crypto: {
                                enabled: false, // Only fiat is supported for memecoin purchases
                            },
                            fiat: {
                                enabled: true,
                            },
                            defaultMethod: "fiat",
                        }}
                    />
                </div>
            </CrossmintProvider>
        </div>
    );
}
3

Run your app

npm run dev

Visit http://localhost:3000 to see your checkout!

4

Test your app

Memecoin purchases are only supported in production, so pay with credit card in a small amount to test the flow.

Here’s how your embedded checkout will look after implementation: 🎉 Congratulations! You’ve successfully set up your embedded memecoin checkout. Check out the Next Steps section below to learn how to customize your integration.

Option 2 - Headless Memecoin Checkout

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.

POST https://staging.crossmint.com/api/2022-06-09/orders

Refer to the complete create order API reference here.

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:6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN'; // trump token
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,
executionParameters: {
mode: "exact-in", // The execution method for the order. It also 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://www.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 NFT’s, exact-in is for fungibles.

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.

Checkout.com docs for desktop

Checkout.com docs for mobile

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",
          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
// 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 via whichever payment method, you'll need to poll the Get Order API to check on the delivery status and present this info to your user.

Refer to the complete get order API reference here.

GET https://staging.crossmint.com/api/2022-06-09/orders/<orderId>

Example response from the Get Order API:

{
    "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 headless checkout API

Slippage

Crossmint applies the slippage specified in your API request via executionParameters.slippageBps. If not provided, Crossmint will use the default slippage configuration from Crossmint’s provider

Next Steps

Order Lifecycle

The order goes through several phases: Learn more about order phases in the headless 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

    • Process credit card payment via Checkout.com
    • Collect payment information using the Checkout.com Flow component
    • 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