# Agent Checkouts Quickstart ⚡
Source: https://docs.crossmint.com/agents/agent-checkouts-quickstart
Start a checkout, answer buyer requests, and read the result
Send the agent a product URL and a task. It navigates the merchant's checkout, asks your application for missing inputs or payment authorization, and returns the purchase result.
Explore the complete experience in a working app
Use the full integration as a reference for your app
## Prerequisites
* **API key**: A production server-side key with `agent-checkouts.create`, `agent-checkouts.read`, `agent-checkouts.update`, and `agent-checkouts.cancel` scopes from the Crossmint Console
* **Buyer identity**: A stable identifier for your user, sent as `x-crossmint-user-id` on every request
* **Purchase task**: A product URL, the variant and quantity to buy, and a spending cap
* **Card authorization**: To complete a card purchase, a [saved](/agents/payment-methods/cards/save-card) and [registered](/agents/payment-methods/cards/register-card) card, plus a client-side key and buyer JWT as described in [Authorize Payments](/agents/checkouts/payments)
Use the same user ID as your JWT's configured user ID claim (`sub` by default), so the checkout and its card authorization belong to the same buyer.
Agent Checkouts does not support staging. These examples run on your backend; keep the server key out of browser code. For direct browser calls, see [Authenticate Each Request](/agents/checkouts/follow-run#authenticate-each-request).
Agent Checkouts and the live demo can place real orders. Use a task and spending cap that you intend to authorize.
## Run a Checkout
Set these variables in your terminal. Reuse them in the following steps:
```bash theme={null}
export CROSSMINT_API_KEY="YOUR_SERVER_API_KEY"
export CROSSMINT_USER_ID="YOUR_USER_ID"
export CHECKOUT_API="https://www.crossmint.com/api/unstable/agent-checkouts"
```
Replace the sample URL and task with your intended purchase, then create a run:
```bash cURL theme={null}
curl --fail-with-body --request POST "$CHECKOUT_API" \
--header "X-API-KEY: $CROSSMINT_API_KEY" \
--header "x-crossmint-user-id: $CROSSMINT_USER_ID" \
--header "Content-Type: application/json" \
--data '{
"request": {
"startUrl": "https://merchant.example/products/classic-tee",
"task": "Buy one black Classic Tee in size medium. Use standard delivery and pay by card."
},
"constraints": {
"maxCost": { "amount": "100.00", "currency": "USD" }
}
}'
```
The response contains a `runId`. Save it for subsequent calls:
```bash theme={null}
export RUN_ID="YOUR_RETURNED_RUN_ID"
```
To prefill shipping details or set the browser country, see [Configure a Checkout](/agents/checkouts/configure).
Read its current status and any request waiting for your application:
```bash cURL theme={null}
curl --fail-with-body "$CHECKOUT_API/$RUN_ID" \
--header "X-API-KEY: $CROSSMINT_API_KEY" \
--header "x-crossmint-user-id: $CROSSMINT_USER_ID"
```
While the run is active, repeat this read every 2 seconds. When `status` is `"awaiting_input"`, handle `requiredAction`. Your application chooses how to present the request: in a web page, a native app, or a conversation.
| `requiredAction.request.interaction.kind` | What your application does |
| - | - |
| `form` | Collect the requested fields. Use ordinary controls for `handling: "standard"` and `CrossmintProtectedInput` for `handling: "protected"`. |
| `payment` | For `method: "card"`, authorize the buyer's saved card with an order intent. For `method: "klarna"`, show the buyer the request's `authorizationUrl` and send no answer. |
For live updates and conversation history, see [Follow a Run](/agents/checkouts/follow-run). You can optionally display the agent's browser using `run.browser.embedUrl` once `run.browser` is available.
Copy `requiredAction.requestId` into your response. For example, if a form requests a standard field whose `key` is `email`, send the buyer's answer:
```bash cURL theme={null}
curl --fail-with-body --request POST "$CHECKOUT_API/$RUN_ID/messages" \
--header "X-API-KEY: $CROSSMINT_API_KEY" \
--header "x-crossmint-user-id: $CROSSMINT_USER_ID" \
--header "Content-Type: application/json" \
--data '{
"id": "answer_email_001",
"parts": [{
"type": "input_response",
"requestId": "YOUR_FORM_REQUEST_ID",
"action": "submit",
"response": { "kind": "form", "answers": { "email": "buyer@example.com" } }
}]
}'
```
Each submission answers the complete request: include all required fields, and omit optional fields left unanswered. [Handle Buyer Inputs](/agents/checkouts/buyer-inputs) shows the field types, ordinary rendering, and protected collection.
When the agent asks for a card payment, follow [Authorize Payments](/agents/checkouts/payments) to create an order intent for the request's amount and merchant using the same buyer's saved card. Complete any verification, then submit this body to the same messages endpoint:
```json theme={null}
{
"id": "answer_payment_001",
"parts": [{
"type": "input_response",
"requestId": "YOUR_PAYMENT_REQUEST_ID",
"action": "submit",
"response": { "kind": "payment", "orderIntentId": "YOUR_ORDER_INTENT_ID" }
}]
}
```
Use a unique message `id` for each answer. If you retry after a lost acknowledgement, resend the same ID and body. A payment answer contains the order intent ID; it never contains card details.
After each answer, resume reading the run and handle further requests until it finishes. A read can briefly show a request you already answered; handle each `requestId` only once, retrying its original message if needed.
At a terminal status, read the result:
| Status | What to read |
| - | - |
| `succeeded` | `result.summary` describes the purchase. If `result.purchase.kind` is `"receipt_captured"`, read `result.purchase.receipt`. |
| `blocked` | `result.code` and `result.summary` explain why the agent stopped. |
| `failed` | `reason` explains the failure. |
| `cancelled` | Cancellation has completed. |
An accepted answer resumes the agent. The final run status tells you whether the purchase succeeded.
To stop an active run, request cancellation and keep reading until it reaches a terminal status:
```bash cURL theme={null}
curl --fail-with-body --request POST "$CHECKOUT_API/$RUN_ID/cancel" \
--header "X-API-KEY: $CROSSMINT_API_KEY" \
--header "x-crossmint-user-id: $CROSSMINT_USER_ID"
```
See [Finish or Cancel](/agents/checkouts/follow-run#finish-or-cancel) for the complete result behavior.
## Launching in Production
Agent Checkouts already uses production credentials. Before serving buyers, add [streaming and recovery](/agents/checkouts/follow-run), [buyer input handling](/agents/checkouts/buyer-inputs), and [payment authorization](/agents/checkouts/payments) for your application's user experience. The sample app above shows how these pieces fit together.
If a create-run acknowledgement is lost, inspect [List Checkouts](/api-reference/agent-checkouts/list-agent-checkouts) before creating another run.
## Learn More
Prefill buyer details and set merchant guidance
Stream updates, steer the agent, and recover after disconnects
# Agent Cards Quickstart ⚡
Source: https://docs.crossmint.com/agents/cards-quickstart
Get your agent paying with agent cards in 15 minutes
Give agents permission to pay with a user's card using Crossmint's Agentic Payments API. This quickstart runs the [agent cards reference app](https://github.com/Crossmint/card-permissions-quickstart).
By the end you will have a running app where a user can sign in, save a card, register its supported rails, authorize an order intent, and mint secure card credentials.
See the full flow in action without setting anything up locally.
Explore the full reference implementation for agents, payment methods, and agent cards.
## Prerequisites
* Node.js 20+ and pnpm
* A [Stytch](https://stytch.com) account (used as the auth provider in this example)
**Eligible Cards**
Agent cards work with any credit or debit card. What varies by card is who issues the agent's credential: a card-network program, or Crossmint's encrypted card fallback.
Card-network rails — Visa Intelligent Commerce and Mastercard Agent Pay — support a subset of cards, and Visa and Mastercard keep expanding that coverage:
* **Mastercard Agent Pay:** Mastercard credit and debit cards
* **Visa Intelligent Commerce:** U.S.-issued Visa credit and debit cards, except business cards, prepaid cards, Chase cards, and Fidelity cards
Cards that no card-network rail supports yet, such as AMEX, Ramp, or non-US Visa cards, use the [encrypted card rail](/agents/payment-methods/cards/retrieve-agent-card). To enable it in production, contact us.
## Setup
You can ask your agent to walk you through the setup to run this quickstart.
```bash theme={null}
git clone https://github.com/Crossmint/card-permissions-quickstart.git
cd card-permissions-quickstart
pnpm install
```
Copy the example file and fill in the keys from the previous steps:
```bash theme={null}
cp .env.example .env.local
```
```bash .env.local theme={null}
NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN=your-stytch-public-token
NEXT_PUBLIC_CROSSMINT_CLIENT_API_KEY=your-crossmint-client-api-key
```
Sign in to the Crossmint Staging Console and create a project.
Staging keys come with **all scopes enabled by default**, so you don't need to configure anything else for the quickstart. Copy the key and paste it into your `.env.local`.
In the Stytch dashboard, in the **Test** environment of a **B2C** project:
1. **Configuration → SDK Configuration → OAuth** — enable **Google** (Stytch's shared test client works, no Google Cloud setup needed).
2. **Configuration → Redirect URLs** — add `http://localhost:3000` as both **Login** and **Signup**. Use exactly that, no trailing slash or `/callback`.
3. **Project Settings → API Keys** — copy the **Public token** (`public-token-test-...`) into `NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN` in your `.env.local`.
In the Crossmint Staging Console, under **3P Auth providers**:
1. Select **Stytch** from the provider dropdown.
2. Paste your Stytch **Project ID** (`project-test-...`, found in Stytch under **Project Settings → Project ID**).
3. Leave **Verifier Id** as `sub` (default).
Crossmint will start trusting JWTs minted by your Stytch project on subsequent SDK calls.
```bash theme={null}
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000).
## Understanding the user flow
Once running, the app walks the user through six steps:
1. **Authenticate.** Sign in with Google via Stytch. The session JWT is bridged into the Crossmint SDK so every API call is scoped to the user.
2. **Save a card.** The `CrossmintPaymentMethodManagement` component collects the card in a PCI-compliant iframe and returns a `paymentMethodId`.
3. **Register the card.** Registration discovers the card's supported rails, such as Visa Intelligent Commerce or Mastercard Agent Pay. It grants no spending permission and requires no verification.
4. **Create an order intent.** The app creates an allowance with an amount, description, expiration, and—when already known—the merchant. The response can contain multiple rails, each with its own verification status and credential formats. If no card-network rail is usable, the response also includes an `encrypted-card` rail that is active immediately (in production, only for projects with encrypted card access enabled).
5. **Verify the rail you want to use.** If the selected rail has `status: "pending_verification"`, the `OrderIntentVerification` component runs its provider ceremony. Other rails do not need to be verified and can remain pending. A returning Visa user authenticates with the passkey already bound to the device.
6. **Mint credentials.** The app uses the selected rail's `rail` and `provider` and requests a credential for a specific amount. If the merchant was not set on the order intent, the app supplies it now. The response contains the secure one-time card number, expiration, and CVC. For the `encrypted-card` rail, minting takes an RSA public key instead and returns the saved card as a JWE for the agent to decrypt locally.
Treat `rails` as a menu, not a checklist. Only the rail used to mint the credential must be `active`; unrelated rails do not block it.
## Next Steps
Style the card and passkey verification modals to match your brand.
Let Crossmint drive a real browser and complete checkout on any site.
# Agent Cards
Source: https://docs.crossmint.com/agents/cards/overview
Save a user's card once, then issue as many scoped, revocable agent cards from it as your agents need
Your user saves their real card once. From that card, you create as many **scoped authorizations** as your agents need, each with its own limit, purpose, and expiration. Crossmint calls these order intents. From an order intent, the agent mints a credential on whichever rail the purchase needs, and pays without ever seeing the real card number.
For the problem this solves and how it compares to other approaches, read [How Agents Pay](/agents/how-agents-pay).
## Key Features
One saved card backs any number of scoped agent cards
Every agent card has an amount, a purpose, an expiration, and optionally a merchant
Visa Intelligent Commerce and Mastercard Agent Pay hold the limit, not your code
Agents receive one-time or encrypted credentials from a PCI-compliant vault
Network tokens, encrypted card, and protocol credentials such as SPT from the same authorization
The user cancels an agent card or deletes the saved card at any time
## How It Works
Your app renders a Crossmint-hosted secure iframe. The card goes straight into Crossmint's PCI-compliant vault and never touches your servers.
Each agent card is an order intent: an allowance with an amount, description, expiration, and optionally a merchant. Create one per agent, per task, or per purchase. The user approves it with a passkey or a network-hosted step, and can cancel it at any time.
From an approved agent card, the agent mints the credential the purchase needs: a one-time card number bounded by the allowance, an encrypted card for cards outside the network programs, or a protocol credential. A network-issued credential is bounded to the allowance, expires, and is revoked with the agent card. The encrypted card is the saved card itself, so keep the purchase within the allowance and cancel the agent card when it is no longer needed.
## Rails
One agent card can mint credentials on several rails. Pick the rail the purchase needs; the rest stay unused.
| Rail | What the agent receives | Limit enforced by |
| - | - | - |
| Visa Intelligent Commerce | One-time card number or network token bounded by the allowance | Visa |
| Mastercard Agent Pay | One-time card number or network token bounded by the allowance | Mastercard |
| Encrypted card | The saved card encrypted to a key only the agent holds, when no network rail is available | Your allowance handling |
| SPT | A protocol-specific credential for merchants and services that accept it | The protocol |
Depending on the rail, the credential is delivered as a one-time card number, a network token, or a protocol-specific format such as MPP.
## Get Started
Run the reference app: sign in, save a card, authorize an agent card, and mint a credential
See the full flow without setting anything up locally
## Guides
Collect a user's card in a PCI-compliant iframe
Discover the rails a saved card supports
Create and authorize a scoped order intent
Mint a credential from an approved agent card
Style the approval modal to match your brand
Cancel an agent card or delete a saved card
## API Reference
Create, list, and delete saved cards
Create, mint credentials from, and revoke agent cards
## FAQs
No. The agent receives a one-time card-network credential minted for a specific amount, or, when no network rail is available, a copy of the saved card encrypted to a key the agent controls. The real card stays in Crossmint's PCI-compliant vault.
Yes. Every purchase runs on the user's own card, so the card's normal protections apply. Nothing is prefunded and no new account is opened.
The agent card includes an encrypted-card rail. Minting from it returns the saved card encrypted to a public key you supply, so the agent can still pay while the card details stay out of transit logs. In production this rail is available only for projects with encrypted card access enabled.
Yes. [Agent Checkouts](/agents/checkouts/overview) accepts any method the merchant supports, and a Crossmint Agent Card is the recommended one. See [Authorize Payments](/agents/checkouts/payments).
# Handle Buyer Inputs
Source: https://docs.crossmint.com/agents/checkouts/buyer-inputs
Render ordinary and protected fields and submit one complete form response
An Agent Checkout form can contain ordinary and protected fields. Render ordinary fields with your own controls. Render each field whose `handling` is `"protected"` with `CrossmintProtectedInput`; your application receives a reference instead of its value.
## Prerequisites
* **Checkout integration**: Complete the [Agent Checkouts quickstart](/agents/agent-checkouts-quickstart).
* **React SDK**: Install `@crossmint/client-sdk-react-ui` version 4.9.0 or later.
* **API key**: Use a production key with `agent-checkouts.read` and `agent-checkouts.update`. Add `protected-inputs.create` to the client-side key when collecting protected fields.
* **User authentication**: Pass the buyer's JWT from your [external authentication integration](/wallets/guides/bring-your-own-auth). Use the same project, environment, and user as the checkout run.
## Collect and Submit Answers
Use an open `input_request` part from the Messages API, or `requiredAction.request` when polling the run. Both expose the same `interaction.fields` descriptors. A protected field belongs to a `"form"` interaction.
```json theme={null}
{
"type": "input_request",
"requestId": "req_456",
"status": "open",
"question": "Enter your email and password to sign in.",
"expiresAt": "2026-10-02T15:30:00.000Z",
"interaction": {
"kind": "form",
"fields": [
{
"key": "email",
"label": "Email",
"required": true,
"handling": "standard",
"input": { "kind": "text", "autoComplete": "email" }
},
{
"key": "password",
"label": "Password",
"required": true,
"handling": "protected",
"input": {
"kind": "text",
"display": "masked",
"autoComplete": "current-password"
}
}
]
}
}
```
Pass each protected descriptor unchanged as the component's `field`. It supplies the key, accessible label, requiredness, and supported input hints. `display: "masked"` controls appearance; `handling: "protected"` controls how the value is collected. A standard text field can also be masked.
Keep the request's field order and use each `key` as its answer key. Display its `label`, respect `required`, and pass supported presentation hints to your controls. For a standard single-line text descriptor:
```tsx StandardTextField.jsx theme={null}
export function StandardTextField({ field, value, onChange }) {
return (
);
}
```
Your parent component owns the value and `onChange` callback. For other input kinds, choose a control and submit the corresponding typed value:
| Input | Ordinary Control | Answer |
| - | - | - |
| Single-line text | Text input; use a password input for `display: "masked"` | String, preserving leading zeros |
| Multiline text | Textarea | String |
| Number / integer | Numeric input; parse before submission | Finite number / safe integer |
| Boolean | Checkbox or toggle | `true` or `false`; required means an answer must be present, not necessarily `true` |
| Choice, `selection.kind: "one"` | Select or radio group | One option's `value` |
| Choice, `selection.kind: "many"` | Multiple select or checkbox group | Array of option values, respecting `selection.min` and optional `max` |
Use choice labels for display and values for answers. Placeholder options are not answers. Disabled options cannot be newly selected; preserve already-selected disabled options. Browser form serialization omits disabled controls, so include those locked selections explicitly when constructing answers.
For example, an ordinary response can contain `email: "buyer@example.com"`, `quantity: 2`, `giftWrap: false`, and `colors: ["navy", "black"]`. Submit only keys requested by this form. Omit unanswered optional keys; `null` is not an answer.
This example handles one protected field. Your page owns the visible label, button, and error text. The iframe contains only the input. Call `ref.collect()` when the buyer chooses to continue; an HTML `
Combine the collected reference with ordinary answers under the keys from the request. For the email and password example, send this body to `POST /api/unstable/agent-checkouts/{runId}/messages`:
Send all required answers in one `response.answers` object, using the field keys from the request. Ordinary answers are strings, booleans, numbers, or choice values; protected answers are `{ protectedInputId }`. Answers do not repeat `handling`. Omit unanswered optional field keys.
Follow [response and retry handling](/agents/checkouts/follow-run#respond-or-steer): retain one message ID and body until acknowledged, and use a new ID after correcting a definitive validation rejection. If the request closes or expires, load the current request.
An accepted answer resumes the run; it does not confirm that the merchant accepted the value. Follow subsequent messages for application results or another request.
## Choose the Input Type and Presentation
| Input | Protected support | Representation |
| - | - | - |
| Single-line text | Yes | Preserves the string, including leading zeros. Use for passwords, verification codes, and identifiers. |
| Number | Yes | A finite numeric value; permits fractions. |
| Integer | Yes | A safe whole-number value. |
| Multiline text, boolean, choice | No | Render ordinary controls for fields with `handling: "standard"`. |
Use `appearance` to supply fonts and supported theme variables for the input. Style labels, buttons, and error messages in your own page. `disabled` and `invalid` update the mounted input dynamically. Numeric fields use numeric keyboard hints; a text code can specify `inputMode: "numeric"` without losing leading zeros.
Protected references last 24 hours by default and at most 7 days. The component's optional `expiresAt` sets the reference lifetime; the request's `expiresAt` is the separate deadline to answer the request.
## Use a Chat or Native Handoff
A messaging agent can collect ordinary answers, then send the buyer to an authenticated page that renders only the protected fields. Your application associates that page with the run and request, merges the references with the ordinary answers, and submits the complete response. A native app can use the same page in a webview.
Collection keeps plaintext out of your application JavaScript and answer messages. After application to a merchant page, page content, screenshots, or recordings can expose values. Do not rely on the collection component to mask browser evidence.
## Next Steps
Reuse the buyer's merchant session on later checkouts
Review form, payment, and steering message formats
# Configure a Checkout
Source: https://docs.crossmint.com/agents/checkouts/configure
Set the purchase task, buyer details, browser country, and merchant guidance
Configure a run before it starts so the agent knows what to buy and can reuse information you already have. Missing details can become [buyer input requests](/agents/checkouts/buyer-inputs) during checkout.
## Prerequisites
* A [working checkout integration](/agents/agent-checkouts-quickstart)
* A production API key with `agent-checkouts.create`; add `agent-checkouts.buyer-profiles.create` when creating a buyer profile
* A stable buyer identity shared by the run and any profiles or payment authorization
The examples use a server-side key. For client-side authentication, see [Follow a Run](/agents/checkouts/follow-run#authenticate-each-request).
## Configure the Run
A run needs `request.startUrl` and `constraints.maxCost`. Add `request.task` when the URL does not fully describe the purchase.
```typescript theme={null}
const BASE_URL = "https://www.crossmint.com/api/unstable/agent-checkouts";
const headers = {
"Content-Type": "application/json",
"X-API-KEY": process.env.CROSSMINT_API_KEY!,
"x-crossmint-user-id": "YOUR_USER_ID",
};
const checkout = {
request: {
startUrl: "https://merchant.example/products/classic-tee",
task: "Buy one black Classic Tee in size medium. Use standard delivery and pay by card.",
},
constraints: {
maxCost: { amount: "100.00", currency: "USD" },
},
};
```
Use a product URL when it identifies the item. A merchant home page, category, search, or cart URL also works when the task describes what to find. Include quantity, variants, delivery preferences, and billing preferences in the task.
`maxCost` limits what the agent may spend. A checkout that would exceed it ends as `blocked` with `result.code: "policy.max_cost_exceeded"`.
Name the payment method in the task, then [authorize payment](/agents/checkouts/payments) when requested. Keep raw card details and protected values out of the task.
A buyer profile stores name, contact, and shipping details. Create it once and attach its ID to later runs for the same buyer. It does not contain payment credentials.
```typescript theme={null}
// Uses BASE_URL and headers from the previous step.
const profileResponse = await fetch(`${BASE_URL}/buyer-profiles`, {
method: "POST",
headers,
body: JSON.stringify({
label: "Home",
name: { first: "Ada", last: "Lovelace" },
contact: { email: "ada@example.com" },
shipping: {
addressLines: ["1 Market St"],
locality: "San Francisco",
administrativeAreaCode: "US-CA",
postalCode: "94105",
countryCode: "US",
},
}),
});
if (!profileResponse.ok) {
throw new Error(`Could not create buyer profile: ${profileResponse.status}`);
}
const { id: buyerProfileId } = await profileResponse.json();
```
Use the profile for reusable identity and address data, and the task for this purchase's preferences. The agent can still request details the profile does not supply, such as a gift message or a merchant-specific question.
Buyer-profile create, list, read, update, and delete operations have separate `agent-checkouts.buyer-profiles.*` scopes. See [Create Buyer Profile](/api-reference/agent-checkouts/create-buyer-profile) for the complete schema.
Add `browser.location` to request where the managed browser's network traffic appears to originate:
```json theme={null}
{
"browser": {
"location": { "type": "country", "countryCode": "CA" }
}
}
```
This is an optional fragment of the create body. `countryCode` uses ISO 3166-1 alpha-2 codes, such as `US`, `GB`, and `CA`; Crossmint trims and uppercases it. Managed browsers use US egress when you omit the location.
Location affects network egress. It does not set or verify residence, shipping address, language, or purchase eligibility.
Routing is best effort, and there is no fixed public list of supported countries. If the requested location is unavailable, the run ends as `failed` with `reason: "browser_location_unsupported"` instead of knowingly using another country. Retry later, choose another country, or start a new run without `browser.location` to use the US default.
`merchantGuidance` supplies short operational notes about a merchant's checkout. Use it for repeatable problems you have observed, rather than ordinary checkout instructions.
| Observed problem | Useful guidance |
| - | - |
| A fit-finder modal derails size selection | Close the fit finder and use the product page's size dropdown. |
| The agent misses the guest path | Guest checkout is the Continue without an account link below the sign-in form. |
| The checkout button stays disabled briefly | Wait for the cart drawer to finish loading before pressing Checkout. |
Guidance applies to the merchant of `startUrl`, for this run only. It cannot expand the task, authorize consent, or raise the spending cap. The agent uses the actual page when it contradicts the guidance.
Supplying guidance replaces any guidance Crossmint maintains for that merchant. Omit it for standard checkouts; an empty string is rejected. The limit is 20,000 characters, but keep notes short and concrete.
Combine the options you need in one create request:
```typescript theme={null}
// Uses checkout, buyerProfileId, BASE_URL, and headers from the earlier steps.
const response = await fetch(BASE_URL, {
method: "POST",
headers,
body: JSON.stringify({
...checkout,
buyerProfileId,
browser: { location: { type: "country", countryCode: "CA" } },
merchantGuidance: "Close the fit finder and use the product page's size dropdown.",
}),
});
if (!response.ok) {
throw new Error(`Could not start checkout: ${response.status}`);
}
const { runId } = await response.json();
```
To reuse a merchant session, also set `browser.profileId`. It is independent of location; see [Reuse Merchant Sessions](/agents/checkouts/merchant-sessions).
Compare the run's result and browser behavior with earlier attempts when evaluating guidance. Update or remove notes when the merchant changes its layout.
## Next Steps
Observe progress and handle requests after starting the checkout
Review the complete create request schema
# Follow a Run
Source: https://docs.crossmint.com/agents/checkouts/follow-run
Observe checkout updates, recover after disconnects, and respond or steer through messages
Follow a checkout from its returned `runId` until it reaches a terminal status. The run exposes current status and `requiredAction`; its messages expose conversation, progress, activity, input requests, and results.
## Prerequisites
* A checkout started through the [quickstart](/agents/agent-checkouts-quickstart)
* A production API key with `agent-checkouts.read` and `agent-checkouts.update`; add `agent-checkouts.cancel` to cancel runs
* The same buyer identity used to create the run
## Authenticate Each Request
For a backend integration, keep the server key outside client bundles and send the buyer's identifier on every call:
```typescript theme={null}
const BASE_URL = "https://www.crossmint.com/api/unstable/agent-checkouts";
const runId = "YOUR_RUN_ID";
const headers = {
"Content-Type": "application/json",
"X-API-KEY": process.env.CROSSMINT_API_KEY!,
"x-crossmint-user-id": "YOUR_USER_ID",
};
```
For browser or native calls, use a production client-side key and `Authorization: Bearer YOUR_USER_JWT` instead of `x-crossmint-user-id`. Register your JWT issuer under **3P Auth providers** in the Crossmint Console and allow your app's origins. The request bodies are the same.
## Observe and Recover
| Strategy | Use it when |
| - | - |
| Backend SSE stream | Your server can keep a connection open. Relay updates to your website, native app, messaging agent, or voice agent through its existing channel. |
| Direct client stream | Your browser or native client calls Crossmint with a client key and JWT. Use a fetch-based SSE client; native `EventSource` cannot attach the authentication headers. |
| Polling | Your runtime cannot keep a stream open. Read the run for status and requests, and list messages for conversation details. |
Crossmint recommends streaming for live production integrations. The [quickstart](/agents/agent-checkouts-quickstart) uses polling to keep its example small.
The stream is an HTTP response with content type `text/event-stream`. It carries both run snapshots and message changes:
```bash theme={null}
curl --no-buffer \
--url "https://www.crossmint.com/api/unstable/agent-checkouts/YOUR_RUN_ID/messages/stream" \
--header "Accept: text/event-stream" \
--header "X-API-KEY: YOUR_SERVER_API_KEY" \
--header "x-crossmint-user-id: YOUR_USER_ID"
```
```text theme={null}
id: cursor_01
event: run.updated
data: {"runId":"...","revision":3,"status":"awaiting_input","requiredAction":{...}}
id: cursor_02
event: message.upsert
data: {"id":"msg_...","revision":2,"role":"assistant","parts":[...]}
```
Store each event's `id` as the resume cursor. Keep the greatest run `revision`, and the greatest message `revision` for each message ID. Replayed or stale snapshots must not replace newer state. Revisions can skip; they order snapshots, while the cursor identifies the stream replay position.
Handle open requests once by `requestId`, whether they arrive in `requiredAction` or a message part. Dispatch `interaction.kind: "form"` to [Handle Buyer Inputs](/agents/checkouts/buyer-inputs), and `"payment"` to [Authorize Payments](/agents/checkouts/payments).
Reconnect with the cursor in the `last-event-id` header or `after` query parameter. If the cursor is rejected or recovery needs a fresh snapshot, use the next step.
List all message pages before opening a stream for an existing run. The final page's `streamCursor` provides the handoff to live updates. Read the current run after history so you also discover a pending action.
```typescript theme={null}
// Uses BASE_URL, runId, and headers from the authentication section.
const messages = new Map();
let cursor: string | null = null;
let streamCursor: string | null = null;
do {
const query = new URLSearchParams({ limit: "100" });
if (cursor !== null) {
query.set("cursor", cursor);
}
const response = await fetch(`${BASE_URL}/${runId}/messages?${query}`, { headers });
if (!response.ok) {
throw new Error(`Could not list messages: ${response.status}`);
}
const page = await response.json();
for (const message of page.data) {
const existing = messages.get(message.id);
if (!existing || message.revision > existing.revision) {
messages.set(message.id, message);
}
}
cursor = page.nextCursor;
streamCursor = page.streamCursor;
} while (cursor !== null);
const response = await fetch(`${BASE_URL}/${runId}`, { headers, cache: "no-store" });
if (!response.ok) {
throw new Error(`Could not read checkout: ${response.status}`);
}
const run = await response.json();
const streamUrl = new URL(`${BASE_URL}/${runId}/messages/stream`);
if (streamCursor !== null) {
streamUrl.searchParams.set("after", streamCursor);
}
```
Render the recovered messages and current run. If `run.status` is `"awaiting_input"` and its request has not been answered, handle `run.requiredAction` before connecting to `streamUrl` with the same authentication. Retain your request deduplication state across reconnects. See [List Messages](/api-reference/agent-checkouts/list-agent-checkout-messages) and [Stream Messages](/api-reference/agent-checkouts/stream-agent-checkout-messages) for pagination and replay details.
Read `GET /{runId}` with the same headers and `cache: "no-store"`. Poll every 2 seconds while active, increase the interval while nothing changes, and reset it after a status change or response. Stop at a terminal status.
Retry transient read failures with bounded backoff. For `429`, respect the response's `retryAfterSeconds`. This read helper illustrates the retry behavior:
```typescript theme={null}
// Uses BASE_URL, runId, and headers from the authentication section.
async function readRun() {
const retryable = new Set([408, 429, 500, 502, 503, 504]);
for (let attempt = 0; attempt < 5; attempt++) {
let response: Response | undefined;
try {
response = await fetch(`${BASE_URL}/${runId}`, { headers, cache: "no-store" });
} catch (error) {
if (attempt === 4) {
throw error;
}
}
if (response?.ok) {
return response.json();
}
if (response && (!retryable.has(response.status) || attempt === 4)) {
throw new Error(`Could not read checkout: ${response.status}`);
}
const body = response?.status === 429 ? await response.json().catch(() => null) : null;
const seconds = body?.retryAfterSeconds;
const serverDelay = typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0
? seconds * 1_000 : 0;
await new Promise((resolve) => setTimeout(resolve, Math.max(1_000 * 2 ** attempt, serverDelay)));
}
throw new Error("Could not read checkout after retries");
}
```
A poll immediately after a successful response can still contain the same action. Track answered request IDs so you do not submit again. For resumed integrations, recover message history rather than relying only on an in-memory set.
## Process Message Parts
A message can contain several parts. Store it by `id`, retain its greatest `revision`, and use `role` to identify the speaker.
| Part | What Your App Does |
| - | - |
| `text` | Show the instruction or reply. A user message's `delivery` distinguishes accepted from consumed. |
| `progress` | Show the agent's current progress. |
| `activity` | Optionally show operations and their running, completed, incomplete, or uncertain status. |
| `input_request` | Handle an `open` request according to `interaction.kind`. |
| `input_response` | Show the projected application outcome, alternative, or decline. Acceptance is separate from application at the merchant. |
| `result` | Show `outcome` and `summary`, with the receipt or blocked code when present. |
Your UI can also display the read-only checkout browser in an iframe using `run.browser.embedUrl`. `browser` is `null` until a session exists; displaying it is optional.
## Respond or Steer
Submit the [form answer](/agents/checkouts/buyer-inputs) or [payment authorization](/agents/checkouts/payments) to `POST /{runId}/messages`. Generate the message `id` once and retain its exact body until acknowledged. If acknowledgement is lost, retry the same ID and body. After a definitive validation rejection, correct the response with a new ID. Do not reuse an ID for changed content.
Respond before the request's `expiresAt`. If it closes or expires, load the current request. After a response, continue observing: the agent may ask several questions during a purchase.
To choose another path, send an alternative instead of the requested answer:
```json theme={null}
{
"id": "msg_alternative_123",
"parts": [{
"type": "input_response",
"requestId": "req_123",
"action": "alternative",
"text": "Check out as a guest instead"
}]
}
```
Use `action: "decline"` without `text` to refuse a request. To steer without answering a particular request, send a text part:
```json theme={null}
{
"id": "msg_steering_123",
"parts": [{ "type": "text", "text": "Use standard shipping. Choose navy if black is sold out." }]
}
```
A `202 Accepted` confirms message acceptance, not that the agent consumed it. Follow its `delivery` and subsequent run updates.
## Finish or Cancel
| Run Status | Result |
| - | - |
| `succeeded` | The merchant confirmed the purchase. `result.purchase` contains a captured receipt or reports confirmation without a receipt. |
| `blocked` | The agent stopped because of a policy, product, fulfillment, or merchant condition. Use `result.code` for logic and `result.summary` for display. |
| `failed` | The run could not continue. Use `reason` for recovery logic. |
| `cancelled` | Cancellation completed. |
For a succeeded run with `result.purchase.kind: "receipt_captured"`, read `result.purchase.receipt` for the total and merchant order ID. The result message supplies the user-facing summary.
Cancel an active run with `POST /{runId}/cancel`, using the same authentication. It returns `202 Accepted`; keep observing until the run reaches a terminal status. The stream closes after terminal updates.
## Connect Your User Channel
| Channel | Integration |
| - | - |
| Web | Render inputs and authorization in your application. |
| Native | Render ordinary controls natively; use an authenticated webview for protected collection or card verification. |
| Messaging or voice | Map messages into the conversation, collect ordinary answers there, and send an authenticated application link when a protected field or verification requires a web surface. |
| Server agent | Consume updates on your backend and involve a buyer surface when input or verification requires it. |
Your application associates each handoff with the buyer, run, and request. See [Handle Buyer Inputs](/agents/checkouts/buyer-inputs#use-a-chat-or-native-handoff) for combining answers collected across channels.
## Next Steps
Render and submit ordinary and protected answers
Answer card requests with order intents and send Klarna links to the buyer
# Reuse Merchant Sessions
Source: https://docs.crossmint.com/agents/checkouts/merchant-sessions
Save a merchant session after sign-in and reuse it on later checkouts
Some purchases require a merchant account for saved addresses, member prices, order history, or a cart the user already filled. A browser profile lets Agent Checkouts save the merchant session after the user signs in and load it on later runs.
Complete sign-in through [buyer input requests](/agents/checkouts/buyer-inputs). Password collection is separate from the saved session.
## Prerequisites
* **Agent Checkout integration**: Complete the [Agent Checkouts quickstart](/agents/agent-checkouts-quickstart) so your application can start a run, observe it, and send messages.
* **Server-side API key**: Use a production key with `agent-checkouts.create`, `agent-checkouts.read`, `agent-checkouts.update`, and `agent-checkouts.browser-profiles.create` scopes.
Browser profiles are scoped to a user. With a server-side key, identify that user with `x-crossmint-user-id` on every request.
Always send `x-crossmint-user-id` with a server-side key. Without it, requests use your project's shared service subject, so different users could share the same browser profile.
```typescript theme={null}
const BASE_URL = "https://www.crossmint.com/api/unstable/agent-checkouts";
const headers = {
"Content-Type": "application/json",
"X-API-KEY": process.env.CROSSMINT_API_KEY!,
"x-crossmint-user-id": "YOUR_USER_ID",
};
```
## Sign In Once and Reuse the Session
Create one profile and store its `id` with the user in your system. The optional `label` is only for your own bookkeeping.
```bash cURL theme={null}
curl --request POST \
--url https://www.crossmint.com/api/unstable/agent-checkouts/browser-profiles \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: YOUR_SERVER_API_KEY' \
--header 'x-crossmint-user-id: YOUR_USER_ID' \
--data '{ "label": "Primary shopping profile" }'
```
```typescript Node.js theme={null}
// Uses BASE_URL and headers from the prerequisites.
const res = await fetch(`${BASE_URL}/browser-profiles`, {
method: "POST",
headers,
body: JSON.stringify({ label: "Primary shopping profile" }),
});
if (!res.ok) {
throw new Error(`Could not create browser profile: ${res.status}`);
}
const { id: profileId } = await res.json();
```
The response contains metadata only. It does not return cookies, tokens, or other browser state.
```json theme={null}
{
"id": "7c3b1f2a-9d54-4e80-b1a6-2f0c8e5d4a31",
"label": "Primary shopping profile",
"createdAt": "2026-08-10T11:00:00.000Z",
"updatedAt": "2026-08-10T11:00:00.000Z"
}
```
A user can have one browser profile. Reuse that profile rather than creating one for each merchant.
Pass the profile as `browser.profileId` when you start a run. The first run starts without a saved merchant session, so tell the agent to sign in before it buys.
```typescript theme={null}
// Uses BASE_URL, headers, and profileId from the earlier steps.
const res = await fetch(BASE_URL, {
method: "POST",
headers,
body: JSON.stringify({
request: {
startUrl: "https://shop.example.com/products/classic-tee",
task: "Sign in, then buy the medium in black.",
},
browser: { profileId },
constraints: {
maxCost: { amount: "100.00", currency: "USD" },
},
}),
});
if (!res.ok) {
throw new Error(`Could not start checkout: ${res.status}`);
}
const { runId } = await res.json();
```
You can also include `browser.location` in the same object when the checkout should run from a particular country. See [Configure a Checkout](/agents/checkouts/configure#configure-the-run).
[Follow the run](/agents/checkouts/follow-run) and answer sign-in requests through [Handle Buyer Inputs](/agents/checkouts/buyer-inputs). An accepted form response does not itself confirm successful sign-in.
Answer each request by its `requestId`, then continue observing the run. After the checkout finishes cleanly, the merchant's session is stored in the browser profile.
The profile itself does not report what happened during a checkout. Read the run and its messages to confirm whether the agent signed in, encountered another verification step, or completed the purchase.
Pass the same `browser.profileId` on the next checkout for this user.
```typescript theme={null}
// Uses BASE_URL, headers, and profileId from the earlier steps.
const nextResponse = await fetch(BASE_URL, {
method: "POST",
headers,
body: JSON.stringify({
request: {
startUrl: "https://shop.example.com/products/wool-scarf",
task: "Buy one in grey using my saved account.",
},
browser: { profileId },
constraints: {
maxCost: { amount: "100.00", currency: "USD" },
},
}),
});
if (!nextResponse.ok) {
throw new Error(`Could not start checkout: ${nextResponse.status}`);
}
```
The run loads the saved session before it navigates. If the merchant has expired the session or requires the user to sign in again, the agent sends new input requests. Handle them the same way as the first run.
## How Browser Profiles Stay Protected
* **The profile API returns metadata only.** Cookies, tokens, and other saved browser state are not returned.
* **Saved browser state is not added to a model prompt.** It is loaded only into the browser for that user's checkout.
* **Each run has its own browser.** The browser is released when the run finishes.
* **Payment authorization is separate.** Follow [Authorize Payments](/agents/checkouts/payments) when the run asks for a card credential.
## Manage a Browser Profile
Browser profile routes live under `https://www.crossmint.com/api/unstable/agent-checkouts/browser-profiles` and are documented with the Agent Checkouts API reference.
* A user can have one profile. Creating another returns `409`.
* Requesting a profile owned by another user returns `404`, so its existence cannot be probed.
* `label` is the only editable field.
* Deleting a profile irreversibly erases its saved browser state. Runs already using it continue, and erasure finishes after they end.
## Next Steps
Pay with an Agent Card, a merchant-saved card, or a local method
Set purchase context before starting the run
# Agent Checkouts
Source: https://docs.crossmint.com/agents/checkouts/overview
One API to start from any URL and buy with any payment method the merchant accepts, under a spending cap you set
Agent Checkouts lets your agent buy from any merchant with a single API. Start a run from any URL, optionally describe what to buy in natural language, and set a spending cap. Crossmint completes the purchase on your user's behalf and returns the receipt.
Behind that API, Crossmint picks the right rail for each merchant: an agentic commerce protocol where the merchant supports one, and an optimized browser session everywhere else. You integrate once and get every merchant. For the problem this solves and how it compares to building it yourself, read [How Agents Buy](/agents/how-agents-buy).
Agent Checkouts runs in production only. There is no staging environment, so use a production API key and live auth credentials.
## Key Features
Any starting URL, with or without a purchase API or protocol support
Crossmint Agent Cards, other agent cards, cards on file, Apple Pay, Google Pay, PayPal, Shop Pay, and more
Agentic commerce protocols where supported, an optimized browser everywhere else
The checkout never spends above the cap you set
Embed the session so your user watches the purchase happen
Prefill identity, shipping, and intent; the agent asks the user only for what is missing
## How It Works
Send any starting URL and the maximum the checkout may spend. Add an optional task when the URL alone does not identify the product, variant, quantity, or other purchase preferences.
Follow the run for its next required action, and read or stream its messages for the complete conversation. Your app can answer forms, authorize a secure card credential, answer protected fields, or send a text message to steer the agent at any time.
The run ends with a receipt, a safe blocked outcome, or a clear failure reason. Cancel at any point before completion.
## Get Started
Start a checkout and handle its requests
Explore the full checkout experience in the sample app
See how the API and UI components fit together
## Guides
Set the task, buyer details, browser country, and merchant guidance
Observe updates, recover after disconnects, and steer the agent
Render ordinary and protected fields and submit complete answers
Answer card requests with order intents and send Klarna links to the buyer
Save a signed-in session and reuse it on later checkouts
## API Reference
Create, read, respond to, and cancel checkouts
Reusable name, contact, and shipping details
Saved merchant logins for later checkouts
## FAQs
Any method the merchant accepts: a Crossmint Agent Card, an agent card from another provider such as Ramp or Link, a card already saved in the user's account at the merchant, Apple Pay, Google Pay, PayPal, or a local method such as Shop Pay, bank transfer, or Bizum. See [Authorize Payments](/agents/checkouts/payments).
Follow the run to read its current status and `requiredAction`. Read or stream `/messages` when your UI needs the complete conversation, progress, activity, input requests, or result. See [Follow a Run](/agents/checkouts/follow-run) for both views and recovery.
No. When the run asks for a card payment, create an order intent for the same buyer and amount, then send only its `orderIntentId` in an input response. Raw card details never belong in the run request or message history. See [Authorize Payments](/agents/checkouts/payments).
It moves to `awaiting_input` whenever it needs information or authorization. Form requests list typed fields. Ordinary answers contain values; [protected field answers](/agents/checkouts/buyer-inputs) contain collected references. Card payment requests take an order intent ID. Klarna payment requests link to a page where the buyer pays. You can pre-answer many ordinary questions with a [buyer profile and task](/agents/checkouts/configure).
Yes. Send a text part to `/messages` at any time before the run finishes. Use it to change preferences, answer a conversational question, or redirect the agent without starting over.
Yes. A [browser profile](/agents/checkouts/merchant-sessions) keeps the browser state from a login so later checkouts start signed in, with access to saved addresses, payment methods, and member pricing.
Yes. Every endpoint accepts a server-side key, or a client-side key plus the signed-in user's JWT from your auth provider. See [Follow a Run](/agents/checkouts/follow-run#authenticate-each-request) for both authentication methods.
# Authorize Payments
Source: https://docs.crossmint.com/agents/checkouts/payments
Answer card payment requests with order intents and send Klarna payment links to the buyer
Agent Checkouts can use any payment method the merchant accepts. Tell the agent which method to use in the run's `task`. If the agent needs a card credential, it pauses and sends a typed payment request for your application to authorize. If the merchant takes Klarna, the payment request links to a page where the buyer pays.
Your agent runtime can run on a server and communicate through any user channel, including a website, native app, messaging app, or voice assistant. The user only needs a secure application surface when they must authorize or verify the card.
## Prerequisites
* **Agent Checkout integration**: Complete the [Agent Checkouts quickstart](/agents/agent-checkouts-quickstart) so your application can start a run, observe it, and send messages.
* **Agent Card setup**: To use an Agent Card, [save the user's card](/agents/payment-methods/cards/save-card), [register it](/agents/payment-methods/cards/register-card), and be ready to [create an order intent](/agents/payment-methods/cards/create-agent-card).
* **Consistent user identity**: Use the same Crossmint project, environment, and user identity for the checkout run and its order intent.
## Choose the Method in the Task
Describe the user's preferred method when you start the run. Be specific enough that the agent does not have to infer which option to select.
| Method | Example task | What else to provide |
| - | - | - |
| Crossmint Agent Card | `Buy one in black and pay by card.` | Authorize the typed payment request with an order intent. |
| Klarna | `Buy one in black and pay with Klarna.` | Show the buyer the payment request's `authorizationUrl`. See [Handle a Klarna Payment Request](#handle-a-klarna-payment-request). |
| Card saved at the merchant | `Buy one in black using my saved Visa ending in 4242.` | Start the run with the user's [`browser.profileId`](/agents/checkouts/merchant-sessions). |
| Local or alternative method | `Buy one in black and pay with Shop Pay.` | Answer any instructions the merchant presents. If the method is unavailable, steer the run to another method. |
The task selects the method. It does not authorize a charge. Only answer a typed payment request when the agent is ready to pay.
## Authorize an Agent Card
The run moves to `awaiting_input` and exposes the request in both places you can observe a run:
* `requiredAction.request` when you read the run
* an `input_request` message part when you list or stream messages
Dispatch on `interaction.kind === "payment"`, then on `interaction.method`. Answer with an order intent only when `method` is `"card"`. For `"klarna"`, see [Handle a Klarna Payment Request](#handle-a-klarna-payment-request). A card request carries charge metadata only. It never contains card details, an iframe URL, or an order intent ID. A Klarna request also carries `authorizationUrl`, a link that opens the buyer's payment page, so keep it as private as a credential.
An open payment message part looks like this:
```json theme={null}
{
"type": "input_request",
"status": "open",
"requestId": "req_123",
"question": "Authorize a card payment of 42.50 USD at shop.example.com.",
"expiresAt": "2026-10-02T15:30:00.000Z",
"interaction": {
"kind": "payment",
"purpose": "checkout_payment",
"method": "card",
"amount": { "kind": "exact", "value": "42.50", "currency": "USD" },
"merchant": {
"url": "https://shop.example.com",
"name": "Example Shop",
"countryCode": "US"
}
}
}
```
* `method: "card"` requests a card credential.
* Other `method` values, such as `"klarna"`, do not request a card credential. Do not answer them with an order intent.
* `amount.value` is a decimal string and `amount.currency` is an ISO 4217 code. `kind: "exact"` is the requested payable total; `kind: "maximum"` is the run's cost ceiling.
* `merchant` identifies where the credential will be used. Copy its `url`, `name`, and `countryCode` unchanged when you create the order intent.
Create the order intent in your authenticated application with a production client-side key that has `order-intents.create` and `order-intents.read` scopes. Send the JWT for the same user who owns the saved card and the checkout run.
Use the exact amount from the payment request, set a short expiration, and copy `interaction.merchant` into the order intent. This makes the merchant available to payment rails that need it before credential minting. If you omit it, Agent Checkouts supplies the same merchant when it requests the credential.
```typescript theme={null}
// Uses an open payment request from the checkout run.
const runId = "YOUR_RUN_ID";
const apiKey = "YOUR_CLIENT_API_KEY";
const jwt = "YOUR_USER_JWT";
const checkoutHeaders = { "X-API-KEY": apiKey, Authorization: `Bearer ${jwt}` };
const runResponse = await fetch(
`https://www.crossmint.com/api/unstable/agent-checkouts/${runId}`,
{ headers: checkoutHeaders }
);
if (!runResponse.ok) {
throw new Error(`Could not read checkout: ${runResponse.status}`);
}
const run = await runResponse.json();
const interaction = run.status === "awaiting_input" ? run.requiredAction.request.interaction : undefined;
if (interaction?.kind !== "payment" || interaction.method !== "card") {
throw new Error("Checkout does not have an open card payment request");
}
const action = run.requiredAction;
const paymentRequest = action.request;
const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1_000).toISOString();
const response = await fetch("https://www.crossmint.com/api/unstable/order-intents", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": apiKey,
Authorization: `Bearer ${jwt}`,
},
body: JSON.stringify({
paymentMethodId: "YOUR_PAYMENT_METHOD_ID",
amount: {
value: paymentRequest.interaction.amount.value,
currency: paymentRequest.interaction.amount.currency,
},
description: `Agent Checkout at ${paymentRequest.interaction.merchant.name}`,
merchant: paymentRequest.interaction.merchant,
expiresAt,
}),
});
if (!response.ok) {
throw new Error(`Could not create order intent: ${response.status}`);
}
let orderIntent = await response.json();
```
`YOUR_PAYMENT_METHOD_ID` is the ID returned when you [save](/agents/payment-methods/cards/save-card) and [register](/agents/payment-methods/cards/register-card) the user's card. For the complete request and response, see [Create an Agent Card](/agents/payment-methods/cards/create-agent-card#create-the-order-intent).
Read `orderIntent.rails` and look for an `active` rail whose `credentialFormats` includes `"card"`. The top-level order-intent status does not tell you whether a card credential is ready.
If no card rail is active, follow [Verify the Selected Rail](/agents/payment-methods/cards/create-agent-card#verify-the-selected-rail) for `pending_verification`, or [Recollect the CVC](/agents/payment-methods/cards/recollect-cvc) for `pending_cvc_recollection`. Fetch the order intent again after that step and confirm that a card-capable rail is active. If none can become active, ask the buyer for another card.
**Encrypted card in production.** Agent Checkouts can pay with the `encrypted-card` rail even when encrypted card access is not enabled for your project. The card never leaves the Crossmint vault: the checkout uses it to pay the merchant, and your application only handles the `orderIntentId`. Without that access, your own reads of the order intent do not list the rail, so this check finds no active card rail when the saved card has no usable card-network rail. Submit the `orderIntentId` anyway. If the checkout answers `rail_unavailable`, [recollect the CVC](/agents/payment-methods/cards/recollect-cvc) and answer the new payment request.
The [Agent Card guide](/agents/payment-methods/cards/create-agent-card#verify-the-selected-rail) shows the verification component. Card setup and verification are separate from submitting the checkout response.
Send one `input_response` part that references the request's `requestId`. Do not send card numbers, expiration dates, or security codes.
You can instead send `action: "alternative"` with text such as `"Pay with Shop Pay instead"`, or `action: "decline"` to refuse the charge. See [Send Agent Checkout Message](/api-reference/agent-checkouts/send-agent-checkout-message) for the complete schema.
Answer before the request's `expiresAt`. If it expires, observe the run for a new request and answer its new `requestId`.
Never put card details in the `task`, a buyer profile, form answers, or a text message. Raw card details are not accepted.
The accepted response resumes the run. Continue streaming messages or reading the run until it completes, asks for another input, or stops.
If the merchant total changes or the authorization cannot be used, the agent sends a new payment request. Answer the new `requestId`; do not reuse a response from an earlier request.
## Handle a Klarna Payment Request
Some merchants take payment through Klarna. Then the payment request has `method: "klarna"` and links to a page where the buyer pays with their own Klarna account:
```json theme={null}
{
"kind": "payment",
"purpose": "checkout_payment",
"method": "klarna",
"amount": { "kind": "exact", "value": "129.00", "currency": "GBP" },
"merchant": { "url": "https://shop.example.com" },
"authorizationUrl": "https://uc.example.com/payments/klarna/run_123/0123456789abcdefghijABCDEFGHIJ_-"
}
```
* Show `authorizationUrl` to the buyer unchanged, for example as a link or in a new tab. Do not rewrite it or join it to another origin.
* Do not create an order intent, and do not send an `input_response`. The run continues after the buyer pays. [Follow the run](/agents/checkouts/follow-run) for the result.
* `amount` is the exact total. `merchant` has only `url`.
`authorizationUrl` is a payment capability: anyone who has it can open the payment page. Give it only to the buyer. Do not log it or put it in analytics, traces, or error reports.
## Handle Authorization Problems
Submitting the ID acknowledges authorization; it does not confirm that the merchant accepted the card. [Follow the run](/agents/checkouts/follow-run) for application outcomes, another request, or the final result.
| Condition | Your Next Action |
| - | - |
| The order intent is expired or revoked | Create a replacement when answering the current open payment request. |
| No card rail is active | Complete the verification or CVC recollection required by the selected rail; recheck the order intent. |
| The checkout asks for a new amount or authorization | Use that new request's amount and merchant when creating the replacement order intent. |
| The request closed or expired | Read the run again and handle its current action. |
| The merchant declines the card | The run ends as `blocked` with `result.code: "merchant.payment_declined"`. |
Use the same production project and buyer for the run, saved card, and order intent. Your application sends identifiers instead of raw card data. Protected collection does not guarantee that values applied to merchant inputs are absent from later browser observations or recordings.
For web, native, messaging, and server integrations, see [Connect Your User Channel](/agents/checkouts/follow-run#connect-your-user-channel).
## Next Steps
Create the order intent and verify its selected rail
Reuse a signed-in session on later checkouts
# How Agents Buy
Source: https://docs.crossmint.com/agents/how-agents-buy
Why a secure payment method was not enough for an agent to buy anything, and how one API covers every merchant
Once an agent has a secure card, a second problem appears: it still cannot buy much. Almost every merchant built its checkout for a human with a mouse.
## Problems
An agent holding an Agent Card has spending power, but no place to use it programmatically:
* **Most merchants have no purchase API.** There is nothing to call. The only path to a purchase is the same web checkout a human uses.
* **Checkouts are built to keep bots out.** CAPTCHAs, bot detection, and multi-factor prompts are aimed precisely at automated buyers.
* **Every merchant is different.** Carts, guest checkout, account login, shipping forms, coupon fields, and payment pages all vary. A hand-rolled integration works for one store and breaks on the next.
* **Browser automation is slow and expensive even when it works.** A general-purpose browser agent has to look at every page, decide what to click, and recover from every surprise. One checkout can take minutes and cost several dollars in tokens.
* **Generic automation is not a checkout solution.** A browser agent built for any task treats a purchase like any other web page. A checkout needs things it does not provide: a spending cap enforced outside the model, a way to enter card details without exposing them to the model, a structured way to ask the buyer a question mid-run, and a structured result at the end. It also starts from scratch at each merchant instead of reusing what worked there before.
Three approaches to closing that gap emerged, and each covers only part of the market:
| Approach | Where it works | Where it falls short |
| - | - | - |
| **Merchant APIs** | The few merchants that expose one | Nearly every other merchant |
| **Agentic commerce protocols** (UCP, ACP, AP2, ...) | Merchants that have adopted a given protocol | Adoption is early and fragmented across several protocols, and many implementations hand back a checkout link rather than completing the purchase |
| **Browser automation** | Anywhere a human could buy | Slow, brittle, and expensive: with frontier models a single checkout can cost several dollars in tokens, and it needs constant maintenance |
To buy anywhere, an agent developer would have to build and maintain all three, choose between them per merchant, and keep up as protocols change. That is the effort most teams cannot afford, and the reason many "shopping agents" stop at a product link.
## Solutions
### Agent Checkouts
[Agent Checkouts](/agents/checkouts/overview) puts all three approaches behind one API. Hand it a product URL, a natural-language request, and a hard spending cap, and Crossmint completes the purchase on your user's behalf:
* **The right rail for each merchant.** Crossmint uses an agentic commerce protocol where the merchant supports one, and runs an **optimized browser session** as the universal fallback everywhere else. You integrate once and get every rail, including new ones as merchants adopt them.
* **A complete purchase, not a cart.** The checkout runs end to end and returns a receipt with the merchant order id, not a link for the user to finish.
* **Any payment method the merchant accepts.** A Crossmint Agent Card, an agent card from another provider such as Ramp or Link, a card already on file in the user's merchant account, Apple Pay, Google Pay, PayPal, or a local method such as Shop Pay, bank transfer, or Bizum.
* **Bounded and observable.** The checkout never spends above the cap you set. You can embed the live browser session so the user watches the purchase happen, and the agent asks the user only for what you did not provide up front.
* **Crossmint runs the browser side.** The browser infrastructure, the models, the prompts, and the per-merchant behavior live in Crossmint's service rather than in your integration. Improvements to them apply to every checkout without a change on your side.
### x402 and MPP
Not every purchase has a checkout page. When the counterparty is an API or another agent, the payment is a protocol exchange settled in stablecoins:
| Protocol | How it works | Best for |
| - | - | - |
| [x402](/agents/payment-flows/x402) | The agent calls an endpoint, receives `402 Payment Required`, pays in stablecoin, and retries | Pay-per-call APIs and micropayments |
| [MPP](/agents/payment-flows/mpp) | Agent-to-agent and service-to-service payments over the Machine Payment Protocol | Programmatic machine economies |
Both are paid from an [Agent Wallet](/agents/wallets/overview), with the same scoped, revocable permissions described in [How Agents Pay](/agents/how-agents-pay).
One integration covers both kinds of counterparty: Agent Checkouts for merchants, x402 and MPP for machines. The per-merchant work lives in Crossmint's service, not in your codebase.
## See Also
Scoped cards and wallets the user controls
The product, its features, and where to start
Create your first checkout from a product URL
# How Agents Pay
Source: https://docs.crossmint.com/agents/how-agents-pay
Why giving an agent a payment method used to mean handing over the user's real card, and how scoped cards and wallets fix it
An agent that can only recommend is a search engine. An agent that can act needs to spend money, and the web gave developers no safe way to let it.
## Problems
The only payment credential most of your users have is a card, and nothing about a card was designed to be handed to software that acts on its own:
* **The real card number ends up in the agent's context.** It travels through prompts, logs, tool calls, and model providers. Once leaked, it can be charged again and again. There is no limit the agent cannot exceed, no expiry, and no way to revoke without cancelling the card. It also drags the developer into PCI scope.
* **Keeping a human in the loop defeats the purpose.** Approving every payment is safe, but the user is interrupted at the exact moment the agent was supposed to save them time.
* **Prepaid or virtual cards from a fintech give up the user's card.** The user funds a new balance instead of using the card they already have, loses their rewards, refunds, and chargeback protections, and the developer often becomes the account holder for money that is not theirs.
* **Custodial agent wallets create custody and trust problems.** Someone other than the user controls the funds, which raises licensing questions and still gives the agent unlimited access to whatever is in the wallet.
* **Limits enforced only in application code do not hold.** They stop honest mistakes, not a compromised agent, a manipulated model, or a leaked credential.
Agents amplify every one of these risks. An agent with unlimited access to money is a liability. What is missing is spending power that is **scoped, enforced outside the agent, and revocable by the user**, while the user keeps their own card.
## Solutions
Crossmint gives agents two payment instruments. Both follow the same principle: the user delegates a bounded allowance, the bound is enforced outside the agent, and the user can revoke it at any time.
### Agent Cards
The user saves their real card once, in a Crossmint-hosted PCI-compliant iframe. From that saved card, your app creates as many **scoped authorizations** as it needs. Crossmint calls these **order intents**: each one is an allowance with an amount, a description, an expiration, and optionally a merchant. The user approves each one, and can cancel it at any time.
From an active order intent, the agent **mints credentials on the rail the purchase needs**:
| Rail | What the agent receives | Enforced by |
| - | - | - |
| Visa Intelligent Commerce | A one-time card number or network token bounded by the allowance | Visa |
| Mastercard Agent Pay | A one-time card number or network token bounded by the allowance | Mastercard |
| Encrypted card | The saved card encrypted to a key only the agent holds, for cards outside the network programs; minted for an amount the order intent consumes | The order intent, for the amount; the card network does not bound the card |
| Protocol credentials (SPT) | A protocol-specific token for merchants and services that accept it | The protocol |
Depending on the rail, the credential is delivered as a one-time card number, a network token, or a protocol-specific format such as MPP.
The real card number never leaves the vault. A leaked network credential is worthless: it is bounded to an amount, expires, and is revoked with the order intent. The encrypted card is the exception: it decrypts to the saved card, so the allowance is yours to enforce and you should cancel the order intent as soon as it is no longer needed. And because the purchase runs on the user's own card, rewards, refunds, and chargebacks keep working. Read the [Agent Cards overview](/agents/cards/overview) for the product.
### Agent Wallets
For machine-to-machine payments, cards do not fit. Paying an API per call or settling with another agent involves amounts too small and too frequent for card fees and chargeback rules. These payments run on stablecoins.
The user owns a non-custodial wallet and adds the agent as a **signer with a scoped permission set**: a spend cap, allowed counterparties, and a time window. The permissions are enforced onchain, so a transaction outside them is rejected at the wallet level, and the user can revoke the signer at any time. Neither you nor Crossmint takes custody. Read the [Agent Wallets overview](/agents/wallets/overview) for the product.
Every delegation is explicit, scoped, enforced outside the agent, and revocable. The agent gets exactly the spending power the user granted, and nothing that survives a leak.
## See Also
Why a secure card was not enough, and how one API buys from any merchant
Save once, authorize many, pay on the right rail
Scoped, non-custodial stablecoin wallets
# Overview
Source: https://docs.crossmint.com/agents/overview
Payment infrastructure for AI agents
Enable agents to **buy anything**, all under spending rules defined and controlled by your users. Integrate via a white-label SDK that plugs into your agent framework as a tool or skill, without handling sensitive credentials and while staying PCI-compliant.
Three products make it happen. Each one works on its own and they are built to compose.
## Products
Save a user's card once and issue scoped, revocable agent cards from it
Non-custodial stablecoin wallets with scoped agent access, for x402, MPP, and onchain payments
One API to buy from any merchant with any payment method it accepts
## Key Features
Every allowance is scoped, explicit, and revocable
Card limits hold at Visa and Mastercard, wallet limits hold onchain
Agents pay with one-time or encrypted credentials, never the user's card number
Agentic commerce protocols where merchants support them, an optimized browser everywhere else
Crossmint cards, third-party agent cards, cards on file, Apple Pay, Google Pay, PayPal, and local methods
Cards live in Crossmint's vault, so your app never touches card data
## How They Fit Together
Cards and wallets give the agent **spending power** the user can scope and revoke. Agent Checkouts and the x402 and MPP protocols are the **places that spending power goes**.
| You want the agent to... | Spending power | Where it spends |
| - | - | - |
| Buy from an online store or pay a bill | [Agent Cards](/agents/cards/overview) or any card | [Agent Checkouts](/agents/checkouts/overview) |
| Pay an API per call, or pay another agent | [Agent Wallets](/agents/wallets/overview) | [x402](/agents/payment-flows/x402) and [MPP](/agents/payment-flows/mpp) |
| Send stablecoins, swap, or call a contract | [Agent Wallets](/agents/wallets/overview) | [Onchain actions](/agents/payment-methods/stablecoin-wallets/on-chain-actions) |
Why agents could not pay securely, and how scoped cards and wallets fix it
Why a secure card was not enough to buy anything, and how one API covers every merchant
## Get Started
Get an agent paying with agent cards
Get an agent paying with stablecoins
Buy from any merchant with a single API
# MPP
Source: https://docs.crossmint.com/agents/payment-flows/mpp
Pay MPP endpoints with a Crossmint EVM wallet
This guide shows how to pay MPP (Machine Payment Protocol) endpoints using a Crossmint wallet. The example uses the `mppx/client` library to handle the payment automatically.
## Prerequisites
* A Crossmint EVM wallet with USDC. See the [Wallets quickstart](/agents/stablecoin-wallet-quickstart), then [fund it with USDC](/agents/payment-methods/stablecoin-wallets/onramp-add-funds).
* The agent must be authorized as a signer on the wallet. See [Authorize the Agent](/agents/payment-methods/stablecoin-wallets/authorize-agent).
## Install dependencies
```bash npm theme={null}
npm i @crossmint/client-sdk-react-ui mppx/client viem
```
```bash yarn theme={null}
yarn add @crossmint/client-sdk-react-ui mppx/client viem
```
```bash pnpm theme={null}
pnpm add @crossmint/client-sdk-react-ui mppx/client viem
```
```bash bun theme={null}
bun add @crossmint/client-sdk-react-ui mppx/client viem
```
## Pay an MPP endpoint
```tsx mpp-payment.tsx theme={null}
import { EVMWallet, useWallet } from "@crossmint/client-sdk-react-ui";
import {
createWalletClient,
custom,
http,
MethodNotFoundRpcError,
type EIP1193RequestFn,
} from "viem";
import { tempoDevnet } from "viem/chains";
import { Mppx, tempo } from "mppx/client";
import { CrossmintWallets, createCrossmint } from "@crossmint/wallets-sdk";
const crossmint = createCrossmint({
apiKey: "",
});
const crossmintWallets = CrossmintWallets.from(crossmint);
const wallet = await crossmintWallets.getWallet(
"",
{ chain: "base" }
);
// Use the agent signer
await wallet.useSigner({
type: "server",
secret: process.env.CROSSMINT_SIGNER_SECRET,
});
const evmWallet = EVMWallet.from(wallet);
const address = evmWallet.address as `0x${string}`;
const httpTransport = http();
const request: EIP1193RequestFn = async ({ method, params }) => {
if (method === "wallet_sendCalls") {
throw new MethodNotFoundRpcError(
new Error("wallet_sendCalls not implemented; falling back"),
);
}
if (method === "eth_sendTransaction") {
const [tx] = params as Array<{
to: `0x${string}`;
data?: `0x${string}`;
value?: `0x${string}`;
}>;
const sent = await evmWallet.sendTransaction({
to: tx.to,
data: tx.data ?? "0x",
value: tx.value ? BigInt(tx.value) : undefined,
});
return sent.hash as `0x${string}`;
}
return httpTransport.request({ method, params } as any);
};
const walletClient = createWalletClient({
account: address,
chain: tempoDevnet,
transport: custom({ request }),
});
const mppx = Mppx.create({
polyfill: false,
methods: [
tempo({
mode: "push",
getClient: () => walletClient as any,
}),
],
});
const response = await mppx.fetch("https://api.example.com/protected");
const responseText = await response.text();
console.log("MPP response:", response.status, responseText);
```
## How the code works
The `EVMWallet.from(wallet)` call wraps the Crossmint wallet into an EVM-compatible interface. A custom EIP-1193 request handler is created that delegates `eth_sendTransaction` calls to the Crossmint wallet, which sends transactions using the agent's delegated permissions.
A viem `walletClient` is created with the custom transport, targeting the Tempo devnet chain. This client bridges the Crossmint wallet to standard EVM tooling.
`Mppx.create` initializes the MPP client with the `tempo` payment method in push mode. The `getClient` callback provides the wallet client for signing and sending the onchain transactions.
`mppx.fetch` works like a standard `fetch` call but automatically handles MPP payment negotiation. If the endpoint requires payment, the client pays via Tempo and retries the request.
# x402
Source: https://docs.crossmint.com/agents/payment-flows/x402
Pay x402-protected endpoints with a Crossmint wallet.
This guide shows how to pay x402-protected endpoints using a Crossmint wallet. The example uses the `@x402/core` client library to handle the 402 negotiation and payment automatically.
## Prerequisites
* A funded Crossmint EVM wallet on Base with USDC. See the [Wallets quickstart](/agents/stablecoin-wallet-quickstart), then [fund it with USDC](/agents/payment-methods/stablecoin-wallets/onramp-add-funds).
* The agent must be authorized as a signer on the wallet. See [Authorize the Agent](/agents/payment-methods/stablecoin-wallets/authorize-agent).
## Install dependencies
```bash npm theme={null}
npm i @crossmint/client-sdk-react-ui @crossmint/wallets-sdk @x402/core @x402/evm viem
```
```bash yarn theme={null}
yarn add @crossmint/client-sdk-react-ui @crossmint/wallets-sdk @x402/core @x402/evm viem
```
```bash pnpm theme={null}
pnpm add @crossmint/client-sdk-react-ui @crossmint/wallets-sdk @x402/core @x402/evm viem
```
```bash bun theme={null}
bun add @crossmint/client-sdk-react-ui @crossmint/wallets-sdk @x402/core @x402/evm viem
```
## Fund the wallet
Before the wallet can pay an x402 endpoint, it must hold USDC. In production, fund it through the [Crossmint onramp](/agents/payment-methods/stablecoin-wallets/onramp-add-funds) or by transferring USDC directly to `wallet.address`. In staging, `wallet.stagingFund(5)` mints test USDXM into the wallet.
```tsx x402-funding.tsx theme={null}
// Add this after you have a wallet handle and an authorized signer.
// In production, replace wallet.stagingFund(5) with your onramp order or direct transfer.
await wallet.stagingFund(5); // 5 USDXM, staging only
```
## Pay an x402 endpoint
```tsx x402-payment.tsx theme={null}
import { EVMWallet } from "@crossmint/client-sdk-react-ui";
import { x402Client, x402HTTPClient, wrapFetchWithPayment } from "@x402/core/client";
import { ExactEvmScheme } from "@x402/evm/exact/client";
import type { Hex } from "viem";
import { CrossmintWallets, createCrossmint } from "@crossmint/wallets-sdk";
const crossmint = createCrossmint({
apiKey: "",
});
const crossmintWallets = CrossmintWallets.from(crossmint);
const wallet = await crossmintWallets.getWallet(
"",
{ chain: "base" }
);
// Use the agent signer
await wallet.useSigner({
type: "server",
secret: process.env.CROSSMINT_SIGNER_SECRET,
});
// The wallet must be funded with USDC before it can pay x402 endpoints.
// In production, use the Crossmint onramp or a direct USDC transfer.
// In staging, call wallet.stagingFund(5).
const evmWallet = EVMWallet.from(wallet);
const x402Signer = {
address: evmWallet.address as `0x${string}`,
async signTypedData(typedData: any) {
const { signature } = await evmWallet.signTypedData({
...typedData,
chain: "base",
});
return signature as Hex;
},
};
const client = new x402Client();
client.register("eip155:*", new ExactEvmScheme(x402Signer));
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
const response = await fetchWithPayment("https://api.example.com/protected", {
method: "GET",
});
const responseText = await response.text();
console.log("Response:", response.status, responseText);
if (response.ok) {
const httpClient = new x402HTTPClient(client);
const paymentResponse = httpClient.getPaymentSettleResponse(
(name) => response.headers.get(name)
);
console.log("Payment settled:", paymentResponse);
}
```
## How the code works
The `EVMWallet.from(wallet)` call wraps the Crossmint wallet into an EVM-compatible interface. A custom signer object is created that delegates `signTypedData` calls to the Crossmint wallet, which signs on Base using the agent's delegated permissions.
The `x402Client` is initialized and the EVM scheme is registered for all EIP-155 chains (`eip155:*`). This tells the x402 client how to sign payments on any EVM chain.
`wrapFetchWithPayment` returns a modified `fetch` function that automatically handles the 402 flow: if a request returns `402 Payment Required`, it reads the payment terms, signs a stablecoin payment, and retries with the payment proof attached.
After a successful response, `getPaymentSettleResponse` extracts the settlement receipt from the response headers, confirming the payment was processed onchain.
## Learn more
Set up a stablecoin wallet for your agent.
Fund the wallet with USDC so the agent can pay.
# Create an Agent Card
Source: https://docs.crossmint.com/agents/payment-methods/cards/create-agent-card
Create and authorize an order intent backed by a saved card
An agent card is an order intent that gives an agent a bounded amount to spend before a fixed expiration. An order intent can have multiple rails, each with its own status, credential formats, and verification.
## Prerequisites
* **Registered card** — [register a saved card](/agents/payment-methods/cards/register-card). A card with an `enabled` rail unlocks card-network credentials; a card without one can still use the encrypted card fallback described below.
* **Encrypted card access (production only)** — the `encrypted-card` fallback is enabled for every staging project. In production it must be enabled for your project: contact us for access.
* **Crossmint API key** — a client-side key with `order-intents.create` and `order-intents.read` scopes. In staging, all scopes are included by default.
* **User JWT** — use the JWT for the user who owns the saved card.
## Create the Order Intent
Create an order intent with the saved card, spending limit, description, expiration, and merchant. Include the merchant now when you already know where the agent will spend:
```typescript theme={null}
const CROSSMINT_CLIENT_API_KEY = "YOUR_CROSSMINT_CLIENT_API_KEY";
const jwt = "YOUR_USER_JWT";
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
const response = await fetch("https://staging.crossmint.com/api/unstable/order-intents", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": CROSSMINT_CLIENT_API_KEY,
Authorization: `Bearer ${jwt}`,
},
body: JSON.stringify({
paymentMethodId: "pm_123",
amount: { value: "150.00", currency: "USD" },
merchant: {
name: "Acme Store",
url: "https://acme.example.com",
countryCode: "US",
},
description: "Weekly grocery purchases",
expiresAt,
}),
});
if (!response.ok) {
throw new Error(`Order intent creation failed (${response.status})`);
}
const orderIntent = await response.json();
```
The response separates the order intent's lifecycle from the status of each rail:
```json theme={null}
{
"orderIntentId": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"paymentMethodId": "pm_123",
"status": "active",
"amount": {
"total": "150.00",
"spent": "0.00",
"reserved": "0.00",
"available": "150.00",
"currency": "USD"
},
"merchant": {
"name": "Acme Store",
"url": "https://acme.example.com",
"countryCode": "US"
},
"description": "Weekly grocery purchases",
"rails": [
{
"rail": "agentic-token",
"provider": "vic",
"status": "pending_verification",
"credentialFormats": ["card", "network-token"]
}
],
"verificationConfig": {
"environment": "test",
"publicApiKey": "key_test_123",
"allowanceId": "alw_123"
},
"expiresAt": "2099-01-01T00:00:00.000Z"
}
```
Setting `merchant` when creating the order intent is preferred when you know it. The merchant is fixed for the lifetime of that order intent, and credential requests inherit the restriction.
If the agent will choose a merchant later, omit `merchant`. This creates an open order intent, and every credential request must supply the merchant instead.
The top-level `status` is `active` while the allowance exists. A specific rail can still require verification.
Treat `rails` as a set of independent ways to spend the same allowance:
1. Choose the rail that provides the credential you need.
2. If that rail is `pending_verification`, verify it.
3. Once it is `active`, mint with the same `rail` and `provider` values.
You do not need every rail to be active. Other rails can remain `pending_verification` or `error` without blocking the selected rail. For example, when an allowance contains both VIC and SPT, minting a VIC credential only requires the VIC rail to be verified; you do not need to complete an SPT step. Crossmint currently exposes VIC and Mastercard Agent Pay card-network rails, and the same selection rule applies as more rails are added.
### Encrypted Card Fallback
When no card-network rail can mint credentials, because registration reported none, every rail is in `error`, or none supports a credential format, an active order intent appends an `encrypted-card` rail to `rails`. Card-network rails that failed are kept alongside it so you can still read their `error`, so select the entry whose `rail` is `encrypted-card` rather than assuming a one-element array:
```json theme={null}
{
"rails": [
{
"rail": "agentic-token",
"provider": "vic",
"status": "error",
"credentialFormats": [],
"error": { "code": "CARD_REJECTED" }
},
{
"rail": "encrypted-card",
"status": "active",
"credentialFormats": ["card"]
}
]
}
```
In production, the `encrypted-card` rail appears only when encrypted card access is enabled for your project (contact us for access); without it, an order intent with no usable card-network rail returns `rails` without a fallback, and a direct `encrypted-card` credential request fails with `ADDON_REQUIRED`. This access is granted per project because minting from this rail gives your application the user's real card number. Agent Checkouts is the exception: see [Use with Agent Checkouts](#use-with-agent-checkouts).
The `encrypted-card` rail has no `provider` and is always `active`. It never requires verification, so skip the `OrderIntentVerification` step and mint directly. Instead of a network-issued one-time card, minting returns the user's saved card encrypted to an RSA public key that you supply, so only the holder of the matching private key can read it. See [Retrieve Secure Card Numbers](/agents/payment-methods/cards/retrieve-agent-card#mint-an-encrypted-card) for the request format.
The fallback appears only when no card-network rail is usable. As soon as a card-network rail becomes available, prefer it: it enforces the allowance at the network level and never exposes the real card number.
## Verify the Selected Rail
When the rail you want to use has `status: "pending_verification"`, render `OrderIntentVerification` in your client application. Do not start verification merely because an unrelated rail is pending.
```tsx theme={null}
import {
OrderIntentVerification,
type OrderIntentVerificationProps,
} from "@crossmint/client-sdk-react-ui";
function VerifyOrderIntent({
orderIntent,
}: {
orderIntent: OrderIntentVerificationProps["orderIntent"];
}) {
return (
{
console.log("Allowance verified");
}}
onVerificationError={(error) => {
console.error("Allowance verification failed", error);
}}
/>
);
}
```
After verification completes, fetch the order intent again and wait for the selected rail to report `status: "active"`. You do not need to wait for the other rails.
Card registration never prompts for verification. Verification belongs to an individual order-intent rail. The first Visa verification on a device can create a passkey; later order intents authenticate with the existing passkey when the device remains bound. The component also handles the Mastercard-hosted flow.
For the complete request and response schemas, see the [Create Order Intent API Reference](/api-reference/agentic-commerce/order-intents/create-order-intent).
## Use with Agent Checkouts
For checkout payment requests, follow [Authorize Payments](/agents/checkouts/payments) to create the order intent and submit its ID.
Agent Checkouts can pay with the `encrypted-card` rail without encrypted card access enabled for your project. The access gate keeps the card number from reaching your systems. Inside an Agent Checkout, the card never leaves the Crossmint vault: the checkout uses it to pay the merchant, and your application only handles the `orderIntentId`. Without that access, your own reads of the order intent do not list the rail, but the checkout still uses it when the saved card's CVC is live. If the checkout answers `rail_unavailable`, [recollect the CVC](/agents/payment-methods/cards/recollect-cvc).
## Common Gotchas
Read `rails[].status` before minting. The top-level `status` describes the order intent's lifetime, not whether every rail is ready.
Register the card once, then verify only the order-intent rail you plan to use when it returns `pending_verification`. Do not repeat card registration to authorize a new allowance.
Card networks enforce `amount` and `merchant` only for `agentic-token` rails. When your agent spends with an encrypted card, keep the purchase within the order intent's limits yourself and cancel the order intent when it is no longer needed.
Set `expiresAt` to a future ISO 8601 timestamp that matches the permission you present to the user.
Include `merchant` when creating the order intent if you already know it. Otherwise, omit it and supply a merchant with every credential request.
Use an HTTPS tunnel when testing the verification ceremony from a local browser.
## Next Steps
Mint a credential from an active rail
Style the allowance verification modal and the CVC recollection form
# Customize UI
Source: https://docs.crossmint.com/agents/payment-methods/cards/customize-verification-ui
Style the allowance verification modal and the CVC recollection form to match your brand
The cards flow ships two hosted UIs, both styled through an `appearance` prop:
* `OrderIntentVerification` renders the modal where the user completes the card network's verification, such as a Visa passkey (see [Create an Agent Card](/agents/payment-methods/cards/create-agent-card#verify-the-selected-rail)).
* `CrossmintCvcRecollection` renders the field where the user re-enters a saved card's CVC after it expires (see [Recollect the CVC](/agents/payment-methods/cards/recollect-cvc)).
## Customize the Verification Modal
The `OrderIntentVerification` appearance object has two top-level keys: **`variables`** (global design tokens) and **`rules`** (per-element overrides).
Global design tokens that apply across the entire modal.
| Token | What it controls |
| - | - |
| `fontFamily` | Font stack for all text |
| `fontSizeUnit` | Base body font size (for example, `"14px"`). Title, small, error, input, and passkey sizes scale proportionally |
| `spacingUnit` | Base section spacing (for example, `"16px"`). Modal, input, button-gap, and option-gap scale proportionally |
| `borderRadius` | Default border radius for modal, input, button, and option elements |
| `colors.accent` | Primary/brand color — buttons, focus rings, radio selected state, spinner |
| `colors.textPrimary` | Main text color and button text |
| `colors.textSecondary` | Secondary and placeholder text |
| `colors.backgroundPrimary` | Surface/card background |
| `colors.backgroundSecondary` | Input background, secondary button background |
| `colors.border` | Default border for modal, inputs, and radio |
| `colors.danger` | Error text, error border, error icon |
| `colors.success` | Success icon, success background |
The SDK auto-infers missing color tokens from the ones you provide. For example, setting only `backgroundPrimary` will derive `textPrimary`, `textSecondary`, `backgroundSecondary`, and `border` automatically.
Per-element overrides that take precedence over variables.
| Rule | Customizable properties |
| - | - |
| `Overlay` | `colors.background` (backdrop behind the modal) |
| `Modal` | `borderRadius`, `colors.border` |
| `Input` | `borderRadius`, `colors.background`, `colors.border` |
| `PrimaryButton` | `borderRadius`, `colors.text`, `colors.background`, `hover.colors.background`, `disabled.colors.background` |
| `SecondaryButton` | `colors.text`, `colors.background`, `hover.colors.background` |
| `CloseButton` | `colors.background`, `hover.colors.background` |
| `Radio` | `colors.border`, `selected.colors.border`, `selected.colors.background`, `selected.colors.dot` |
### Apply the Appearance
You do not need to specify everything. The SDK infers missing colors:
```tsx theme={null}
import {
OrderIntentVerification,
type OrderIntentVerificationProps,
} from "@crossmint/client-sdk-react-ui";
function VerifyOrderIntent({
orderIntent,
}: {
orderIntent: OrderIntentVerificationProps["orderIntent"];
}) {
return (
{
console.log("Allowance verified");
}}
onVerificationError={(error) => {
console.error("Allowance verification failed", error);
}}
/>
);
}
```
This alone will auto-derive `textPrimary`, `textSecondary`, `backgroundSecondary`, and `border` from the dark background, and use the accent color for buttons, focus rings, and radio selections.
## Customize the CVC Recollection Form
`CrossmintCvcRecollection` accepts the same `appearance` object as [`CrossmintPaymentMethodManagement`](/agents/payment-methods/cards/save-card#steps). Keep `fontSizeUnit` and `spacingUnit` close to their defaults: they multiply the component's internal sizes rather than set a base size.
| Token | What it controls |
| - | - |
| `fontFamily` | Font stack for the label, input, and button |
| `fontSizeUnit` | Font size multiplier (default `"4px"`) |
| `spacingUnit` | Spacing multiplier (default `"3.33px"`) |
| `borderRadius` | Border radius for the input and button |
| `colors.accent` | Button background and focus ring |
| `colors.textPrimary` | Label and input text |
| `colors.backgroundPrimary` | Input background |
| `colors.borderPrimary` | Input border |
| `colors.danger` | Error text and error border |
```tsx theme={null}
import { CrossmintCvcRecollection } from "@crossmint/client-sdk-react-ui";
function RecollectCvc({ jwt, paymentMethodId }: { jwt: string; paymentMethodId: string }) {
return (
{
console.log("CVC updated");
}}
/>
);
}
```
# Recollect the CVC
Source: https://docs.crossmint.com/agents/payment-methods/cards/recollect-cvc
Ask the user to re-enter a saved card's CVC when it expires
The `encrypted-card` rail returns the user's saved card, including the CVC encrypted when the card was saved. Crossmint keeps that CVC for a limited time. Once it expires, the rail stops minting until the user types the CVC again. `CrossmintCvcRecollection` from `@crossmint/client-sdk-react-ui` renders Crossmint's hosted CVC field for that step: the digits never reach your application.
## Prerequisites
* **Saved and registered card** — follow [Save a Card](/agents/payment-methods/cards/save-card) and [Register a Card](/agents/payment-methods/cards/register-card).
* **Order intent with an `encrypted-card` rail** — follow [Create an Agent Card](/agents/payment-methods/cards/create-agent-card).
* **Crossmint API key** — a client-side key with the `order-intents.read`, `payment-methods.read`, and `payment-methods.update` scopes. In staging, all scopes are included by default.
* **User JWT** — the JWT for the user who owns the card.
* **React SDK** — `@crossmint/client-sdk-react-ui` 4.7.0 or later, with `CrossmintProvider` configured as in the other card guides.
## When to Render the Component
Two signals tell you that the CVC must be collected again. Handle both.
| Signal | Where it appears | Meaning |
| - | - | - |
| `status: "pending_cvc_recollection"` | The `encrypted-card` entry in `rails` of `GET /api/unstable/order-intents/{orderIntentId}` | The saved CVC expired. The rail cannot mint until the user re-enters it |
| `409` with `code: "ORDER_INTENT_CVC_RECOLLECTION_REQUIRED"` | `POST /api/unstable/order-intents/{orderIntentId}/credentials` with `rail: "encrypted-card"` | The CVC expired between your last read and the mint, or is no longer available |
A rail read as `active` can still answer `409` if the CVC expires before the mint. Read the order intent right before the user starts the mint flow, and treat the `409` as the same condition as the pending status.
```typescript theme={null}
const encryptedCard = orderIntent.rails.find((rail) => rail.rail === "encrypted-card");
const needsCvc = encryptedCard?.status === "pending_cvc_recollection";
```
## Render the Hosted CVC Field
Render `CrossmintCvcRecollection` in your client application with the user's JWT and the `paymentMethodId` of the order intent's card. The component renders a single CVC input and a confirm button, and no explanation of its own, so tell the user why the code is requested again.
```tsx theme={null}
import { CrossmintCvcRecollection } from "@crossmint/client-sdk-react-ui";
function RecollectCvc({ jwt, paymentMethodId }: { jwt: string; paymentMethodId: string }) {
return (
The security code for this card has expired. Enter it again to continue.
);
}
```
After `onComplete` fires, fetch the order intent again: the `encrypted-card` rail returns to `status: "active"` and you can mint as described in [Retrieve Secure Card Numbers](/agents/payment-methods/cards/retrieve-agent-card#mint-an-encrypted-card).
`onError` receives `{ reason, retriable, message }`. When `retriable` is `true`, the form stays mounted and the user can submit again. When it is `false`, the component renders nothing afterwards, so unmount it and show your own message.
To style the form, pass an `appearance` prop as described in [Customize UI](/agents/payment-methods/cards/customize-verification-ui#customize-the-cvc-recollection-form).
## Test in Staging
A freshly saved card does not reach `pending_cvc_recollection` during a normal test run. In staging, expire the CVC on demand:
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/unstable/cvc-recollection/expire \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: ' \
--header 'Authorization: Bearer ' \
--data '{ "paymentMethodId": "" }'
```
The call answers `204`. The rail then reports `pending_cvc_recollection` and mints answer `409` until the user completes the form. The endpoint is staging-only.
## Next Steps
Mint the encrypted card once the rail is active again
Cancel an order intent or delete a saved card
# Register a Card
Source: https://docs.crossmint.com/agents/payment-methods/cards/register-card
Register a saved card and discover the order-intent rails it supports
Register a saved card once before creating order intents against it. Registration provisions every supported rail, such as Visa Intelligent Commerce or Mastercard Agent Pay. It does not grant spending permission and does not prompt the user for verification.
## Prerequisites
* **Saved card** — follow [Save a Card](/agents/payment-methods/cards/save-card) and keep its `paymentMethodId`.
* **Crossmint API key** — a client-side key with the `payment-methods.create`, `payment-methods.read`, and `payment-methods.update` scopes. The `update` scope lets the user re-enter the card security code (CVC) later, when a card credential needs it. In staging, all scopes are included by default.
* **User JWT** — use the JWT for the user who owns the saved card.
## Register the Card
Send an idempotent `PUT` request with the consumer information used by card-network rails:
```typescript theme={null}
const CROSSMINT_CLIENT_API_KEY = "YOUR_CROSSMINT_CLIENT_API_KEY";
const jwt = "YOUR_USER_JWT";
const paymentMethodId = "pm_123";
const response = await fetch(
`https://staging.crossmint.com/api/unstable/payment-methods/${paymentMethodId}/order-intent-registration`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-API-KEY": CROSSMINT_CLIENT_API_KEY,
Authorization: `Bearer ${jwt}`,
},
body: JSON.stringify({
email: "buyer@example.com",
countryCode: "US",
languageCode: "en-US",
}),
}
);
if (!response.ok) {
throw new Error(`Card registration failed (${response.status})`);
}
const registration = await response.json();
```
The response reports one or more supported rails independently:
```json theme={null}
{
"paymentMethodId": "pm_123",
"rails": [
{
"rail": "agentic-token",
"provider": "vic",
"status": "enabled"
}
]
}
```
Read the complete `rails` array instead of branching on the card brand or assuming there is only one result. Visa cards can report `provider: "vic"`, and Mastercard cards can report `provider: "agentpay"`.
| Status | Meaning |
| - | - |
| `enabled` | The rail can back a new order intent |
| `pending` | Rail provisioning has not finished |
| `error` | Provisioning failed; read `error.code` |
More than one rail can be available. Registration provisions those rails, but it does not choose between them or require every rail to succeed. Use `GET /api/unstable/payment-methods/{paymentMethodId}/order-intent-registration` to read the registration again.
A card with no `enabled` rail, including one whose `rails` array is empty, can still back an order intent. In that case the order intent's `rails` array includes an `encrypted-card` rail that returns the saved card encrypted to your public key; in production, encrypted card access must be enabled for your project. See [Encrypted Card Fallback](/agents/payment-methods/cards/create-agent-card#encrypted-card-fallback).
Registration is separate from allowance verification: no email code, passkey, or spending approval occurs in this step. You choose which rail to use after creating an order intent.
## Test the Rails
In staging, the saved card number selects a deterministic registration and verification scenario, such as a rejected rail or a failed verification. See [Test Agent Cards](/agents/payment-methods/cards/test-agent-cards) for the test cards.
## Next Steps
Create an order intent backed by one of the card's rails
Run the complete flow in the reference app
# Cancel Card Access
Source: https://docs.crossmint.com/agents/payment-methods/cards/remove-cards
Cancel an order intent or delete a saved card
Cancel an order intent to stop minting credentials from that allowance. Delete a saved card when the user no longer wants it stored in Crossmint.
## Prerequisites
* **Crossmint API key** — a client-side key with `order-intents.revoke` or `payment-methods.delete`, depending on the operation. In staging, all scopes are included by default.
* **User JWT** — use the JWT for the user who owns the resource.
## Cancel an Order Intent
Send a `DELETE` request with the `orderIntentId`:
```typescript theme={null}
const CROSSMINT_CLIENT_API_KEY = "YOUR_CROSSMINT_CLIENT_API_KEY";
const jwt = "YOUR_USER_JWT";
const orderIntentId = "a1b2c3d4-e5f6-4890-abcd-ef1234567890";
const response = await fetch(
`https://staging.crossmint.com/api/unstable/order-intents/${orderIntentId}`,
{
method: "DELETE",
headers: {
"X-API-KEY": CROSSMINT_CLIENT_API_KEY,
Authorization: `Bearer ${jwt}`,
},
}
);
if (!response.ok) {
throw new Error(`Order intent cancellation failed (${response.status})`);
}
```
A successful cancellation returns `204 No Content`. Subsequent reads return `status: "cancelled"`, and no additional credentials can be minted from its rails. Create a new order intent when you need a replacement allowance.
## Delete a Saved Card
Send a `DELETE` request with the `paymentMethodId`:
```typescript theme={null}
const CROSSMINT_CLIENT_API_KEY = "YOUR_CROSSMINT_CLIENT_API_KEY";
const jwt = "YOUR_USER_JWT";
const paymentMethodId = "pm_123";
const response = await fetch(
`https://staging.crossmint.com/api/unstable/payment-methods/${paymentMethodId}`,
{
method: "DELETE",
headers: {
"X-API-KEY": CROSSMINT_CLIENT_API_KEY,
Authorization: `Bearer ${jwt}`,
},
}
);
if (!response.ok) {
throw new Error(`Payment method deletion failed (${response.status})`);
}
```
Deleting the saved card prevents new order intents from being created against it. Existing order intents remain independent resources, so cancel them explicitly when access must end immediately.
## Next Steps
Create a replacement order intent
Inspect order-intent status and balances
# Retrieve Secure Card Numbers
Source: https://docs.crossmint.com/agents/payment-methods/cards/retrieve-agent-card
Mint a one-time card-network credential or an encrypted saved card from an active order-intent rail
Mint a credential for a specific amount and merchant from an order intent. Choose one rail from the order-intent response and use that same rail for verification and credential minting.
The request and response shape depend on the rail:
| Rail | What you receive | When it appears |
| - | - | - |
| `agentic-token` | A one-time card number or network token issued by Visa Intelligent Commerce or Mastercard Agent Pay, bounded by the requested amount and merchant | The saved card supports a card-network rail |
| `encrypted-card` | The user's saved card, encrypted to an RSA public key that you provide. Minting consumes the requested amount from the order intent, like the other rails | No card-network rail is usable for the order intent. In production, only for projects with encrypted card access enabled |
## Prerequisites
* **Verified order intent** — follow [Create an Agent Card](/agents/payment-methods/cards/create-agent-card) and wait for a rail with `status: "active"`.
* **Crossmint API key** — a client-side key with the `order-intents.read` and `order-intents.credentials` scopes. To recover from `ORDER_INTENT_CVC_RECOLLECTION_REQUIRED` with the hosted CVC step, the same key also needs `payment-methods.read` and `payment-methods.update`. In staging, all scopes are included by default.
* **User JWT** — use the JWT for the user who owns the order intent.
## Mint a Card-Network Credential
Find the `agentic-token` rail you want to use and confirm that it is active and supports card credentials. Other rails can remain pending. Send the selected rail's `rail` and `provider` values back in the request.
This example continues from the merchant-scoped order intent created in the previous guide, so the credential request does not repeat the merchant:
```typescript theme={null}
const CROSSMINT_CLIENT_API_KEY = "YOUR_CROSSMINT_CLIENT_API_KEY";
const jwt = "YOUR_USER_JWT";
const orderIntentId = "a1b2c3d4-e5f6-4890-abcd-ef1234567890";
const orderIntentResponse = await fetch(
`https://staging.crossmint.com/api/unstable/order-intents/${orderIntentId}`,
{
headers: {
"X-API-KEY": CROSSMINT_CLIENT_API_KEY,
Authorization: `Bearer ${jwt}`,
},
}
);
if (!orderIntentResponse.ok) {
throw new Error(`Order intent retrieval failed (${orderIntentResponse.status})`);
}
const orderIntent = await orderIntentResponse.json();
const selectedRail = orderIntent.rails.find(
(rail) =>
rail.rail === "agentic-token" &&
rail.status === "active" &&
rail.credentialFormats.includes("card")
);
if (selectedRail == null) {
throw new Error("No active card-network rail supports card credentials");
}
const response = await fetch(
`https://staging.crossmint.com/api/unstable/order-intents/${orderIntent.orderIntentId}/credentials`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": CROSSMINT_CLIENT_API_KEY,
Authorization: `Bearer ${jwt}`,
},
body: JSON.stringify({
rail: selectedRail.rail,
provider: selectedRail.provider,
amount: { value: "25.00", currency: orderIntent.amount.currency },
credential: { format: "card" },
}),
}
);
if (!response.ok) {
throw new Error(`Credential creation failed (${response.status})`);
}
const result = await response.json();
```
### Open Order Intents
If `orderIntent.merchant` is absent, include a merchant in every credential request:
```typescript theme={null}
merchant: {
name: "Acme Store",
url: "https://acme.example.com",
countryCode: "US",
},
```
The response returns the selected rail and the credential value. An inactive status on another rail does not affect this request:
```json theme={null}
{
"id": "cred_123",
"rail": "agentic-token",
"provider": "vic",
"amount": { "value": "25.00", "currency": "USD" },
"credential": {
"format": "card",
"value": {
"number": "4000001000004242",
"expirationMonth": 12,
"expirationYear": 2030,
"cvc": "123"
}
},
"expiresAt": "2099-01-01T00:05:00.000Z"
}
```
Treat the credential as a secret. Use it immediately, never log it, and do not persist it for later reuse. Minting deducts the requested amount from the order intent even if the credential is never used.
For the complete schema, see the [Create Order Intent Credential API Reference](/api-reference/agentic-commerce/order-intents/get-order-intent-credentials).
## Mint an Encrypted Card
When the order intent's `rails` array contains an `encrypted-card` entry, the response cannot contain a network-issued one-time card. Instead, the user's saved card is encrypted inside the PCI-compliant vault to a public key that you control, and Crossmint returns the ciphertext. Only the holder of the matching private key can read the card.
### Generate a Key Pair
Generate an RSA key pair of at least 2048 bits for the `RSA-OAEP-256` algorithm. Keep the private key wherever the agent will decrypt the card; only the public key is sent to Crossmint.
```typescript theme={null}
const { publicKey, privateKey } = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["encrypt", "decrypt"]
);
const { kty, n, e } = await crypto.subtle.exportKey("jwk", publicKey);
```
### Request the Encrypted Card
Send the public key as a JWK with only the `kty`, `n`, and `e` members. The request takes the same `amount` and optional `merchant` as the other rails; the `encrypted-card` rail has no `provider`:
```typescript theme={null}
const CROSSMINT_CLIENT_API_KEY = "YOUR_CROSSMINT_CLIENT_API_KEY";
const jwt = "YOUR_USER_JWT";
const orderIntentId = "a1b2c3d4-e5f6-4890-abcd-ef1234567890";
const response = await fetch(
`https://staging.crossmint.com/api/unstable/order-intents/${orderIntentId}/credentials`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": CROSSMINT_CLIENT_API_KEY,
Authorization: `Bearer ${jwt}`,
},
body: JSON.stringify({
rail: "encrypted-card",
amount: { value: "25.00", currency: "USD" }, // the order intent's currency
credential: {
format: "card",
publicKey: { kty, n, e },
},
}),
}
);
if (!response.ok) {
throw new Error(`Credential creation failed (${response.status})`);
}
const result = await response.json();
```
The response contains the encrypted card as a compact JWE string, with the same `id`, `amount`, and `expiresAt` fields as a card-network credential. The `id` is issued by Crossmint. `expiresAt` is the earlier of two deadlines: the end of the window in which the vault still holds the card's security code, and the order intent's own `expiresAt`. After it passes, mint again for what you still need; the amount already minted does not come back. If the security-code window is what ended, the rail reads `pending_cvc_recollection` and the user re-enters the CVC first; if the order intent itself expired, create a new one:
```json theme={null}
{
"id": "6f1c2a8e-3b7d-4c1e-9a2f-0d5e7b9c4a11",
"rail": "encrypted-card",
"amount": { "value": "25.00", "currency": "USD" },
"credential": {
"format": "card",
"value": "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIiwidHlwIjoiSldFIn0..."
},
"expiresAt": "2026-09-23T16:20:00.000Z"
}
```
### Decrypt the Card
The JWE uses `RSA-OAEP-256` for key encryption and `A256GCM` for content encryption. Decrypt it with your private key using a JOSE library. The plaintext is a JSON object with the same fields as a card-network credential:
```typescript theme={null}
import { compactDecrypt } from "jose";
const { plaintext } = await compactDecrypt(result.credential.value, privateKey);
const card = JSON.parse(new TextDecoder().decode(plaintext));
// { number: "4242424242424242", expirationMonth: 12, expirationYear: 2030, cvc: "123" }
```
The decrypted value is the user's real card, not a one-time number. Treat it with the same care as raw cardholder data: decrypt it only where the payment is made, never log or persist it, and discard the private key when the purchase completes. Minting consumes the requested `amount` from the order intent even if the card is never charged, and the order intent refuses a mint above `amount.available`. The card network does not enforce the allowance, so your agent must keep each purchase within the `amount` it minted and the order intent's `merchant`.
### Encrypted Card Errors
| Error code | Meaning |
| - | - |
| `ORDER_INTENT_INVALID_PUBLIC_KEY` | The vault rejected `publicKey`, for example because `n` and `e` do not form a usable RSA key |
| `ADDON_REQUIRED` | Encrypted card access is not enabled for the project in production; contact us for access |
| `ORDER_INTENT_CVC_RECOLLECTION_REQUIRED` | The card security code (CVC) has expired or is no longer available for this card. The `encrypted-card` rail shows `status: "pending_cvc_recollection"`. Ask the user to re-enter the CVC with [Recollect the CVC](/agents/payment-methods/cards/recollect-cvc), then retry the request |
| `ORDER_INTENT_CREDENTIAL_UNAVAILABLE` | The vault rejected the saved card, or the request does not fit the order intent: a different currency, no merchant on an order intent created without one, or a merchant other than the one the order intent was created with. The `message` says which. A request may name only part of the merchant; every field it names must match |
| `ORDER_INTENT_AMOUNT_EXCEEDED` | The requested `amount` is above `amount.available` |
| `ORDER_INTENT_CREDENTIAL_ALREADY_ISSUED` | `409`. A credential was already issued for this `Idempotency-Key`. Credential payloads are returned once; use a new key for a new mint |
| `ORDER_INTENT_EXPIRED` | The order intent has passed its `expiresAt` |
| `ORDER_INTENT_CANCELLED` | The order intent was cancelled |
To make a retry safe, send an `Idempotency-Key` header (1 to 255 characters) with the request. A second call with the same key never consumes the amount again: while the first is unanswered it is refused, and after a successful mint it returns `ORDER_INTENT_CREDENTIAL_ALREADY_ISSUED`. Without the header every call is a new mint.
A malformed request, such as a `publicKey` whose `kty` is not `RSA` or whose encoded `n` is shorter than 342 base64url characters, fails schema validation before the vault is called and returns a `400` without an order-intent error code. Other unusable RSA keys can reach the vault and return `ORDER_INTENT_INVALID_PUBLIC_KEY`.
## List Order Intents
List every order intent owned by the authenticated user:
```typescript theme={null}
const CROSSMINT_CLIENT_API_KEY = "YOUR_CROSSMINT_CLIENT_API_KEY";
const jwt = "YOUR_USER_JWT";
const response = await fetch("https://staging.crossmint.com/api/unstable/order-intents", {
headers: {
"X-API-KEY": CROSSMINT_CLIENT_API_KEY,
Authorization: `Bearer ${jwt}`,
},
});
if (!response.ok) {
throw new Error(`Order intent listing failed (${response.status})`);
}
const orderIntents = await response.json();
```
Each item includes its live balance and rails. Use `GET /api/unstable/order-intents/{orderIntentId}` when you only need to refresh one order intent.
## Common Gotchas
Use the same currency as the order intent and keep the requested value at or below `amount.available`.
Do not retry a credential request blindly. Each successful mint is a new credential and consumes the requested amount.
Complete allowance verification for the rail you selected and fetch the order intent again before minting. You do not need to verify unrelated rails.
If the order intent was created without `merchant`, include one in every credential request. If the merchant was set at creation, omit it when minting, or repeat it exactly: a different merchant is refused. This applies to every rail, `encrypted-card` included.
With `rail: "encrypted-card"`, send `amount`, an optional `merchant`, and `credential.format` with `credential.publicKey`. Do not send `provider`. Requests with unknown fields are rejected.
## Next Steps
Cancel an order intent or delete a saved card
Run the complete flow in the reference app
# Save a Card
Source: https://docs.crossmint.com/agents/payment-methods/cards/save-card
Save a user's card with Crossmint so you can register it for agentic payment rails
## Introduction
The user enters their card into a Crossmint-hosted UI, and your app receives a `paymentMethodId` for subsequent API calls. Raw card data never reaches your servers.
Saving the card does not grant spending permission. You register the saved card to discover its supported rails, then create and verify a separate order intent before minting credentials.
## Prerequisites
* **Crossmint API key** — a client-side key with the `payment-methods.create`, `payment-methods.read`, and `payment-methods.update` scopes. The `update` scope lets the user re-enter the card security code (CVC) later, when a card credential needs it. In staging, all scopes are included by default.
* **User JWT** — authenticate the user with [Crossmint Auth](/authentication/introduction) or a configured external provider.
**Saved card limit.** In production, each project has a limit on the number of cards it can save, unless the Crossmint team has raised or removed it for the project. The limit counts the saved cards of all the project's users; deleted cards do not count. Staging has no limit. The limit applies to cards only: bank accounts have no limit, but in production saving them must be enabled for your project.
At the limit, saving another card returns `400` with the code `payments:payment-method.card-limit-reached`, and the project's limit in `parameters.limit`. Cards that are already saved keep working, so agents can still spend with them. To save a new card, [remove a card](/agents/payment-methods/cards/remove-cards) that a user no longer needs. To raise the limit, contact the Crossmint team.
## Steps
```bash npm theme={null}
npm install @crossmint/client-sdk-react-ui
```
```bash yarn theme={null}
yarn add @crossmint/client-sdk-react-ui
```
```bash pnpm theme={null}
pnpm add @crossmint/client-sdk-react-ui
```
```bash bun theme={null}
bun add @crossmint/client-sdk-react-ui
```
Set up `CrossmintProvider` at the root of your app. This makes the Crossmint SDK available to all child components.
```tsx theme={null}
"use client";
import { CrossmintProvider } from "@crossmint/client-sdk-react-ui";
function App() {
return (
{/* Your app */}
);
}
```
Place this component on a page where the user manages their payment methods, or in an ephemeral UI dedicated to saving cards.
The `CrossmintPaymentMethodManagement` component handles all PCI compliance concerns. Card data is collected directly by Crossmint and never passes through your servers — this is why the UI must be rendered inside your app rather than called from your backend.
In staging, use the test card number **4242 4242 4242 4242** with any future expiration date and any three-digit CVC. For additional scenarios, see [Test Agent Cards](/agents/payment-methods/cards/test-agent-cards).
```tsx theme={null}
import { CrossmintPaymentMethodManagement } from "@crossmint/client-sdk-react-ui";
function PaymentMethodsPage({ jwt }: { jwt: string }) {
// jwt comes from your own auth provider (for example Stytch, Auth0, Dynamic)
const handlePaymentMethodSelected = (paymentMethod: { paymentMethodId: string }) => {
// Persist this ID so you can register the card and create order intents.
console.log("Card saved:", paymentMethod.paymentMethodId);
};
return (
Payment Methods
Save a card so your agent can request payments on your behalf.
);
}
```
When the user saves a card, the `onPaymentMethodSelected` callback returns a `paymentMethodId`. Persist this ID in your backend associated with the user, apply strict access controls, and avoid exposing it in client-side storage or logs.
The ID cannot reveal the original card number, but it can be used to register the card and create order intents.
## Next Steps
Continue to [Register a Card](/agents/payment-methods/cards/register-card) to discover which order-intent rails support the saved card.
# Test Agent Cards
Source: https://docs.crossmint.com/agents/payment-methods/cards/test-agent-cards
Use staging test cards to exercise card registration, verification, and credential minting
In staging, every card-network rail routes to a deterministic mock of Visa Intelligent Commerce and Mastercard Agent Pay. No request reaches a real card network, but the endpoints, response shapes, statuses, and error codes match production. The card number you save selects the scenario, so you can test success and failure paths without a real card.
## Prerequisites
* **Staging API key** — a client-side key from the Crossmint Staging Console. In staging, all scopes are included by default.
* **Card flow** — the integration from [Save a Card](/agents/payment-methods/cards/save-card), [Register a Card](/agents/payment-methods/cards/register-card), [Create an Agent Card](/agents/payment-methods/cards/create-agent-card), and [Retrieve Secure Card Numbers](/agents/payment-methods/cards/retrieve-agent-card), pointed at `https://staging.crossmint.com`.
## Test Cards
Save one of these numbers with any future expiration date and any three-digit CVC. Any Visa or Mastercard number not listed follows its brand's happy path.
| Card | Brand | Outcome |
| - | - | - |
| `4242 4242 4242 4242` | Visa | Succeeds end to end |
| `5555 5555 5555 4444` | Mastercard | Succeeds end to end |
| `5186 1600 0000 0001` | Mastercard | Fails at registration with `CARD_REJECTED` |
| `4929 9803 9556 7582` | Visa | Fails at verification: the one-time code is rejected with `INVALID_OTP` |
| `5186 1600 0000 0003` | Mastercard | Fails at verification: the hosted page appears to succeed, but the rail never becomes `active` |
## What a Successful Card Returns
[Register a Card](/agents/payment-methods/cards/register-card) reports one card-network rail for the card. The `rail` is always `agentic-token`; the `provider` names the network program behind it, `vic` for Visa Intelligent Commerce or `agentpay` for Mastercard Agent Pay:
```json theme={null}
{
"paymentMethodId": "pm_123",
"rails": [
{
"rail": "agentic-token",
"provider": "agentpay",
"status": "enabled"
}
]
}
```
An order intent created for that card ([Create an Agent Card](/agents/payment-methods/cards/create-agent-card)) carries the same rail with `status: "pending_verification"`. After the cardholder approves the allowance in `OrderIntentVerification`, fetch the order intent again: the rail reports `status: "active"` and you can mint.
```json theme={null}
{
"rail": "agentic-token",
"provider": "agentpay",
"status": "active",
"credentialFormats": ["card", "network-token"]
}
```
## Failure Scenarios
The three cards below are designed to fail. The error each one produces is the expected result of the scenario, not
a problem with your integration.
### Card Rejected at Registration
Save `5186 1600 0000 0001`. The registration request succeeds, but the Mastercard rail is reported in `error`:
```json theme={null}
{
"paymentMethodId": "pm_123",
"rails": [
{
"rail": "agentic-token",
"provider": "agentpay",
"status": "error",
"error": { "code": "CARD_REJECTED" }
}
]
}
```
The card is saved and the rejection is permanent. Confirm that your UI treats this as a saved card with one failed rail, not as a failed save. Order intents for this card include an `encrypted-card` rail, which is enabled for every staging project, so mint through it instead. See [Mint an Encrypted Card](/agents/payment-methods/cards/retrieve-agent-card#mint-an-encrypted-card).
### One-Time Code Rejected
Save `4929 9803 9556 7582`. Registration succeeds with a `vic` rail `enabled`, and the order intent's rail is `pending_verification`. In `OrderIntentVerification`, the Visa flow asks for a one-time code and rejects every code with `INVALID_OTP`. The user can enter another code, but this card never accepts one, so verification does not complete and the rail stays `pending_verification`:
```json theme={null}
{
"rail": "agentic-token",
"provider": "vic",
"status": "pending_verification",
"credentialFormats": ["card", "network-token"]
}
```
Confirm that your UI lets the user retry or abandon the verification, and that nothing mints while the rail is `pending_verification`.
### Hosted Page Returns but Verification Fails
Save `5186 1600 0000 0003`. Registration succeeds with an `agentpay` rail `enabled`. During verification, the mock Mastercard-hosted page shows a success message and returns to your application, but when Crossmint confirms the result with the network, the confirmation fails. `OrderIntentVerification` calls `onVerificationError`, and the rail stays `pending_verification`.
Confirm that your integration waits for `onVerificationComplete` and then fetches the order intent until the rail reports `active`, rather than minting as soon as the hosted page closes.
## Test Verification
Verification is the step where the cardholder approves an order intent's allowance through the `OrderIntentVerification` component. The mocks run the same browser flow as production, so popups, message listeners, and origin checks are all exercised.
* **Visa** — the verification asks the user to choose a one-time code method (a masked phone number or email address) and accepts any code, unless the card scenario says otherwise. On a device without a passkey, a mock Visa screen enrolls one. Later order intents on the same device go straight to passkey authentication, as in production.
* **Mastercard** — the component opens a mock Mastercard-hosted page that shows its progress and returns to your application. The rail becomes `active` only after Crossmint confirms the result, so fetch the order intent again before minting.
Use an HTTPS tunnel when you test verification from a local browser. It exercises the same iframe, origin, and popup behavior as your deployed site.
## Check the Minted Credentials
Mock credentials are deterministic, so repeated runs return stable values. Use them in integration tests only; they are not accepted by real merchants.
| Format | Value |
| - | - |
| `card` | A virtual card number: `4000 0010 0000 4242` for the `4242` Visa card and `5100 0010 0000 4446` for the `4444` Mastercard. Other cards return `4000 0010 0000 0000` (Visa) or `5100 0010 0000 0006` (Mastercard). The expiration date matches the saved card, and the CVC is derived deterministically |
| `network-token` | The same virtual number with a deterministic cryptogram. Visa returns ECI `07` and Mastercard returns ECI `06` |
Credential expiry follows each network's rule rather than a uniform test value. The Visa mock returns a short synthetic cryptogram window, so do not calibrate how long a credential stays usable against it. Use every credential immediately.
## Test Common Scenarios
The allowance accounting in staging is real, so these scenarios behave exactly as in production.
* **Spend limits** — create an order intent for `10.00`, mint a credential for `7.00`, then read the order intent: `amount.available` is `3.00`. Minting `4.00` next fails with `ORDER_INTENT_AMOUNT_EXCEEDED`.
* **Encrypted card fallback** — save `5186 1600 0000 0001` and mint through the `encrypted-card` rail, as described in [Card Rejected at Registration](#card-rejected-at-registration).
* **CVC recollection** — expire a saved card's CVC on demand to test the `pending_cvc_recollection` state. See [Test in Staging](/agents/payment-methods/cards/recollect-cvc#test-in-staging).
## Next Steps
Create an order intent and verify its rail
Run the complete flow in the reference app
# Authorize the Agent
Source: https://docs.crossmint.com/agents/payment-methods/stablecoin-wallets/authorize-agent
Grant your agent delegated control over the user's wallet.
## Introduction
To let the agent move funds from the user's wallet, you create a **signer** for the agent and add it to the wallet. A signer is just a key that can authorize transactions — the agent gets its own, separate from the user's.
You add the signer in two parts so the user stays in control:
1. **On your server**, you create the signer and register it on the wallet as *pending*. The signer's secret stays in your backend and never reaches the browser or the agent.
2. **On the client**, the user approves the new signer with a one-time email code. Only after this step can the agent sign anything.
Once approved, the agent can transact within the limits you set — and the user can revoke it at any time.
## Prerequisites
* A user wallet. See [Server signer setup](/wallets/guides/signers/server-signer#signer-secret-generator) if you have not done this yet.
* A Crossmint **server-side** API key from the Crossmint Console.
* The user must be authenticated and signed in to your app.
## Steps
The server signer is backed by a 32-byte secret. Generate one with [the signer secret generator](/agents/stablecoin-wallet-quickstart#setup) and store it only as a backend environment variable.
```bash theme={null}
CROSSMINT_SERVER_SIDE_API_KEY=sk_staging_...
CROSSMINT_SIGNER_SECRET=your-64-char-hex-secret
```
Anyone who holds `CROSSMINT_SIGNER_SECRET` can sign on behalf of every wallet it has been authorized on. Never commit it, never expose it to the client, and rotate it if you suspect leakage.
Use a server action (or any server-only handler) to call `addSigner` in `prepareOnly` mode. The secret is read from the environment and never sent over the wire.
```typescript theme={null}
"use server";
import { createCrossmint, CrossmintWallets } from "@crossmint/wallets-sdk";
export async function prepareServerSigner({ walletAddress }: { walletAddress: string }) {
const crossmint = createCrossmint({
apiKey: process.env.CROSSMINT_SERVER_SIDE_API_KEY,
});
const wallets = CrossmintWallets.from(crossmint);
const wallet = await wallets.getWallet(walletAddress, { chain: "base-sepolia" });
const { locator, signatureId } = await wallet.addSigner(
{ type: "server", secret: process.env.CROSSMINT_SIGNER_SECRET },
{ prepareOnly: true }
);
return { locator, signatureId };
}
```
`prepareOnly: true` is what makes this safe. Without it, the SDK would auto-approve using whatever signer is active, which on the server is the server signer itself — the user would never see a prompt.
On the client, call the server action and then ask the SDK to approve the pending signature with the user's recovery method. This is the step that triggers the email code.
```tsx theme={null}
"use client";
import { useWallet, useCrossmintAuth } from "@crossmint/client-sdk-react-ui";
import { prepareServerSigner } from "@/app/actions/add-server-signer";
export function AuthorizeAgent() {
const { wallet } = useWallet();
const { user } = useCrossmintAuth();
const handleAuthorize = async () => {
if (!wallet || !user?.email) return;
const { signatureId } = await prepareServerSigner({ walletAddress: wallet.address });
await wallet.useSigner({ type: "email", email: user.email });
await wallet.approve({ signatureId });
};
return ;
}
```
Once the user completes the verification, the server signer becomes an authorized signer on the wallet. Verify by calling `wallet.signers()` — you should see an entry with `type: "server"`.
## Revoking the Agent
Once authorized, the agent can be revoked at any time. See [Remove Agent Access](/agents/payment-methods/stablecoin-wallets/remove-agent-access).
## Common Gotchas
If you omit it, the server signer signs its own authorization and the user never approves. The pending signature step is what gives the user veto power.
Anyone holding the secret can sign for every wallet it has been authorized on. Treat it like a database password — environment variables only, never the client bundle.
## Next Steps
Add stablecoin balance to the user's wallet so the authorized agent can start spending.
Full reference for the server signer, secret rotation, and multi-tenant patterns.
# Create a User Wallet
Source: https://docs.crossmint.com/agents/payment-methods/stablecoin-wallets/create-user-wallet
Create a non-custodial wallet owned by your end user, with email or passkey recovery.
## Introduction
The user wallet is the root of every agent stablecoin flow. It is **non-custodial** and owned by the end user, with an **email** (or passkey) recovery method that stays in the user's control. Your agent is later authorized to use the wallet as a [signer](/agents/payment-methods/stablecoin-wallets/authorize-agent) with explicit spending limits, and that access can be revoked at any time.
Subsequent logins from the same user return the same wallet — creation is idempotent.
## Prerequisites
* A staging Crossmint client API key from the Crossmint Console. In staging, all scopes are enabled by default.
* The user must be authenticated. If you have not set up authentication, see the [Agents overview](/agents/overview) first.
## Steps
```bash npm theme={null}
npm install @crossmint/client-sdk-react-ui
```
```bash yarn theme={null}
yarn add @crossmint/client-sdk-react-ui
```
```bash pnpm theme={null}
pnpm add @crossmint/client-sdk-react-ui
```
```bash bun theme={null}
bun add @crossmint/client-sdk-react-ui
```
Set up `CrossmintProvider`, `CrossmintAuthProvider`, and `CrossmintWalletProvider` at the root of your app. Pass `createOnLogin` to `CrossmintWalletProvider` so the SDK creates the user's wallet automatically as soon as they sign in, with an email-recovery.
```tsx theme={null}
"use client";
import {
CrossmintAuthProvider,
CrossmintProvider,
CrossmintWalletProvider,
} from "@crossmint/client-sdk-react-ui";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Use `useWallet()` anywhere below the provider to access the wallet. The hook exposes a `status` field you can use to render a loading state during creation.
```tsx theme={null}
"use client";
import { useWallet } from "@crossmint/client-sdk-react-ui";
export function WalletSection() {
const { wallet, status } = useWallet();
if (status === "in-progress" || status === "not-loaded") {
return
Creating wallet…
;
}
return {wallet?.address};
}
```
When `status` is `loaded`, `wallet.address` is the user's wallet address and the wallet is ready for delegation.
## Manual Creation (Non-React or Server-Side)
If you are not using the React SDK — for example, creating the wallet server-side from a custom auth backend — use the wallets SDK directly:
```typescript theme={null}
import { createCrossmint, CrossmintWallets } from "@crossmint/wallets-sdk";
const crossmint = createCrossmint({
apiKey: process.env.CROSSMINT_SERVER_SIDE_API_KEY,
});
const wallets = CrossmintWallets.from(crossmint);
const wallet = await wallets.createWallet({
chain: "base-sepolia",
owner: `userId:${userId}`,
signer: { type: "email", email: userEmail },
});
```
`owner` scopes the wallet to your authenticated user. Pass the same identifier on subsequent `getWallet` calls to resolve it.
## Common Gotchas
Wallet creation is idempotent — repeated calls return the same wallet. You do not need to guard against double-creation in your UI.
The current API accepts one recovery method when the wallet is created. Add the agent as a scoped signer so you can revoke its access without changing the wallet address.
On the React SDK, the user is identified by the auth provider's session. On the server, you must explicitly scope ownership with `owner: userId:` so the wallet resolves correctly on later `getWallet` calls.
## What Is Next
With the user's wallet created, the next step is to add your agent as a signer so it can sign on the user's behalf.
Continue to [Authorize the Agent](/agents/payment-methods/stablecoin-wallets/authorize-agent).
For a full app you can clone and test end-to-end, see the [Stablecoin Wallet Quickstart](/agents/stablecoin-wallet-quickstart).
# Using the Wallet
Source: https://docs.crossmint.com/agents/payment-methods/stablecoin-wallets/on-chain-actions
Use the wallet to send stablecoins, swap, bridge, and call any contract from your agent.
## Introduction
Once a signer is authorized, your agent can drive the wallet like any EOA: **send stablecoins, swap, bridge, or call an arbitrary contract**. All operations go through the same `wallet` handle — the only thing that changes is which signer is active.
Policies attached to the server signer (spend caps, allowed contracts) are enforced on every transaction, so the agent can only sign what the user delegated.
## Prerequisites
* A user wallet with an authorized server signer. See [Authorize the Agent](/agents/payment-methods/stablecoin-wallets/authorize-agent) if you have not done this yet.
* The `walletAddress` of the user's wallet.
* A Crossmint **server-side** API key and the `CROSSMINT_SIGNER_SECRET` for the authorized signer.
## Activate the Server Signer
Before any server-side signing, activate the authorized server signer. Every subsequent call on that `wallet` handle is signed in-process with no user prompt.
```typescript theme={null}
import { createCrossmint, CrossmintWallets } from "@crossmint/wallets-sdk";
const crossmint = createCrossmint({
apiKey: process.env.CROSSMINT_SERVER_SIDE_API_KEY,
});
const wallets = CrossmintWallets.from(crossmint);
const wallet = await wallets.getWallet(walletAddress, { chain: "base-sepolia" });
await wallet.useSigner({
type: "server",
secret: process.env.CROSSMINT_SIGNER_SECRET,
});
```
## Send Stablecoins
The canonical agent action: transfer stablecoins to a recipient. Wrap it in a server action so the secret stays on the backend.
```typescript theme={null}
"use server";
import { createCrossmint, CrossmintWallets } from "@crossmint/wallets-sdk";
export async function sendUsdxmFromServer({
walletAddress,
recipient,
amount,
}: {
walletAddress: string;
recipient: string;
amount: string;
}) {
const crossmint = createCrossmint({
apiKey: process.env.CROSSMINT_SERVER_SIDE_API_KEY,
});
const wallets = CrossmintWallets.from(crossmint);
const wallet = await wallets.getWallet(walletAddress, { chain: "base-sepolia" });
await wallet.useSigner({
type: "server",
secret: process.env.CROSSMINT_SIGNER_SECRET,
});
const tx = await wallet.send(recipient, "usdxm", amount);
return { hash: tx.hash, explorerLink: tx.explorerLink };
}
```
`wallet.send(recipient, symbol, amount)` accepts any token symbol the chain supports (`usdc`, `usdt`, `usdxm` on staging, native gas tokens). The transaction is sponsored by default — the user does not need to hold ETH for gas.
See [Transfer Tokens](/wallets/guides/transfer-tokens) for the full symbol list and per-chain behavior.
## Read Balances
```typescript theme={null}
const balances = await wallet.balances(["usdxm", "eth"]);
const usdxm = balances.tokens.find((t) => t.symbol === "usdxm");
```
`balances()` returns native balance and token balances in one call. Pass an explicit array to include non-default symbols. For richer patterns (historical, multi-chain aggregation), see [Check Balances](/wallets/guides/check-balances).
## Fund a Staging Wallet
On supported staging chains, you can mint test stablecoins directly into the wallet — no faucet round-trip.
```typescript theme={null}
await wallet.stagingFund(5); // 5 USDXM
```
This is staging-only and exists to unblock local development. For production funding flows, see [Onramp & Add Funds](/agents/payment-methods/stablecoin-wallets/onramp-add-funds). For EVM-native testnet tokens, see [Fund a Staging Wallet](/wallets/guides/fund-staging-wallet).
## Call Any Contract
The wallet exposes a generic transaction path for anything that is not a first-class helper — swaps, bridges, DeFi, NFT mints, custom protocols. The exact call shape differs per chain; follow the reference for the chain you are targeting:
| Chain | Reference |
| - | - |
| EVM | [Custom EVM Transactions](/wallets/guides/send-transaction-evm) |
| Solana | [Custom Solana Transactions](/wallets/guides/send-transaction-solana) |
| Stellar | [Custom Stellar Transactions](/wallets/guides/send-transaction-stellar) |
## Common Gotchas
Activating a signer applies only to the `wallet` handle you call it on. If you load a fresh wallet via `getWallet`, you must call `useSigner` again before signing.
Gas sponsorship is enabled by default on supported chains, but check the chain matrix in [Transfer Tokens](/wallets/guides/transfer-tokens) before assuming the user does not need a gas balance.
## Next Steps
Pay HTTP-402-gated endpoints automatically using the authorized wallet.
Full reference for signers, transactions, webhooks, and error handling.
# Onramp & Add Funds
Source: https://docs.crossmint.com/agents/payment-methods/stablecoin-wallets/onramp-add-funds
Fund an agent wallet for x402, MPP, and onchain transactions.
## Introduction
Once a user wallet is created and the agent is authorized, the next step is to fund it with stablecoins. The agent needs USDC in the wallet to pay x402 endpoints, settle MPP requests, and submit onchain transactions. The onramp flow lets users buy USDC with a credit card and deliver it directly to their Crossmint wallet.
Onramp is a **UI flow, not a headless API**. Card authorization, 3DS challenges, and KYC checks all require the user to interact with a Crossmint-hosted component, the same way Apple Pay or Stripe Elements work. Raw card data and PII never reach your servers.
In this guide, you will learn how to:
* Create a Crossmint order using a server action
* Use Crossmint's embedded checkout component to handle KYC, payment, and delivery automatically
## Prerequisites
Install the Crossmint client SDK:
```bash npm theme={null}
npm install @crossmint/client-sdk-react-ui
```
```bash yarn theme={null}
yarn add @crossmint/client-sdk-react-ui
```
```bash pnpm theme={null}
pnpm add @crossmint/client-sdk-react-ui
```
```bash bun theme={null}
bun add @crossmint/client-sdk-react-ui
```
Create a [server-side API key](/introduction/platform/api-keys/server-side) with the `orders.create` and `orders.read` scopes enabled.
Create a [client-side API key](/introduction/platform/api-keys/client-side) for the embedded checkout component.
In staging, all scopes are enabled by default.
Make sure the user has a Crossmint wallet. See [Create a User Wallet](/agents/payment-methods/stablecoin-wallets/create-user-wallet) if you have not done this yet.
Add environment variables to your `.env.local`:
```bash .env.local theme={null}
NEXT_PUBLIC_CROSSMINT_CLIENT_SIDE_API_KEY="_YOUR_CLIENT_API_KEY_"
CROSSMINT_SERVER_SIDE_API_KEY="_YOUR_SERVER_API_KEY_"
```
## Steps
Create a server action that creates onramp orders via the Crossmint API. The recipient is the user's wallet address — funds are delivered straight to the wallet your agent is authorized on.
```typescript app/actions/createOrder.ts theme={null}
"use server";
const USDC_TOKEN_LOCATORS = {
solanaDevnet: "solana:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
baseSepolia: "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
stellarTestnet: "stellar:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
};
interface CreateOrderParams {
walletAddress: string;
receiptEmail: string;
amount: string;
chain: "solanaDevnet" | "baseSepolia" | "stellarTestnet";
}
export async function createOrder({ walletAddress, receiptEmail, amount, chain }: CreateOrderParams) {
const serverApiKey = process.env.CROSSMINT_SERVER_SIDE_API_KEY;
if (serverApiKey == null) {
throw new Error("CROSSMINT_SERVER_SIDE_API_KEY is not set");
}
const response = await fetch("https://staging.crossmint.com/api/2022-06-09/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": serverApiKey,
},
body: JSON.stringify({
lineItems: [
{
tokenLocator: USDC_TOKEN_LOCATORS[chain],
executionParameters: {
mode: "exact-in",
amount,
},
},
],
payment: {
method: "card",
receiptEmail,
},
recipient: {
walletAddress,
},
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Failed to create order");
}
return response.json();
}
```
Create a client component that pulls the user's wallet from `useWallet()`, creates an order, and renders the embedded checkout. Because the agent is already authorized on the wallet, any USDC delivered here is immediately spendable by the agent on x402 endpoints, MPP requests, and onchain transactions.
```tsx app/components/OnrampCheckout.tsx theme={null}
"use client";
import { useState } from "react";
import {
CrossmintEmbeddedCheckout,
useCrossmintAuth,
useWallet,
} from "@crossmint/client-sdk-react-ui";
import { createOrder } from "../actions/createOrder";
export default function OnrampCheckout() {
const { wallet } = useWallet();
const { user } = useCrossmintAuth();
const [order, setOrder] = useState<{ orderId: string; clientSecret: string } | null>(null);
const [amount, setAmount] = useState("5");
const [isLoading, setIsLoading] = useState(false);
async function handleSubmit() {
if (!wallet || !user?.email) return;
setIsLoading(true);
try {
const result = await createOrder({
walletAddress: wallet.address,
receiptEmail: user.email,
amount,
chain: "baseSepolia",
});
setOrder({ orderId: result.order.orderId, clientSecret: result.clientSecret });
} catch (error) {
console.error("Failed to create order:", error);
} finally {
setIsLoading(false);
}
}
if (order != null) {
return (
);
}
```
The embedded checkout already runs inside the `CrossmintProvider` you set up to create the user's wallet, so no extra provider is needed here.
Drop `OnrampCheckout` into the page where the user funds their wallet — for example, right after authorizing the agent.
```tsx app/page.tsx theme={null}
import OnrampCheckout from "./components/OnrampCheckout";
export default function FundPage() {
return (
);
}
```
When the user clicks **Continue to Checkout**, the server action creates an order and the embedded checkout component handles KYC, payment, and delivery automatically. As soon as USDC lands in the wallet, the agent can spend it on x402 endpoints, MPP requests, and onchain transactions.
**Testing:** Use the test credit card number `4242 4242 4242 4242` with any future expiration date and any 3-digit CVC.
## Common Gotchas
The recipient of the order is always the user's wallet address. The agent does not have its own balance — it spends from the user's wallet using its delegated server signer to pay x402 endpoints, MPP requests, and onchain transactions.
The token locators above target Base Sepolia, Solana Devnet, and Stellar Testnet. Switch to mainnet locators only after end-to-end staging tests pass.
Card collection, 3DS, and KYC must be completed by the user in the embedded checkout. An agent cannot complete onramp on behalf of the user.
## What Is Next
Once funded, send stablecoins, call contracts, and handle x402 or MPP payments from your agent.
Set up the card that will fund the wallet.
# Remove Agent Access
Source: https://docs.crossmint.com/agents/payment-methods/stablecoin-wallets/remove-agent-access
Revoke an agent signer from a user's wallet.
## Introduction
This page covers how to remove an agent's access to a user's wallet by revoking its server signer. The user authorizes the removal with their recovery method, and the signer is immediately removed from the wallet's signer set.
## Prerequisites
* A user wallet with at least one server signer authorized. See [Authorize the Agent](/agents/payment-methods/stablecoin-wallets/authorize-agent) if you have not done this yet.
* The `locator` returned by `wallet.addSigner(...)` (or the signer address — both work as locators).
* The user's recovery method (for example, their email signer) — revocation must be authorized by the user, not by the agent itself.
## Revoke an agent signer
Use this when the user wants to cut off a specific agent from a specific wallet. The user authorizes removal with their recovery method (one-time email code), and the server signer is immediately removed from the wallet's signer set. Any in-flight signature requests against that signer fail.
```typescript theme={null}
"use client";
import { useWallet, useCrossmintAuth } from "@crossmint/client-sdk-react-ui";
export function RevokeAgent({ locator }: { locator: string }) {
const { wallet } = useWallet();
const { user } = useCrossmintAuth();
const handleRevoke = async () => {
if (!wallet || !user?.email) return;
await wallet.useSigner({ type: "email", email: user.email });
await wallet.removeSigner({ locator });
};
return ;
}
```
Revocation is per-wallet. If the same server secret has been authorized on multiple wallets — for example because you use a single backend signer for many users — removing it from one wallet does **not** affect the others.
## Common Gotchas
Calls to `wallet.removeSigner` must be made with the user's recovery method active (e.g. `wallet.useSigner({ type: "email", email: user.email })`). If the server signer is active when you call remove, the request fails — by design, an agent cannot revoke itself.
The same secret produces a different `locator` on each wallet it is authorized on. Persist the `locator` returned by `addSigner` against the wallet address so revocation can target the right entry.
## Next Steps
Re-authorize an agent on the wallet after revocation.
Full reference for the server signer.
# Server Agent Wallets
Source: https://docs.crossmint.com/agents/payment-methods/stablecoin-wallets/server-agent-wallets
Backend-only wallets for autonomous agents with no end user.
## Introduction
A **server agent wallet** is a wallet whose root signer is your backend, not an end user. Use it when there is no human in the loop — autonomous market makers, scheduled task runners, treasury bots, or any agent that operates entirely on its own keys.
This is the opposite end of the trust spectrum from the user-authorized flow. The server signer is the wallet's recovery method, so there is no user-controlled recovery method to delegate to and no user OTP step. Your backend creates the wallet, signs every transaction, and is fully responsible for the secret.
## Prerequisites
* A Crossmint **server-side** API key from the Crossmint Console.
* A secure secret store for the signer key (environment variable, KMS, or hardware-backed vault). The user-authorized flow has the user as a fallback if the server is compromised — this flow does not.
This page is under construction. Full content — including signer key management patterns, multi-tenant isolation, and the canonical [server signer reference](/wallets/guides/signers/server-signer) — is coming soon.
## What Is Next
Full reference for the server signer, secret rotation, and key management.
Once the wallet is set up, drive it the same way as a user-authorized wallet.
# Agent Wallets Quickstart ⚡
Source: https://docs.crossmint.com/agents/stablecoin-wallet-quickstart
Get your agent paying with stablecoins in 10 minutes.
Create a user-owned smart wallet, authorize an agent as a delegated signer, and send USDXM on Base Sepolia. This quickstart runs the [stablecoin wallet reference app](https://github.com/Crossmint/stablecoin-wallet-quickstart).
By the end you'll have a running app where a user can sign in, get a non-custodial wallet, authorize an agent with a one-time email code, and watch the agent send a stablecoin transfer signed entirely on your backend — no user prompt per transaction.
See the full flow in action without setting anything up locally.
Explore the full reference implementation for user wallets, server signers, and USDXM transfers.
## Prerequisites
* Node.js 20+ and pnpm
## Setup
You can ask your agent to walk you through the setup to run this quickstart.
```bash theme={null}
git clone https://github.com/Crossmint/stablecoin-wallet-quickstart.git
cd stablecoin-wallet-quickstart
pnpm install
```
Copy the example file. You'll fill in the values in the next steps.
```bash theme={null}
cp .env.example .env.local
```
```bash .env.local theme={null}
NEXT_PUBLIC_CROSSMINT_CLIENT_API_KEY=your-crossmint-client-api-key
CROSSMINT_SERVER_SIDE_API_KEY=your-crossmint-server-api-key
CROSSMINT_SIGNER_SECRET=your-64-char-hex-secret
```
Sign in to the Crossmint Staging Console and create a project. Then in **Project Settings → API keys**, create:
1. A **client-side** key → paste into `NEXT_PUBLIC_CROSSMINT_CLIENT_API_KEY`.
2. A **server-side** key → paste into `CROSSMINT_SERVER_SIDE_API_KEY`.
Staging keys come with **all scopes enabled by default**, so you don't need to configure anything else for the quickstart.
The agent's server signer uses a 32-byte secret that never leaves your backend. Generate one and paste it into `CROSSMINT_SIGNER_SECRET`. Both raw 64-char hex and the prefixed `xmsk1_<64-hex>` format are accepted.
Anyone who holds this secret can sign on behalf of the wallets it has been authorized on. Store it only as a server-side environment variable; never commit it or ship it to the browser.
```bash theme={null}
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000).
## Understanding the user flow
Once running, the app walks the user through four steps on Base Sepolia:
1. **Authenticate.** Sign in with email OTP or Google via `CrossmintAuthProvider`. Crossmint issues a user JWT that scopes every subsequent SDK call to the signed-in user.
2. **Get a wallet.** `CrossmintWalletProvider` is configured with `createOnLogin={{ chain: "base-sepolia", recoveryMethods: [{ type: "email" }] }}`, so a non-custodial smart wallet is created on first login with the user's email as the recovery method. The address appears as soon as `useWallet()` resolves.
3. **Authorize the agent.** Clicking **Authorize your agent** calls `wallet.addSigner({ type: "server", secret }, { prepareOnly: true })` from your backend, then the user approves the pending registration with a one-time email code. From here on, the server signer can sign for the wallet without any user prompt.
4. **Fund and send.** `wallet.stagingFund(5)` mints 5 USDXM into the wallet (staging-only). A server action then activates the server signer with `wallet.useSigner({ type: "server", secret })` and calls `wallet.send(recipient, "usdxm", amount)` — signed entirely on your backend. This is how an agent spends from a user's wallet in production. To fund a production wallet with USDC, use [Onramp & Add Funds](/agents/payment-methods/stablecoin-wallets/onramp-add-funds) or a direct transfer.
Toggle **Show code** in the header to view the relevant SDK snippet alongside each step.
## Next Steps
Send stablecoins, swap, bridge, and call any contract from your agent.
HTTP-native micropayments — pay-per-call APIs and machine-to-machine settlement.
Revoke an agent's signer from a user's wallet.
# Agent Wallets
Source: https://docs.crossmint.com/agents/wallets/overview
Give an agent scoped access to a non-custodial stablecoin wallet for x402, MPP, and onchain actions
An Agent Wallet is a non-custodial smart wallet that an agent can spend from within limits the owner sets. For consumer agents, the user owns the wallet and adds the agent as a signer with a spend cap, allowed counterparties, and a time window. For autonomous agents with no end user, your backend owns the wallet directly.
Permissions are enforced onchain and the owner can revoke them at any time. Neither you nor Crossmint takes custody of funds. Agent Wallets are how agents pay other machines: APIs behind x402 and MPP, other agents, and any contract onchain. For the problem this solves, read [How Agents Pay](/agents/how-agents-pay#agent-wallets).
## Key Features
The user, or your backend, holds the root key; nobody else can move funds
Spend cap, allowed counterparties, and time window per agent
A transaction outside the delegation is rejected at the wallet level
Pay x402 and MPP endpoints per call, in stablecoins
Users top up with the Crossmint onramp or a direct USDC transfer
Remove the agent signer at any time; the user keeps the wallet and funds
## When Are Agent Wallets the Best Fit?
* **The agent pays machines, not stores.** APIs behind x402 or MPP and other agents settle in stablecoins, per call, with no merchant account.
* **Payments are small or frequent.** Card fees and chargebacks make micropayments impractical; stablecoins do not have that problem.
* **The agent acts onchain.** Sending stablecoins, swapping, bridging, and calling contracts all run from the same wallet.
## How It Works
The Crossmint SDK creates a non-custodial wallet with email or passkey recovery. The user funds it through the Crossmint onramp or a direct USDC transfer.
The agent's key is added as a signer, bound to a permission set the user approves: spend cap, allowed counterparties, and time window.
The agent signs and submits transactions with its own key, with no user prompt per action. Anything outside the delegated scope is rejected at the wallet level.
Removing the agent signer ends its access immediately. The user keeps the wallet and its funds.
## Two Wallet Models
| Model | Who owns the wallet | Use it when |
| - | - | - |
| [User wallet](/agents/payment-methods/stablecoin-wallets/create-user-wallet) | Your end user | An agent spends on behalf of a person who stays in control |
| [Server agent wallet](/agents/payment-methods/stablecoin-wallets/server-agent-wallets) | Your backend | An autonomous agent operates entirely on its own keys with no human in the loop |
## Get Started
Run the reference app: create a user wallet, authorize an agent, and send a stablecoin transfer from your backend
See the full flow without setting anything up locally
## Guides
A non-custodial wallet owned by your end user
Grant your agent delegated, scoped control
Fund the wallet with a card or a direct transfer
Backend-only wallets for autonomous agents
Send, swap, bridge, and call any contract
Pay x402-protected endpoints per call, in stablecoins
Pay Machine Payment Protocol endpoints from an EVM wallet
Revoke the agent signer from a user's wallet
## FAQs
No. The wallet is non-custodial. The user (or, for server agent wallets, your backend) holds the root signer, and the agent only holds a delegated key bound to the permissions the owner approved.
The spend cap, the addresses or protocols the agent can interact with, and the time window in which the delegation is valid. The limits are enforced onchain, so a transaction outside them is rejected at the wallet level.
Yes, at any time. See [Remove Agent Access](/agents/payment-methods/stablecoin-wallets/remove-agent-access).
Agent Wallets are Crossmint smart wallets. The [Wallets documentation](/wallets/overview) covers chains, signers, recovery, and the full SDK.
# Get Usage
Source: https://docs.crossmint.com/api-reference/admin/get-usage
api-reference/_open-api/AdminAPIs.json GET /v1-alpha1/projects/{projectId}/usage
Get usage data for a project.
**API scope required** `projects:usage.read`
# Cancel Agent Checkout
Source: https://docs.crossmint.com/api-reference/agent-checkouts/cancel-agent-checkout
api-reference/_open-api/Agents.json POST /unstable/agent-checkouts/{id}/cancel
Requests cancellation of a checkout run. The response is an acknowledgement; the run reaches `cancelled` once the agent stops, which Get Agent Checkout reflects. Returns `409` when the run can no longer be cancelled.
**API scope required**: `agent-checkouts.cancel`
# Create Agent Checkout
Source: https://docs.crossmint.com/api-reference/agent-checkouts/create-agent-checkout
api-reference/_open-api/Agents.json POST /unstable/agent-checkouts
Starts a checkout run: the agent opens `startUrl`, works toward `task` within `constraints`, and reports back through the run's messages. A `buyerProfileId` supplies identity and shipping details; a `browserProfileId` reuses a saved merchant login. The response is the run in `queued` status. Poll Get Agent Checkout or stream its messages to follow it.
**API scope required**: `agent-checkouts.create`
# Create Browser Profile
Source: https://docs.crossmint.com/api-reference/agent-checkouts/create-browser-profile
api-reference/_open-api/Agents.json POST /unstable/agent-checkouts/browser-profiles
Create a subject-scoped browser profile: a durable browser identity that lets a user's merchant login be reused across checkout runs instead of signing in on every run.
A user has at most one browser profile, and it accumulates saved state across merchants.
**API scope required**: `agent-checkouts.browser-profiles.create`
# Create Buyer Profile
Source: https://docs.crossmint.com/api-reference/agent-checkouts/create-buyer-profile
api-reference/_open-api/Agents.json POST /unstable/agent-checkouts/buyer-profiles
Create a subject-scoped buyer profile (buyer identity + global shipping address) that Universal Checkout can prefill during a checkout run. Payment data is never stored.
**API scope required**: `agent-checkouts.buyer-profiles.create`
# Delete Browser Profile
Source: https://docs.crossmint.com/api-reference/agent-checkouts/delete-browser-profile
api-reference/_open-api/Agents.json DELETE /unstable/agent-checkouts/browser-profiles/{id}
Log the user out and erase their saved browser state. This is irreversible.
It does not cancel checkouts already running with the profile: those runs continue, and erasure completes once they finish. A `503` means erasure could not be scheduled and the profile is intact — retry.
**API scope required**: `agent-checkouts.browser-profiles.delete`
# Delete Buyer Profile
Source: https://docs.crossmint.com/api-reference/agent-checkouts/delete-buyer-profile
api-reference/_open-api/Agents.json DELETE /unstable/agent-checkouts/buyer-profiles/{id}
Delete a buyer profile owned by the authenticated user.
**API scope required**: `agent-checkouts.buyer-profiles.delete`
# Get Agent Checkout
Source: https://docs.crossmint.com/api-reference/agent-checkouts/get-agent-checkout
api-reference/_open-api/Agents.json GET /unstable/agent-checkouts/{id}
Returns one checkout run by ID. Read `status` to see where it is: `queued`, `running`, `awaiting_input` when the agent needs an answer from the user, then `succeeded`, `failed`, `cancelled`, or `blocked`. `requiredAction` describes what an `awaiting_input` run is waiting for.
**API scope required**: `agent-checkouts.read`
# Get Browser Profile
Source: https://docs.crossmint.com/api-reference/agent-checkouts/get-browser-profile
api-reference/_open-api/Agents.json GET /unstable/agent-checkouts/browser-profiles/{id}
Retrieve a browser profile owned by the authenticated user. The response carries metadata only: what a checkout run did with the profile is reported on that run, not here.
A profile owned by another user returns `404`, not `403`.
**API scope required**: `agent-checkouts.browser-profiles.read`
# Get Buyer Profile
Source: https://docs.crossmint.com/api-reference/agent-checkouts/get-buyer-profile
api-reference/_open-api/Agents.json GET /unstable/agent-checkouts/buyer-profiles/{id}
Retrieve a buyer profile owned by the authenticated user.
**API scope required**: `agent-checkouts.buyer-profiles.read`
# List Agent Checkout Messages
Source: https://docs.crossmint.com/api-reference/agent-checkouts/list-agent-checkout-messages
api-reference/_open-api/Agents.json GET /unstable/agent-checkouts/{id}/messages
Returns the run's conversation, one page at a time: the agent's progress reports (`assistant`) and the user's answers (`user`). Each message carries the run `revision` it was recorded at. For a live view, use Stream Agent Checkout Messages.
**API scope required**: `agent-checkouts.read`
# List Agent Checkouts
Source: https://docs.crossmint.com/api-reference/agent-checkouts/list-agent-checkouts
api-reference/_open-api/Agents.json GET /unstable/agent-checkouts
Returns the authenticated user's checkout runs, newest first, one page at a time. Each run carries its `status`, the task and start URL it was created with, and `knownSpentUsdMicros` so far.
**API scope required**: `agent-checkouts.read`
# List Browser Profiles
Source: https://docs.crossmint.com/api-reference/agent-checkouts/list-browser-profiles
api-reference/_open-api/Agents.json GET /unstable/agent-checkouts/browser-profiles
List the authenticated user's browser profiles, newest first.
**API scope required**: `agent-checkouts.browser-profiles.read`
# List Buyer Profiles
Source: https://docs.crossmint.com/api-reference/agent-checkouts/list-buyer-profiles
api-reference/_open-api/Agents.json GET /unstable/agent-checkouts/buyer-profiles
List the authenticated user's buyer profiles, newest first. Paginate with the opaque `cursor` from `nextCursor`.
**API scope required**: `agent-checkouts.buyer-profiles.read`
# Send Agent Checkout Message
Source: https://docs.crossmint.com/api-reference/agent-checkouts/send-agent-checkout-message
api-reference/_open-api/Agents.json POST /unstable/agent-checkouts/{id}/messages
Sends a message into a checkout run: free text, or an `input_response` that answers an `awaiting_input` run by submitting form values, choosing an alternative, or declining. The response acknowledges receipt with a `messageId`; the agent's reply arrives as a later message.
**API scope required**: `agent-checkouts.update`
# Stream Agent Checkout Messages
Source: https://docs.crossmint.com/api-reference/agent-checkouts/stream-agent-checkout-messages
api-reference/_open-api/Agents.json GET /unstable/agent-checkouts/{id}/messages/stream
Server-sent events for one checkout: `message.upsert` carries a message, `run.updated` a run update, and each event's `id` is the cursor to resume from. Resume with `last-event-id` or `after`; omit both to stream from the start.
The checkout is read before the stream opens, so an unknown or inaccessible one returns a normal error status. A stale resume cursor, or one from another checkout, closes the stream instead: re-read the message history and reconnect from the cursor it reports.
**API scope required**: `agent-checkouts.read`
# Update Browser Profile
Source: https://docs.crossmint.com/api-reference/agent-checkouts/update-browser-profile
api-reference/_open-api/Agents.json PATCH /unstable/agent-checkouts/browser-profiles/{id}
Rename a browser profile. `label` is the only editable field.
**API scope required**: `agent-checkouts.browser-profiles.update`
# Update Buyer Profile
Source: https://docs.crossmint.com/api-reference/agent-checkouts/update-buyer-profile
api-reference/_open-api/Agents.json PATCH /unstable/agent-checkouts/buyer-profiles/{id}
Update a buyer profile's writable fields (any non-empty subset).
**API scope required**: `agent-checkouts.buyer-profiles.update`
# Create Agent Card
Source: https://docs.crossmint.com/api-reference/agentic-commerce/order-intents/create-order-intent
api-reference/_open-api/Agents.json POST /unstable/order-intents
Create a new order intent.
**API scope required**: `order-intents.create`
# Get Agent Card
Source: https://docs.crossmint.com/api-reference/agentic-commerce/order-intents/get-order-intent
api-reference/_open-api/Agents.json GET /unstable/order-intents/{orderIntentId}
Get a specific order intent by ID.
**API scope required**: `order-intents.read`
# Get Agent Card Credentials
Source: https://docs.crossmint.com/api-reference/agentic-commerce/order-intents/get-order-intent-credentials
api-reference/_open-api/Agents.json POST /unstable/order-intents/{orderIntentId}/credentials
Create a payment credential from an order intent.
**API scope required**: `order-intents.credentials`
# List Agent Cards
Source: https://docs.crossmint.com/api-reference/agentic-commerce/order-intents/list-order-intents
api-reference/_open-api/Agents.json GET /unstable/order-intents
List all order intents for the authenticated user.
**API scope required**: `order-intents.read`
# Revoke Agent Card
Source: https://docs.crossmint.com/api-reference/agentic-commerce/order-intents/revoke-order-intent
api-reference/_open-api/Agents.json DELETE /unstable/order-intents/{orderIntentId}
Cancel an active order intent.
**API scope required**: `order-intents.revoke`
# Create Payment Method
Source: https://docs.crossmint.com/api-reference/agentic-commerce/payment-methods/create-payment-method
api-reference/_open-api/PaymentMethods.json POST /unstable/payment-methods
Saves a payment method for a user so it can be reused across Checkout, Onramp, and Offramp orders. Sensitive fields (PAN, CVC, account numbers, IBANs, Bre-B keys) must be submitted through `vault.crossmint.com`, which tokenizes them before forwarding to Crossmint servers. Duplicate detection prevents saving the same card or account twice for the same user.
Supported types are `card`, the bank-account variants (`bank-account-us`, `bank-account-mx-clabe`, `bank-account-co`, `bank-account-sepa-iban`, `bank-account-ph`) and `bre-b`. For bank accounts the currency must match the account geography.
**API scope required**: `payment-methods.create`
**This endpoint is gated behind Payment Card Industry Data Security Standard (PCI DSS) compliance.** Because it accepts raw card numbers, Card Verification Codes (CVCs), and bank account details, you may only use it if your organization is PCI DSS compliant and has been explicitly enabled by Crossmint. You must provide evidence of your compliance (e.g. your Attestation of Compliance) and reach out to Crossmint to have it turned on — it is disabled by default.
**If you are not PCI compliant, do not use this endpoint.** Instead, use the client-side [Save a Card](/agents/payment-methods/cards/save-card) flow, which renders Crossmint's `CrossmintPaymentMethodManagement` component. Card data is collected directly by Crossmint in the browser and never touches your servers, so **you do not need to be PCI compliant** to use it.
This endpoint requires a JWT from an **external auth provider** (Auth0, Firebase, Stytch, etc.) or a **custom JWT** backed by a JWKS endpoint. Crossmint Auth is not supported.
This endpoint accepts raw card data and bank account details. Requests must be sent to the **vault host** (`vault.staging.crossmint.com` or `vault.crossmint.com`), which tokenizes sensitive fields before forwarding to the API. Raw card numbers, CVCs, and bank account numbers never reach `api.crossmint.com` directly.
# Delete Payment Method
Source: https://docs.crossmint.com/api-reference/agentic-commerce/payment-methods/delete-payment-method
api-reference/_open-api/PaymentMethods.json DELETE /unstable/payment-methods/{paymentMethodId}
Soft-deletes a payment method and returns it. It can no longer be used for payments. A deleted bank account stays readable through retrieve, and through list with `status=deleted`, and carries the terminal status `deleted`. A deleted card is excluded and carries no status. Deleting a bank account that is already deleted returns it again.
**API scope required**: `payment-methods.delete`
This endpoint requires a JWT from an **external auth provider** (Auth0, Firebase, Stytch, etc.) or a **custom JWT** backed by a JWKS endpoint. Crossmint Auth is not supported.
# List Payment Methods
Source: https://docs.crossmint.com/api-reference/agentic-commerce/payment-methods/list-payment-methods
api-reference/_open-api/PaymentMethods.json GET /unstable/payment-methods
Returns a paginated list of payment methods for a user, newest first by default. A deleted payment method is omitted; pass `status=deleted` to list the deleted bank accounts.
**API scope required**: `payment-methods.read`
This endpoint requires a JWT from an **external auth provider** (Auth0, Firebase, Stytch, etc.) or a **custom JWT** backed by a JWKS endpoint. Crossmint Auth is not supported.
# Get Action Status
Source: https://docs.crossmint.com/api-reference/common/get-action-status
api-reference/_open-api/MintingAPIs.json GET /2022-06-09/actions/{actionId}
Use this API to poll for the status of asynchonous actions such as NFT mints, transfers, etc.
**API scope required**: `nfts.create`
The shape of the data property in the `200` response for this API depends on the type of action initially performed.
For example, minting/burning an NFT, creating/updating a collection or template, or NFT transfers.
The `action` property will indicate the type of action originally performed and will be one of:
* `nfts.create`
* `nfts.delete`
* `nfts.update`
* `collections.create`
* `collections.update`
* `wallets:nfts.transfer`
# Check Token Support
Source: https://docs.crossmint.com/api-reference/headless/check-token-support
api-reference/_open-api/HeadlessAPIs.json GET /v1-alpha2/tokens/{tokenLocator}
Get a token and its available features by its locator. This endpoint allows you to check if a specific token is supported by Crossmint and what features are available for it.
**API scope required**: `orders.create`
# Create Order
Source: https://docs.crossmint.com/api-reference/headless/create-order
api-reference/_open-api/HeadlessAPIs.json POST /2022-06-09/orders
Creates a new order that can be used to complete a headless checkout.
# Edit Order
Source: https://docs.crossmint.com/api-reference/headless/edit-order
api-reference/_open-api/HeadlessAPIs.json PATCH /2022-06-09/orders/{orderId}
Edit an existing order. You can update the recipient, the payment method, and/or the locale.
**API scope required**: `orders.update`
# Get All Tokens
Source: https://docs.crossmint.com/api-reference/headless/get-all-tokens
api-reference/_open-api/Orders.json GET /2024-09-26/tokens
Returns a paginated list of the tokens that Crossmint supports for checkout, newest first: the tokens your users can buy through Crossmint. Access to this endpoint is enabled per project; contact support to enable it.
**API scope required**: `tokens.read`
# Get Order
Source: https://docs.crossmint.com/api-reference/headless/get-order
api-reference/_open-api/HeadlessAPIs.json GET /2022-06-09/orders/{orderId}
Get specific order by ID.
**Authentication**: Use either a server-side API key with `orders.read` scope, or the `clientSecret` from order creation as an authorization header.
# List Orders
Source: https://docs.crossmint.com/api-reference/headless/list-orders
api-reference/_open-api/HeadlessAPIs.json GET /2022-06-09/orders
Returns a paginated list of orders for your project, sorted by creation date (newest first by default). Use the `nextCursor` or `previousCursor` returned in the response to navigate between pages.
Optional filters accept comma-separated values and repeated query parameters (maximum 20 terms per filter). Keep the same filter params when following cursors.
**API scope required**: `orders.read`
# Create Payment Method
Source: https://docs.crossmint.com/api-reference/headless/payment-methods/create-payment-method
api-reference/_open-api/PaymentMethods.json POST /unstable/payment-methods
Save a payment method for a user so it can be used with Checkout, Onramp, and Offramp orders
Saves a payment method for a user so it can be reused across Checkout, Onramp, and Offramp orders.
**Card payments** require PCI-DSS certification for production access. Contact your Crossmint customer success engineer to get started. Bank account and SEPA payment methods do not have this requirement.
## Returns
Returns a [PaymentMethod](/api-reference/headless/payment-methods/payment-method-object) object.
# Delete a Payment Method
Source: https://docs.crossmint.com/api-reference/headless/payment-methods/delete-payment-method
api-reference/_open-api/PaymentMethods.json DELETE /unstable/payment-methods/{paymentMethodId}
Soft-delete a payment method. The record is retained for compliance but can no longer be used.
Deletes a payment method. The record is retained internally but can no longer be used for payments.
## Returns
Returns the [PaymentMethod](/api-reference/headless/payment-methods/payment-method-object) object.
# Retrieve a Payment Method
Source: https://docs.crossmint.com/api-reference/headless/payment-methods/get-payment-method
api-reference/_open-api/PaymentMethods.json GET /unstable/payment-methods/{paymentMethodId}
Fetch a single payment method by its ID
Retrieves a payment method by its ID.
## Returns
Returns a [PaymentMethod](/api-reference/headless/payment-methods/payment-method-object) object.
# List Payment Methods by User
Source: https://docs.crossmint.com/api-reference/headless/payment-methods/list-payment-methods
api-reference/_open-api/PaymentMethods.json GET /unstable/payment-methods
Retrieve all payment methods saved for a given user
Returns a paginated list of payment methods for a given user.
## Returns
An object with a `data` array containing [PaymentMethod](/api-reference/headless/payment-methods/payment-method-object) objects. Use the `cursor` parameter to paginate through results. When `nextCursor` is present, additional pages are available in that direction.
# The Payment Method Object
Source: https://docs.crossmint.com/api-reference/headless/payment-methods/payment-method-object
Attributes and structure of a saved payment method
PaymentMethod objects represent your user's payment instruments. You can use them with any of the Crossmint products like Checkout, Onramp, and Offramp.
## Type Values
`type` names the **account identifier** (the capture schema Crossmint collects and validates) — never the payment rail, scheme, or clearing house. The same saved account can be paid over multiple rails; rail selection happens at payment execution time.
| Value | Description |
| - | - |
| `card` | Credit or debit card. |
| `bank-account-us` | US bank account (routing number + account number). |
| `bank-account-sepa-iban` | Bank account identified by an IBAN within the supported SEPA scope. |
| `bank-account-co` | Colombian bank account (institution ID + account number + national document). |
| `bre-b` | Colombian Bre-B key: an `@handle`, a mobile number, an e-mail, an identity document, or a commerce code. |
| `bank-account-id` | Indonesian bank account (institution ID + account number), paid out over BI-FAST. |
| `bank-account-mx-clabe` | Mexican bank account, identified by an 18-digit CLABE (Clave Bancaria Estandarizada). |
| `bank-account-ng` | Nigerian bank account (institution ID + 10-digit NUBAN, the Nigeria Uniform Bank Account Number), paid out over NIP. |
| `bank-account-ph` | Philippine bank account (institution ID + account number), paid out over InstaPay or PESONet. |
| `bank-account-th` | Thai bank account (institution ID + account number), paid out over PromptPay. |
| `bank-account-vn` | Vietnamese bank account (institution ID + account number), paid out over Napas 247 or CITAD. |
`bank-account-sepa-iban` is the payment-method type; `sepa` and `sepa-instant` are rail values within it. A SEPA IBAN account can support both rails.
## Attributes
Unique identifier for the payment method.
Whether this is the user's default payment method. Returned for `card` and `bank-account-sepa-iban` only.
Server-derived label, returned for `card` and `bank-account-sepa-iban` only (e.g. `"Visa ••1111"`, `"Deutsche Bank ••6789"`). Absent on every other bank-account type; read `label` there.
Payment method type. One of `card`, `bank-account-us`, `bank-account-sepa-iban`, `bank-account-co`, `bre-b`, `bank-account-id`, `bank-account-mx-clabe`, `bank-account-ng`, `bank-account-ph`, `bank-account-th`, or `bank-account-vn`.
Bank accounts and Bre-B keys only. Readiness of the destination, not user approval, ownership, or a guaranteed payout. One of:
* `pending`: initial readiness checks are running.
* `active`: required readiness checks passed. Orders still run their own eligibility checks.
* `rejected`: the destination cannot receive a supported payout. The initial checks ended without a usable record, a provider refused the account, or the bank returned a payout to it. Retrying requires a new payment method.
* `deleted`: removed by the client. Never reactivates.
`pending` moves to `active`, `rejected`, or `deleted`; `active` moves to `rejected` or `deleted`; `rejected` moves only to `deleted`. Creation starts the readiness checks once; reads, label edits, and orders never restart them. Treat an unrecognized status as not ready for orders. See [Manage Payment Methods](/offramp/guides/collect-bank-accounts#statuses).
Always `null` except on a `rejected` method, where it may be `destination-not-found`, `destination-closed`, `destination-cannot-receive`, or `no-rail-available`. `null` when no safe destination condition was established. Never carries provider errors or compliance detail. Codes are extensible, so tolerate unknown values.
Card details. Present when `type` is `card`.
Card brand. Can be `visa`, `mastercard`, `amex`, `discover`, `jcb`, `unionpay`, or `diners-club`.
Last four digits of the card number.
Two-digit expiration month.
Four-digit expiration year.
Billing details for the cardholder.
Full name.
Phone number (E.164 format).
Billing address.
Card funding type. Can be `credit`, `debit`, `prepaid`, or `unknown`.
Bank Identification Number (first 6-8 digits of the card number).
Two-letter country code of the card issuer.
Bank account details. Present when `type` is `bank-account-us`, `bank-account-sepa-iban`, `bank-account-co`, `bank-account-id`, `bank-account-mx-clabe`, `bank-account-ng`, `bank-account-ph`, `bank-account-th`, or `bank-account-vn`. A `bre-b` method returns `breB` instead.
The person or company that owns the account. Always the value the client sent, never the bank's record of the holder. Derived from `billing` when the record predates this field.
Full legal name.
E-mail address.
Phone number (E.164 format).
Holder address, without a country: the account's own `country` is the bank's jurisdiction. Present only on types that collect one. Never on `bank-account-mx-clabe`.
Name of the bank. `null` if not resolvable. Absent for `bank-account-co`, `bank-account-id`, `bank-account-mx-clabe`, `bank-account-ng`, `bank-account-ph`, `bank-account-th`, and `bank-account-vn`.
Last four digits of the account number, CLABE, or IBAN.
ISO 4217 currency code (e.g. `usd`, `mxn`, `eur`, `cop`).
ISO 3166-1 alpha-2 country code.
Account holder type. `individual` or `business`. Required on creation. May be absent on records created before the field was enforced.
Read-only. The payout rails a currently-available provider has confirmed for this account. Absent when no payout provider currently serves the account.
`pending` while asynchronous rail resolution is still running for optional rails (baseline rails appear immediately); `complete` once resolved.
Rail values this account supports. See [Payment Schemes](/offramp/payment-methods) for the current rail catalog and indicative delivery times.
Rails still being resolved. Non-empty only while `status` is `pending`; `[]` once `status` is `complete`.
Bank branch address.
Nine-digit ABA routing number.
`checking` or `savings`.
BIC/SWIFT code (8 or 11 characters).
Identifier of the bank that holds the account, for example `co-bancolombia`. See [Bank Institution IDs](/offramp/guides/bank-institution-ids).
`checking`, `savings`, or `electronic-deposit`.
The holder's national document. The full number is never returned.
One of `cc`, `nit`, `ce`, `pa`, `ppt`, `ti`, `rc`, `te`, or `die`.
Last four digits of the document number.
Identifier of the bank that holds the account, for example `id-bca`. See [Bank Institution IDs](/offramp/guides/bank-institution-ids).
`checking` or `savings`. Present when sent on creation.
Bre-B key details. Present when `type` is `bre-b`, in place of `bankAccount`. The key itself is never returned.
The person or company that owns the key. Always the value the client sent.
Full legal name.
E-mail address.
Phone number (E.164 format).
Masked form of the key. An e-mail key is masked in place. Every other key shows its last four characters.
Always `cop`. Bre-B only settles in Colombian pesos.
Always `CO`.
Key holder type. `individual` or `business`.
Category of the key, derived from its format. One of `alphanumeric`, `phone`, `email`, `identification`, or `commerce`. A client-supplied value is refused.
Display name of the institution that holds the key. Absent until a name source answers.
Read-only. The payout rails a currently-available provider advertises for this key. Absent when no provider serves the key.
Rail values this key supports. Bre-B carries `supported` alone, without the `status` and `pending` fields an ordinary bank account returns.
Bank accounts only. The client's own name for this payment method, when one was sent on creation. At most 255 characters. A card request does not accept it.
Read-only. Bank accounts only. ISO 8601 timestamp of the most recent successful offramp payout funded by this account. Absent until the first payout.
```json Card theme={null}
{
"paymentMethodId": "d4e5f6a7-b8c9-0123-def4-567890abcdef",
"default": true,
"displayName": "Visa ••1111",
"type": "card",
"card": {
"brand": "visa",
"last4": "1111",
"expiration": {
"month": "12",
"year": "2028"
},
"billing": {
"name": "Alice Smith",
"phone": "+12125551234",
"address": {
"line1": "123 Main St",
"city": "Springfield",
"stateOrRegion": "IL",
"postalCode": "62701",
"country": "US"
}
},
"fundingType": "debit",
"bin": "411111",
"country": "US"
}
}
```
```json US Bank Account theme={null}
{
"paymentMethodId": "f2ecef7e-91af-4fa7-93f9-12db856c227f",
"type": "bank-account-us",
"bankAccount": {
"bankName": "Chime",
"accountSuffix": "6259",
"currency": "usd",
"country": "US",
"entityType": "individual",
"bankAddress": {
"line1": "123 Main St",
"city": "New York",
"stateOrRegion": "NY",
"postalCode": "10001",
"country": "US"
},
"routingNumber": "091300010",
"accountType": "checking",
"rails": { "status": "complete", "supported": ["rtp", "ach-same-day", "ach-us"], "pending": [] }
}
}
```
```json SEPA theme={null}
{
"paymentMethodId": "3323460b-d9d2-4ec6-b380-8a3e01e3f226",
"default": false,
"displayName": "Deutsche Bank ••6789",
"type": "bank-account-sepa-iban",
"bankAccount": {
"bankName": "Deutsche Bank",
"accountSuffix": "6789",
"currency": "eur",
"country": "DE",
"entityType": "individual",
"bic": "COBADEFFXXX"
}
}
```
```json CO Bank Account theme={null}
{
"paymentMethodId": "1cad2281-bd3d-4219-8787-7dfea94c60b1",
"type": "bank-account-co",
"bankAccount": {
"accountSuffix": "7320",
"institutionId": "co-bancolombia",
"accountType": "checking",
"currency": "cop",
"country": "CO",
"entityType": "individual",
"accountHolder": { "name": "Camila Restrepo", "document": { "type": "cc", "suffix": "4050" } },
"rails": { "status": "pending", "supported": [], "pending": ["ach-credit"] }
}
}
```
```json CO Bre-B key theme={null}
{
"paymentMethodId": "9f1a6b2c-3d4e-4f50-9a61-2b3c4d5e6f70",
"type": "bre-b",
"status": "active",
"statusReason": null,
"breB": {
"accountHolder": {
"name": "Carlos Gómez",
"phoneNumber": "+573001234567"
},
"accountSuffix": "CDKS",
"currency": "cop",
"country": "CO",
"entityType": "individual",
"keyType": "alphanumeric"
}
}
```
```json ID Bank Account theme={null}
{
"paymentMethodId": "7a3e5c91-2b4d-4f86-a1c0-9d8e7f6a5b42",
"type": "bank-account-id",
"bankAccount": {
"accountSuffix": "7890",
"institutionId": "id-bca",
"currency": "idr",
"country": "ID",
"entityType": "individual",
"accountHolder": { "name": "Budi Santoso" },
"rails": { "status": "complete", "supported": ["bi-fast"], "pending": [] }
}
}
```
```json MX Bank Account theme={null}
{
"paymentMethodId": "27bacbe1-d0ff-4bc3-b37a-ca91cc64fa39",
"type": "bank-account-mx-clabe",
"bankAccount": {
"accountHolder": {
"name": "Luis Gomez Rios",
"phoneNumber": "+525512345678"
},
"accountSuffix": "7899",
"currency": "mxn",
"country": "MX",
"entityType": "individual"
}
}
```
```json NG Bank Account theme={null}
{
"paymentMethodId": "2e9b4d17-6a3c-4f85-b1d0-8c7e5a2f9b63",
"type": "bank-account-ng",
"bankAccount": {
"accountSuffix": "7897",
"institutionId": "ng-gt-bank",
"currency": "ngn",
"country": "NG",
"entityType": "individual",
"accountHolder": { "name": "Chinedu Okafor" },
"rails": { "status": "complete", "supported": ["nip"], "pending": [] }
}
}
```
```json PH Bank Account theme={null}
{
"paymentMethodId": "5e2b7c41-9d0a-4f3b-8c6d-2a1b3c4d5e6f",
"type": "bank-account-ph",
"bankAccount": {
"accountSuffix": "7890",
"institutionId": "ph-bdo",
"currency": "php",
"country": "PH",
"entityType": "individual",
"accountHolder": { "name": "Maria Santos" },
"rails": { "status": "complete", "supported": ["instapay", "pesonet"], "pending": [] }
}
}
```
```json TH Bank Account theme={null}
{
"paymentMethodId": "8a4c7e39-2f1b-4d96-b3e5-7c0d2a9f6b41",
"type": "bank-account-th",
"bankAccount": {
"accountSuffix": "7890",
"institutionId": "th-kasikorn-bank",
"currency": "thb",
"country": "TH",
"entityType": "individual",
"accountHolder": { "name": "Somchai Jaidee" },
"rails": { "status": "complete", "supported": ["promptpay"], "pending": [] }
}
}
```
```json VN Bank Account theme={null}
{
"paymentMethodId": "3f7d1b86-9e2a-4c50-8d34-1a6e9b7c5f02",
"type": "bank-account-vn",
"bankAccount": {
"accountSuffix": "4567",
"institutionId": "vn-acb",
"currency": "vnd",
"country": "VN",
"entityType": "individual",
"accountHolder": { "name": "Nguyễn Văn An" },
"rails": { "status": "complete", "supported": ["napas-247", "citad"], "pending": [] }
}
}
```
# Update a Payment Method
Source: https://docs.crossmint.com/api-reference/headless/payment-methods/update-payment-method
api-reference/_open-api/PaymentMethods.json PATCH /unstable/payment-methods/{paymentMethodId}
Update the default status of an existing payment method
Updates a payment method's default status. Only `default` can be changed.
## Returns
Returns the updated [PaymentMethod](/api-reference/headless/payment-methods/payment-method-object) object.
# Process Payment
Source: https://docs.crossmint.com/api-reference/headless/process-payment
api-reference/_open-api/HeadlessAPIs.json POST /2022-06-09/orders/{orderId}/payment
Process payment for an order using a crypto transaction ID.
**Authentication**: Use either a server-side API key with `orders.update` scope, the `clientSecret` from order creation as an authorization header, or a JWT token with `orders.create` scope.
# Introduction
Source: https://docs.crossmint.com/api-reference/introduction
Explore the APIs and test them live from the browser
This section provides a detailed guide for each of the APIs, with sample code and responses.
## Testing the APIs from the browser
The reference pages allow you to call the APIs directly from the browser. To get started:
1. Create a developer account on the Staging or Production consoles. Read more about the environments [here](/introduction/platform/staging-vs-production).
2. Create an [API key](/introduction/platform/api-keys) from the `API keys` tab on the console, with the permissions required for the APIs you want to use.
3. From the page of your API of choice, insert the right API key in the authorization slot, introduce the different parameters, and call "Send".
4. (Optional) To call APIs in the production environment, find the API endpoint selector (`⌄`) next to the endpoint URL, and change it to `www.crossmint.com/api`.
The following guide will show you how to create a wallet and mint an NFT into it within 5 minutes. For this, make sure your API key has the scopes `wallets.create`, and `nfts.create`.
Follow the next steps to create a wallet within 5 minutes:
You may add any additional properties. Each API reference page includes the full list of properties and admissible values.
Scroll back to the top of the page and click the blue `Send` button to trigger
the API call.
Here is an example response object:
**Success!** The next guide will show you how to mint an NFT into this wallet and view the content.
In the previous guide, you created a wallet. Now you will deliver an NFT into it and read the wallet's content.
This is a default collection associated to your account. You can create new ones using the [Create Collections](/minting/nfts/integrate/create-collections) API.
The selector to choose between the different metadata option can be hard to spot. Look closely at the screenshots below to see where it is.
You can provide your own or use this example URL:
```bash metadata URL theme={null}
https://bafkreidbf4jpxpecwezjbagwmua5qv62ifhhtzsspoml7zls6iq6ms4byi.ipfs.nftstorage.link/
```
```json example JSON theme={null}
{
"name": "Crossmint Test NFT",
"description": "Created with the Crossmint minting API",
"image": "https://bafkreiexjl6kw4khdxkrt6dojgacscnzvrys47t472l2t7d6r2ss65kifq.ipfs.nftstorage.link/",
"external_url": "https://docs.crossmint.com",
"attributes": [
{
"trait_type": "contract",
"value": "ERC-721"
},
{
"trait_type": "background",
"value": "black"
}
]
}
```
This URL must point to a valid JSON file that adheres to [EVM metadata standards](/minting/nfts/integrate/define-metadata).
There are two options for the `recipient` parameter. The first enables minting to an email address. The second enables minting directly to a wallet address.
```javascript email recipient theme={null}
// format
email::
// example
email:testy@crossmint.xyz:polygon
```
```javascript wallet recipient theme={null}
// format
:
// example
polygon:0x0359Cd99FD20e853a85489DFC93EaDFeF7461590
```
Enter your own email address on this step and you can log in to see your NFT in the Crossmint wallet later, which can be accessed from crossmint.com or staging.crossmint.com if you are in staging.
This will prevent unnecessary uploading of files that are already pinned to IPFS.
Crossmint has a 10MB re-upload size limit. For larger files, upload your media to IPFS, create a metadata.json as shown above, submit the metadata file's URL, and ensure `reuploadLinkedFiles` is set to `false`.
The default option is `true`.
Scroll back to the top of the page and click the blue `Send` button to trigger
the API call.
Here is an example response object:
If you followed this guide closely you can view the NFT in the staging wallet at [https://staging.crossmint.com/user/collection](https://staging.crossmint.com/user/collection).
If you minted directly to a wallet address you can view the NFT in that application. Ensure you are accessing it from a testnet if you used staging.
# Create IP Asset
Source: https://docs.crossmint.com/api-reference/ip/create-ip-asset
api-reference/_open-api/Story.json POST /v1/ip/collections/{collectionId}/ipassets
Create a new IP Asset
**API scope required**: `nfts.create`
This API is still under development. Contact support for early access.
# Create IP Asset (Idempotent)
Source: https://docs.crossmint.com/api-reference/ip/create-ip-asset-idempotent
api-reference/_open-api/Story.json PUT /v1/ip/collections/{collectionId}/ipassets/{customerFacingId}
Create a new IP Asset with a pre-computed id, or get an existing one if the id already exists
**API scope required**: `nfts.create`
This API is still under development. Contact support for early access.
# Create IP Collection
Source: https://docs.crossmint.com/api-reference/ip/create-ip-collection
api-reference/_open-api/Story.json POST /v1/ip/collections
Create a new collection with a pre-computed id, or get an existing one if the id already exists
**API scope required**: `collections.create`
This API is still under development. Contact support for early access.
# Create IP Collection (Idempotent)
Source: https://docs.crossmint.com/api-reference/ip/create-ip-collection-idempotent
api-reference/_open-api/Story.json PUT /v1/ip/collections/{collectionId}
Create a new collection with a pre-computed id, or get an existing one if the id already exists
**API scope required**: `collections.create`
This API is still under development. Contact support for early access.
# Get All IP Collections
Source: https://docs.crossmint.com/api-reference/ip/get-all-ip-collections
api-reference/_open-api/Story.json GET /v1/ip/collections
Get all collections associated with the Developer Project
**API scope required**: `collections.read`
This API is still under development. Contact support for early access.
# Get IP Action
Source: https://docs.crossmint.com/api-reference/ip/get-ip-action
api-reference/_open-api/Story.json GET /v1/ip/actions/{actionId}
Get an action by its id
**API scope required**: `nfts.create`
This API is still under development. Contact support for early access.
# Get IP Asset
Source: https://docs.crossmint.com/api-reference/ip/get-ip-asset
api-reference/_open-api/Story.json GET /v1/ip/collections/{collectionId}/ipassets/{customerFacingId}
Get a single IP Asset
**API scope required**: `nfts.read`
This API is still under development. Contact support for early access.
# Get IP Assets in Collection
Source: https://docs.crossmint.com/api-reference/ip/get-ip-assets
api-reference/_open-api/Story.json GET /v1/ip/collections/{collectionId}/ipassets
Get all IP Assets in a collection
**API scope required**: `nfts.read`
This API is still under development. Contact support for early access.
# Get IP Collection
Source: https://docs.crossmint.com/api-reference/ip/get-ip-collection
api-reference/_open-api/Story.json GET /v1/ip/collections/{collectionId}
Get a collection by its id deployed on the Story chain
**API scope required**: `collections.read`
This API is still under development. Contact support for early access.
# Get IP Asset Graph
Source: https://docs.crossmint.com/api-reference/ip/get-ip-graph
api-reference/_open-api/Story.json GET /v1/ip/graph/{ipAssetId}
Get the graph of an IP Asset, by default it will fetch the first level of parents and children (depth = 1). You can customize the depth using the query parameter 'depth' to a maximum of 3. Maximum 100 parents or children will be returned for each level.
The ipAssetId parameter should be the Story Protocol asset ID (not the Crossmint ID). Must start with '0x' followed by hexadecimal characters.
**API scope required**: `nfts.read`
This API is still under development. Contact support for early access.
# Get IP Asset License
Source: https://docs.crossmint.com/api-reference/ip/get-ip-license
api-reference/_open-api/Story.json GET /v1/ip/licenses/{ipassetId}
Get the licenses of an IP Asset
The ipassetId parameter should be the Story Protocol asset ID (not the Crossmint ID). Must start with '0x' followed by hexadecimal characters.
**API scope required**: `nfts.read`
This API is still under development. Contact support for early access.
# Update IP Asset
Source: https://docs.crossmint.com/api-reference/ip/update-ip-asset
api-reference/_open-api/Story.json PATCH /v1/ip/collections/{collectionId}/ipassets/{customerFacingId}
Update an existing IP Asset
**API scope required**: `nfts.update`
This API is still under development. Contact support for early access.
# Create Collection
Source: https://docs.crossmint.com/api-reference/minting/collection/create-collection
api-reference/_open-api/MintingAPIs.json POST /2022-06-09/collections/
Create a collection that you can mint NFTs/SFTs from
**API scope required**: `collections.create`
# Create Collection (Idempotent)
Source: https://docs.crossmint.com/api-reference/minting/collection/create-collection-idempotent
api-reference/_open-api/MintingAPIs.json PUT /2022-06-09/collections/{collectionId}
Create a collection that you can mint NFTs/SFTs from. This API is idempotent,
if you call it multiple times with the same ID, only one will be created.
**API scope required**: `collections.create`
# Get All Collections
Source: https://docs.crossmint.com/api-reference/minting/collection/get-all-collections
api-reference/_open-api/MintingAPIs.json GET /2022-06-09/collections/
List all collections created under the current Crossmint project
**API scope required**: `collections.read`
For Solana collections the `onChain.contractAddress` property will be named `onChain.mintAddress`
```json 200 theme={null}
{
"results": [
{
"id": "bb691876-edb3-404c-af3e-c019b8e2ed2c",
"metadata": {
"name": "Test Collection",
"description": "Test",
"imageUrl": "ipfs://QmVocoiYXZLAtheEHV3VF8w4pa68bkPutT8cQZdMrrpzxh",
"symbol": "XMINT"
},
"fungibility": "non-fungible",
"onChain": {
"chain": "polygon",
"type": "erc-721",
"contractAddress": "0x9564bD85f3D5677D86244dDb06F06bbD22D9d0DB"
},
"supplyLimit": 95,
"payments": {
"price": "0.001",
"recipientAddress": "0x6C3b3225759Cbda68F96378A9F0277B4374f9F06"
}
}
]
}
```
# Get Base URI
Source: https://docs.crossmint.com/api-reference/minting/collection/get-base-uri
api-reference/_open-api/MintingAPIs.json GET /v1-alpha1/minting/collections/{collectionId}/base-uri
Get the Base URI of a collection as it appears onchain.
**API scope required**: `collections.read`
# Get Collection Info
Source: https://docs.crossmint.com/api-reference/minting/collection/get-collection
api-reference/_open-api/MintingAPIs.json GET /2022-06-09/collections/{collectionId}
Get information about a specific collection.
**API scope required**: `collections.read`
For Solana collections the `onChain.contractAddress` property will be named `onChain.mintAddress`
# Get Royalties Configuration
Source: https://docs.crossmint.com/api-reference/minting/collection/get-royalties-configuration
api-reference/_open-api/MintingAPIs.json GET /v1-alpha1/minting/collections/{collectionId}/royalties
Fetch the royalty configuration for a collection, from its current state
in the blockchain.
This API is only supported on EVM chains.
If you call GET too soon after PUT/DELETE,
you may not yet see your latest changes, as they can take a few seconds to
record on the blockchain.
**API scope required**: `collections.read`
# Get Transferability
Source: https://docs.crossmint.com/api-reference/minting/collection/get-transferability
api-reference/_open-api/MintingAPIs.json GET /v1-alpha1/minting/collections/{collectionId}/transferable
Get the transferable status of a collection.
This API is supported on EVM and Aptos chains.
You must contact sales to gain access to this API.
**API scope required**: `collections.read`
# Remove Royalties
Source: https://docs.crossmint.com/api-reference/minting/collection/remove-royalties
api-reference/_open-api/MintingAPIs.json DELETE /v1-alpha1/minting/collections/{collectionId}/royalties
Remove all royalties from a given collection. No new NFT sales will yield royalties to the creator.
This API is only supported on EVM Chains.
**API scope required**: `collections.update`
# Set Base URI
Source: https://docs.crossmint.com/api-reference/minting/collection/set-base-uri
api-reference/_open-api/MintingAPIs.json PUT /v1-alpha1/minting/collections/{collectionId}/base-uri
Update the Base URI of a collection. Setting the baseURI enables
excluding the metadata param when minting. Tokens minted without the metadata
param will have a tokenURI of:
`{BASE_URI}/{TOKEN_ID}`
This API is currently only supported on EVM Chains.
**API scope required**: `collections.update`
# Set Royalties
Source: https://docs.crossmint.com/api-reference/minting/collection/set-royalties
api-reference/_open-api/MintingAPIs.json PUT /v1-alpha1/minting/collections/{collectionId}/royalties
Configure royalties for all NFTs in a collection.
This API is only supported for EVM chains and implements the EIP-2981 standard.
**API scope required**: `collections.update`
# Set Transferability
Source: https://docs.crossmint.com/api-reference/minting/collection/set-transferability
api-reference/_open-api/MintingAPIs.json PUT /v1-alpha1/minting/collections/{collectionId}/transferable
Update the transferable status of a collection.
This API is supported on EVM and Aptos chains.
You must contact sales to gain access to this API.
**API scope required**: `collections.update`
# Update Collection
Source: https://docs.crossmint.com/api-reference/minting/collection/update-collection
api-reference/_open-api/MintingAPIs.json PATCH /2022-06-09/collections/{collectionId}
Update the sales details of a collection
**API scope required**: `collections.update`
# Burn NFT
Source: https://docs.crossmint.com/api-reference/minting/nfts/burn-nft
api-reference/_open-api/MintingAPIs.json DELETE /2022-06-09/collections/{collectionId}/nfts/{id}
Burn a minted NFT.
**API scope required**: `nfts.delete`
# Edit NFT
Source: https://docs.crossmint.com/api-reference/minting/nfts/edit-nft
api-reference/_open-api/MintingAPIs.json PATCH /2022-06-09/collections/{collectionId}/nfts/{id}
Edit a minted NFT's metadata on IPFS.
If you are using a custom baseURI, invoking this will overwrite the specific tokenURI for the edited token.
**API scope required**: `nfts.update`
# Get All NFTs
Source: https://docs.crossmint.com/api-reference/minting/nfts/get-nfts
api-reference/_open-api/MintingAPIs.json GET /2022-06-09/collections/{collectionId}/nfts
Get a list of all the NFTs in a given collection.
**API scope required**: `nfts.read`
# Mint NFT
Source: https://docs.crossmint.com/api-reference/minting/nfts/mint-nft
api-reference/_open-api/MintingAPIs.json POST /2022-06-09/collections/{collectionId}/nfts
Mint your NFTs and deliver them to a web3 wallet or an email address
**API scope required**: `nfts.create`
For uncompressed Solana NFTs, [contact sales](https://www.crossmint.com/contact/sales) for access.
# Mint NFT with ID
Source: https://docs.crossmint.com/api-reference/minting/nfts/mint-nft-idempotent
api-reference/_open-api/MintingAPIs.json PUT /2022-06-09/collections/{collectionId}/nfts/{id}
This pathway allows you to mint NFTs and guarantee idempotency
to ensure you never double mint for the same NFT.
**API scope required**: `nfts.create`
Subsequent requests to this endpoint with the same `id` in the path will ***not*** mint additional NFTs.
Furthermore, the success responses (with status code 200) will be different once the initial request has completed and includes the metadata for the minted NFT.
```json EVM theme={null}
{
"id": "",
"metadata": {
"name": "",
"image": "",
"description": ""
},
"onChain": {
"status": "",
"tokenId": "",
"owner": "",
"txId": "",
"contractAddress": "",
"chain": ""
},
"actionId": ""
}
```
```json Solana theme={null}
{
"id": "",
"metadata": {
"name": "",
"symbol": "",
"description": "",
"seller_fee_basis_points": 0,
"image": "",
"attributes": [],
"properties": {}
},
"onChain": {
"status": "success",
"mintHash": "",
"txId": "",
"owner": "",
"chain": "solana"
},
"actionId": ""
}
```
# Mint SFT
Source: https://docs.crossmint.com/api-reference/minting/nfts/mint-sft
api-reference/_open-api/MintingAPIs.json POST /2022-06-09/collections/{collectionId}/sfts
Mint your SFTs and deliver them to a web3 wallet or an email address
**API scope required**: `nfts.create`
# Mint Status
Source: https://docs.crossmint.com/api-reference/minting/nfts/mint-status
api-reference/_open-api/MintingAPIs.json GET /2022-06-09/collections/{collectionId}/nfts/{id}
Get the status and associated information for a mint operation.
**API scope required**: `nfts.read`
```json 200 EVM theme={null}
{
"id": "",
"metadata": {
"name": "",
"image": "",
"description": ""
},
"onChain": {
"status": "success",
"tokenId": "",
"owner": "",
"txId": "",
"contractAddress": "",
"chain": "polygon"
},
"action": "https://staging.crossmint.com/api/2022-06-09/actions/"
}
```
```json 200 Solana theme={null}
{
"id": "",
"metadata": {
"name": "",
"symbol": "",
"seller_fee_basis_points": 0,
"properties": {},
"description": "",
"image": "",
"attributes": []
},
"onChain": {
"status": "success",
"mintHash": "",
"txId": "",
"owner": "",
"chain": "solana"
},
"action": "https://staging.crossmint.com/api/2022-06-09/actions/"
}
```
# Create Template
Source: https://docs.crossmint.com/api-reference/minting/template/create-template
api-reference/_open-api/MintingAPIs.json POST /2022-06-09/collections/{collectionId}/templates
Create a token template, that NFTs or SFTs may be minted from
**API scope required**: `nfts.create`
# Create Template with ID
Source: https://docs.crossmint.com/api-reference/minting/template/create-template-idempotent
api-reference/_open-api/MintingAPIs.json PUT /2022-06-09/collections/{collectionId}/templates/{templateId}
Create a token template with preconfigured metadata
**API scope required**: `nfts.create`
# Delete Template
Source: https://docs.crossmint.com/api-reference/minting/template/delete-template
api-reference/_open-api/MintingAPIs.json DELETE /2022-06-09/collections/{collectionId}/templates/{templateId}
Delete a Token template.
**API scope required**: `nfts.delete`
# Edit Template
Source: https://docs.crossmint.com/api-reference/minting/template/edit-template
api-reference/_open-api/MintingAPIs.json PATCH /2022-06-09/collections/{collectionId}/templates/{templateId}
Edit a Token template.
**API scope required**: `nfts.update`
# Get All Templates
Source: https://docs.crossmint.com/api-reference/minting/template/get-all-templates
api-reference/_open-api/MintingAPIs.json GET /2022-06-09/collections/{collectionId}/templates
Get all of the templates for a collection
**API scope required**: `nfts.read`
# Get Template
Source: https://docs.crossmint.com/api-reference/minting/template/get-template
api-reference/_open-api/MintingAPIs.json GET /2022-06-09/collections/{collectionId}/templates/{templateId}
Fetch the contents of a token template.
**API scope required**: `nfts.read`
# Accept Legal Document
Source: https://docs.crossmint.com/api-reference/users/accept-legal-document
api-reference/_open-api/Users.json PUT /2025-06-09/users/{userLocator}/legal-documents
Record user acceptance of a legal document. Supported types are `crossmint-privacy-policy` and `crossmint-terms-of-service`. The latest version of the document is always recorded. When accepting `crossmint-terms-of-service`, the `region` field is required so the correct regional agreement is resolved.
**API scope required**: `users.create`
# Get a User Document by ID
Source: https://docs.crossmint.com/api-reference/users/get-document
api-reference/_open-api/Users.json GET /2025-06-09/documents/{documentId}
Get a specific document by its ID.
**API scope required**: `users.read`
# Get Identity Verification Status
Source: https://docs.crossmint.com/api-reference/users/get-identity-verification
api-reference/_open-api/Users.json GET /2025-06-09/users/{userLocator}/identity-verification
Get identity verification status for a user.
**API scope required**: `users.read`
# Get Linked External Wallet
Source: https://docs.crossmint.com/api-reference/users/get-linked-wallet
api-reference/_open-api/Users.json GET /2025-06-09/users/{userLocator}/linked-wallets/{address}
Get a linked external wallet for a user by address.
**API scope required**: `users.read`
# Get a User
Source: https://docs.crossmint.com/api-reference/users/get-user
api-reference/_open-api/Users.json GET /2025-06-09/users/{userLocator}
Get a user by its id
**API scope required**: `users.read`
# Link External Wallet to User
Source: https://docs.crossmint.com/api-reference/users/link-wallet
api-reference/_open-api/Users.json PUT /2025-06-09/users/{userLocator}/linked-wallets/{address}
Link an external wallet address to a user. Optionally provide a proof to verify ownership.
**API scope required**: `users.create`
# Trigger Identity Verification
Source: https://docs.crossmint.com/api-reference/users/trigger-identity-verification
api-reference/_open-api/Users.json PUT /2025-06-09/users/{userLocator}/identity-verification
Trigger identity verification for a user.
**API scope required**: `users.create`
# Unlink External Wallet from User
Source: https://docs.crossmint.com/api-reference/users/unlink-wallet
api-reference/_open-api/Users.json DELETE /2025-06-09/users/{userLocator}/linked-wallets/{address}
Unlink an external wallet address from a user.
**API scope required**: `users.create`
# Upload a User Document
Source: https://docs.crossmint.com/api-reference/users/upload-document
api-reference/_open-api/Users.json POST /2025-06-09/documents
Upload a document for a user.
**API scope required**: `users.create`
# Create or Update User
Source: https://docs.crossmint.com/api-reference/users/upsert-user
api-reference/_open-api/Users.json PUT /2025-06-09/users/{userLocator}
Create or update a user and their personal data by user locator. If the user doesn't exist, they will be created.
**API scope required**: `users.create`
# Issue Credential
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/credentials/issue-credential
api-reference/_open-api/VerifiableCredentialsAPIs.json POST /v1-alpha1/credentials/templates/{templateId}/vcs
Issue a credential and deliver it to a wallet or email address.
**API scope required** `credentials.create`
# Get Credential by Credential ID
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/credentials/retrieve-credential-by-id
api-reference/_open-api/VerifiableCredentialsAPIs.json GET /v1-alpha1/credentials/{id}
Get a verifiable credential by the ID associated with it.
This ID will have the format: `urn:uuid:`. For example: `urn:uuid:64f9877d-a19a-4205-8d61-f8c2abed5766`
**API scope required** `credentials.read`. This enpoint will work also with a client side API key.
# Get Credential by NFT ID
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/credentials/retrieve-credential-by-nft
api-reference/_open-api/VerifiableCredentialsAPIs.json GET /v1-alpha1/credentials/templates/{collectionId}/nfts/{id}/credentials
Get a verifiable credential by the ID associated with the minted NFT.
This ID will have the format: ``. For example: `d7eb777b-e9b4-4f34-ab5f-ce199111166a`
**API scope required** `credentials.read`. This endpoint will not work with a client side API key.
# Get Credential by NFT Locator
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/credentials/retrieve-credential-by-nft-locator
api-reference/_open-api/VerifiableCredentialsAPIs.json GET /v1-alpha1/nfts/{nftLocator}/credentials
Get a verifiable credential by the NFT locator.
This locator will have the format: `::`.
For example: `polygon:0x1234abcde...:1`
**API scope required** `credentials.read`. This enpoint will work also with a client side API key.
# Get Credential NFTs
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/credentials/retrieve-credential-nfts
api-reference/_open-api/VerifiableCredentialsAPIs.json GET /v1-alpha1/wallets/{walletLocator}/credential_nfts
Get only the NFTs in a wallet associated with credentials.
This walletLocator will have the format: `:`.
For example: `polygon:0x1234abcde...`
**API scope required** `credentials.read`.
# Revoke Credential
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/credentials/revoke-credential
api-reference/_open-api/VerifiableCredentialsAPIs.json DELETE /v1-alpha1/credentials/{id}
Revoke a verifiable credential by the credential ID.
This involves burning the associated nft.
This ID will have the format: `urn:uuid:`. For example: `urn:uuid:64f9877d-a19a-4205-8d61-f8c2abed5766`
**API scope required** `credentials.create`.
# Verify Credential
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/credentials/verify-credential
api-reference/_open-api/VerifiableCredentialsAPIs.json POST /v1-alpha1/credentials/verification/verify
Verify that a verifiable credential is valid.
**API scope required** `credentials.read`
It is impractical to use the API Playground to verify a credential. Instead you can copy the sample code for your
preferred language in the righthand sidebar and fill in the JSON of the credential you intend to verify.
Here is an example of how to verify a credential using javascript:
```javascript verifyVC.js theme={null}
const options = {
method: "POST",
headers: {
"X-API-KEY": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: '{"credential": {"id":"urn:uuid:e31316b6-3be1-49af-a95f-c7f4c3f52aa1","credentialSubject":{"course":"Blockchain 101","passed":true,"id":"did:polygon:0x6C3b3225759Cbda68F96378A9F0277B4374f9F06"},"expirationDate":"2034-02-03","nft":{"tokenId":"1","chain":"polygon","contractAddress":"0xdC444A3F4768185497Dae6250E2F348b99bE89F3"},"issuer":{"id":"did:polygon:0xa22CaDEdE67c11dc1444E507fDdd9b831a67aBd1"},"type":["VerifiableCredential","65c15243e9bee8deac219d57"],"issuanceDate":"2024-02-05T23:21:32.641Z","@context":["https://www.w3.org/2018/credentials/v1","https://github.com/haardikk21/ethereum-eip712-signature-2021-spec/blob/main/index.html","DUMMY_CROSSMINT/65c15243e9bee8deac219d57"],"proof":{"verificationMethod":"did:polygon:0xa22CaDEdE67c11dc1444E507fDdd9b831a67aBd1#ethereumAddress","ethereumAddress":null,"created":"2024-02-05T23:22:24.058Z","proofPurpose":"assertionMethod","type":"EthereumEip712Signature2021","proofValue":"0x748a55d9770cbc6ef16689f0d9547355e288bcce03a8949a32d2aac59244cb9e08cbf54c960bc00d133fc51e6651a4ac1366aeda320dd121777118a4e74980631b","eip712":{"domain":{"name":"Krebit","version":"0.1","chainId":4,"verifyingContract":"0xD8393a735e8b7B6E199db9A537cf27C61Aa74954"},"types":{"VerifiableCredential":[{"name":"@context","type":"string[]"},{"name":"type","type":"string[]"},{"name":"id","type":"string"},{"name":"issuer","type":"Issuer"},{"name":"credentialSubject","type":"CredentialSubject"},{"name":"issuanceDate","type":"string"},{"name":"expirationDate","type":"string"},{"name":"nft","type":"Nft"}],"CredentialSubject":[{"name":"id","type":"string"},{"name":"course","type":"string"},{"name":"passed","type":"bool"}],"Issuer":[{"name":"id","type":"string"}],"Nft":[{"name":"tokenId","type":"string"},{"name":"contractAddress","type":"string"},{"name":"chain","type":"string"}]},"primaryType":"VerifiableCredential"}}}}',
};
// note the gigantic VC JSON string in the body property
fetch("https://staging.crossmint.com/api/v1-alpha1/credentials/verification/verify", options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
```json 200 valid theme={null}
{
"isValid": true,
"error": null
}
```
```json 200 revoked theme={null}
{
"isValid": false,
"error": "Credential Revoked"
}
```
```json 200 expired theme={null}
{
"isValid": false,
"error": "Credential expired at "
}
```
```json 200 invalid_proof theme={null}
{
"isValid": false,
"error": "Invalid proof"
}
```
# Create Credential template
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/templates/create-template
api-reference/_open-api/VerifiableCredentialsAPIs.json POST /v1-alpha1/credentials/templates/
Create a template, similar to an NFT collection, for issuing verifiable credentials.
**API scope required** `credentials:template.create`
# Create Credential Type with Name
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/types/create-named-type
api-reference/_open-api/VerifiableCredentialsAPIs.json PUT /v1-alpha1/credentials/types/{typeName}
Create or import a type with a given name. This is how you define a custom credential schema.
**API scope required** `credentials.create`
# Create Credential Type
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/types/create-type
api-reference/_open-api/VerifiableCredentialsAPIs.json POST /v1-alpha1/credentials/types
Create or import a credential type with a random UUID. This is how you define a custom credential schema.
**API scope required** `credentials.create`
# Get a Credential Type
Source: https://docs.crossmint.com/api-reference/verifiable-credentials/types/get-type
api-reference/_open-api/VerifiableCredentialsAPIs.json GET /v1-alpha1/credentials/types/{typeName}
Get the schema of a given type by name (or id)
**API scope required** `credentials.read`
# Add Recovery Method
Source: https://docs.crossmint.com/api-reference/wallets/add-recovery-method
api-reference/_open-api/WalletsV1Alpha2.json POST /2025-06-09/wallets/{walletLocator}/recovery-methods
Add a recovery method to a wallet.
# Approve Signature
Source: https://docs.crossmint.com/api-reference/wallets/approve-signature
api-reference/_open-api/WalletsV1Alpha2.json POST /2025-06-09/wallets/{walletLocator}/signatures/{signatureId}/approvals
Submit approval for a signature to sign a message or typed data.
**API scope required**: `wallets:signatures.create`
# Approve Transaction
Source: https://docs.crossmint.com/api-reference/wallets/approve-transaction
api-reference/_open-api/WalletsV1Alpha2.json POST /2025-06-09/wallets/{walletLocator}/transactions/{transactionId}/approvals
Submit approval signature for a pending transaction. Required for transactions using external signers.
**API scope required**: `wallets:transactions.sign`
# Create Signature
Source: https://docs.crossmint.com/api-reference/wallets/create-signature
api-reference/_open-api/WalletsV1Alpha2.json POST /2025-06-09/wallets/{walletLocator}/signatures
Creates a new signature for signing messages or typed data.
**API scope required**: `wallets:signatures.create`
# Create Transaction
Source: https://docs.crossmint.com/api-reference/wallets/create-transaction
api-reference/_open-api/WalletsV1Alpha2.json POST /2025-06-09/wallets/{walletLocator}/transactions
Creates a new transaction for the specified wallet. Transaction will be automatically broadcast once it has all necessary approvals.
**API scope required**: `wallets:transactions.create`
# Create Wallet
Source: https://docs.crossmint.com/api-reference/wallets/create-wallet
api-reference/_open-api/WalletsV1Alpha2.json POST /2025-06-09/wallets
Creates a new wallet of specified type. If called with an idempotency key or for a user who already has a wallet, returns existing wallet. When owner is provided, subsequent calls with the same owner will return the existing wallet. Supports both custodial and non-custodial wallet types.
**API scope required**: `wallets.create`
# Fund Wallet
Source: https://docs.crossmint.com/api-reference/wallets/fund-wallet
api-reference/_open-api/WalletsV1Alpha2.json POST /v1-alpha2/wallets/{walletLocator}/balances
Send funds to a wallet.
**API scope required**: `wallets.fund`
This endpoint is only available in **staging** and only supports **USDXM**.
# Get NFTs from Wallet
Source: https://docs.crossmint.com/api-reference/wallets/get-nfts-from-wallet
api-reference/_open-api/WalletsAPIs.json GET /2022-06-09/wallets/{identifier}/nfts
Fetch the NFTs in a provided wallet
**API scope required**: `wallets:nfts.read`
This API enables fetching the NFTs for a provided wallet address and chain.
The response will be slightly different between EVM, Solana, and other wallets. See the example responses to the right.
```json 200 EVM theme={null}
[
{
"chain": "",
"contractAddress": "",
"tokenId": "",
"metadata": {
"attributes": [],
"collection": {},
"description": "",
"image": "",
"animation_url": "",
"name": ""
},
"locator": "",
"tokenStandard": ""
}
]
```
```json 200 EVM (with subscription) theme={null}
[
{
"chain": "",
"contractAddress": "",
"tokenId": "",
"metadata": {
"attributes": [],
"collection": {},
"description": "",
"image": "",
"animation_url": "",
"name": ""
},
"locator": "",
"tokenStandard": "",
"subscription": {
"expiresAt": ""
}
}
]
```
```json 200 Solana theme={null}
[
{
"chain": "",
"mintHash": "",
"metadata": {
"name": "",
"description": "",
"image": "",
"attributes": []
},
"locator": ""
}
]
```
```json 400 theme={null}
{
"error": ""
}
```
# Get Signature
Source: https://docs.crossmint.com/api-reference/wallets/get-signature
api-reference/_open-api/WalletsV1Alpha2.json GET /2025-06-09/wallets/{walletLocator}/signatures/{signatureId}
Retrieves details about a specific signature by its ID.
**API scope required**: `wallets:signatures.read`
# Get All Signatures
Source: https://docs.crossmint.com/api-reference/wallets/get-signatures
api-reference/_open-api/WalletsV1Alpha2.json GET /2025-06-09/wallets/{walletLocator}/signatures
Retrieves all signatures associated with the specified wallet.
**API scope required**: `wallets:signatures.read`
# Get Delegated Signer
Source: https://docs.crossmint.com/api-reference/wallets/get-signer
api-reference/_open-api/WalletsV1Alpha2.json GET /2025-06-09/wallets/{walletLocator}/signers/{signer}
Retrieve details about a specific delegated signer by its locator.
**API scope required**: `wallets.read`
# Get Transaction
Source: https://docs.crossmint.com/api-reference/wallets/get-transaction
api-reference/_open-api/WalletsV1Alpha2.json GET /2025-06-09/wallets/{walletLocator}/transactions/{transactionId}
Retrieves the current status and details of a specific transaction.
**API scope required**: `wallets:transactions.read`
# Get Wallet Transactions
Source: https://docs.crossmint.com/api-reference/wallets/get-transactions
api-reference/_open-api/WalletsV1Alpha2.json GET /2025-06-09/wallets/{walletLocator}/transactions
Retrieves all transactions associated with the specified wallet. Optionally filter by date range using ISO 8601 format (e.g., 2025-10-27T00:00:00Z).
**API scope required**: `wallets:transactions.read`
# Get Transfer Transaction
Source: https://docs.crossmint.com/api-reference/wallets/get-transfer-token
api-reference/_open-api/WalletsV1Alpha2.json GET /2025-06-09/wallets/{walletLocator}/tokens/{tokenLocator}/transfers/{transactionId}
Retrieves the current status and details of a specific transfer transaction.
**API scope required**: `wallets:transactions.read`
# Get Wallet Balance
Source: https://docs.crossmint.com/api-reference/wallets/get-wallet-balance
api-reference/_open-api/WalletsV1Alpha2.json GET /2025-06-09/wallets/{walletLocator}/balances
Get the balance of a wallet for a given chain and currency
**API scope required**: `wallets:balance.read`
## Example Request
This example uses placeholder values. Modify the call with the wallet locator, tokens, and chains you would like to use.
```bash cURL theme={null}
curl --request GET \
--url 'https://staging.crossmint.com/api/2025-06-09/wallets/email:user@example.com:evm/balances?tokens=usdc,eth,pol&chains=base-sepolia,polygon-amoy' \
--header 'X-API-KEY: '
```
```js Node.js theme={null}
const url =
"https://staging.crossmint.com/api/2025-06-09/wallets/email:user@example.com:evm/balances?tokens=usdc,eth,pol&chains=base-sepolia,polygon-amoy";
const options = { method: "GET", headers: { "X-API-KEY": "" }, body: undefined };
try {
const response = await fetch(url, options);
const data = await response.json();
// Process each token's balance across chains
data.forEach((token) => {
console.log(`${token.symbol}${token.name ? ` (${token.name})` : ""}:`);
console.log(` Total amount: ${token.amount} (${token.rawAmount} raw)`);
console.log(` Decimals: ${token.decimals}`);
// Access balance data for each chain
Object.entries(token.chains).forEach(([chainName, chainBalance]) => {
console.log(` ${chainName}: ${chainBalance.amount}`);
console.log(` Locator: ${chainBalance.locator}`);
if (chainBalance.contractAddress) {
console.log(` Contract: ${chainBalance.contractAddress}`);
} else if (chainBalance.mintHash) {
console.log(` Mint Hash: ${chainBalance.mintHash}`);
} else if (chainBalance.contractId) {
console.log(` Contract ID: ${chainBalance.contractId}`);
}
});
console.log();
});
} catch (error) {
console.error(error);
}
```
```python Python theme={null}
import requests
url = "https://staging.crossmint.com/api/2025-06-09/wallets/email:user@example.com:evm/balances"
querystring = { "tokens": "usdc,eth,pol", "chains": "base-sepolia,polygon-amoy" }
headers = { "X-API-KEY": "" }
response = requests.get(url, params=querystring, headers=headers)
data = response.json()
# Process each token's balance across chains
for token in data:
name_part = f" ({token['name']})" if token.get('name') else ""
print(f"{token['symbol']}{name_part}:")
print(f" Total amount: {token['amount']} ({token['rawAmount']} raw)")
print(f" Decimals: {token['decimals']}")
# Access balance data for each chain
for chain_name, chain_balance in token['chains'].items():
print(f" {chain_name}: {chain_balance['amount']}")
print(f" Locator: {chain_balance['locator']}")
if 'contractAddress' in chain_balance:
print(f" Contract: {chain_balance['contractAddress']}")
elif 'mintHash' in chain_balance:
print(f" Mint Hash: {chain_balance['mintHash']}")
elif 'contractId' in chain_balance:
print(f" Contract ID: {chain_balance['contractId']}")
print()
```
## Example Response
The response returns an array of token objects, each containing balance information across the specified chains:
```json theme={null}
[
{
"symbol": "ETH",
"name": "Ethereum",
"decimals": 18,
"amount": "1.5",
"rawAmount": "1500000000000000000",
"chains": {
"base-sepolia": {
"locator": "base-sepolia:eth",
"amount": "1.0",
"rawAmount": "1000000000000000000"
},
"polygon-amoy": {
"locator": "polygon-amoy:eth",
"amount": "0.5",
"rawAmount": "500000000000000000"
}
}
},
{
"symbol": "USDC",
"name": "USD Coin",
"decimals": 6,
"amount": "100.0",
"rawAmount": "100000000",
"chains": {
"base-sepolia": {
"locator": "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"amount": "50.0",
"rawAmount": "50000000",
"contractAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
},
"polygon-amoy": {
"locator": "polygon-amoy:0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582",
"amount": "50.0",
"rawAmount": "50000000",
"contractAddress": "0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582"
}
}
},
{
"symbol": "SOL",
"decimals": 9,
"amount": "2.5",
"rawAmount": "2500000000",
"chains": {
"solana": {
"locator": "solana:sol",
"amount": "2.5",
"rawAmount": "2500000000"
}
}
},
{
"symbol": "USDC",
"name": "USD Coin",
"decimals": 6,
"amount": "75.0",
"rawAmount": "75000000",
"chains": {
"solana": {
"locator": "solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount": "75.0",
"rawAmount": "75000000",
"mintHash": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
}
}
}
]
```
# Get Wallet By Locator
Source: https://docs.crossmint.com/api-reference/wallets/get-wallet-by-locator
api-reference/_open-api/WalletsV1Alpha2.json GET /2025-06-09/wallets/{walletLocator}
Retrieves a wallet by its locator (address or user identifier and wallet type)
**API scope required**: `wallets.read`
# List Wallet Transfers
Source: https://docs.crossmint.com/api-reference/wallets/list-transfers
api-reference/_open-api/WalletsV1Alpha2.json GET /unstable/wallets/{walletLocator}/transfers
Retrieves activity history for the specified wallet including transactions and other relevant events. This is an unstable API that may change without notice.
**API scope required**: `wallets.read`
This is an **unstable API** that may change without notice. The `unstable` prefix indicates that breaking changes may occur in future releases.
# Create Delegated Signer
Source: https://docs.crossmint.com/api-reference/wallets/register-delegated-key
api-reference/_open-api/WalletsV1Alpha2.json POST /2025-06-09/wallets/{walletLocator}/signers
Create a delegated signer for a smart wallet with optional restrictions around permissions and expiry date.
**API scope required**: `wallets.create`
# Remove Delegated Signer
Source: https://docs.crossmint.com/api-reference/wallets/remove-delegated-key
api-reference/_open-api/WalletsV1Alpha2.json DELETE /2025-06-09/wallets/{walletLocator}/signers/{signer}
Remove an operational signer from a smart wallet.
**API scope required**: `wallets.create`
# Remove Recovery Method
Source: https://docs.crossmint.com/api-reference/wallets/remove-recovery-method
api-reference/_open-api/WalletsV1Alpha2.json DELETE /2025-06-09/wallets/{walletLocator}/recovery-methods/{signer}
Remove one recovery method from a wallet.
# Transfer Token
Source: https://docs.crossmint.com/api-reference/wallets/transfer-token
api-reference/_open-api/WalletsV1Alpha2.json POST /2025-06-09/wallets/{walletLocator}/tokens/{tokenLocator}/transfers
Sends a token of any type from this wallet to a recipient
# Customization
Source: https://docs.crossmint.com/authentication/customization
Customize the authentication flow and email templates
With Crossmint Auth, you can customize the authentication flow and email templates with your brand's identity.
## Modal Customization
The `CrossmintAuthProvider` component allows you to customize the authentication modal appearance and content.
```tsx theme={null}
import { CrossmintAuthProvider } from "@crossmint/client-sdk-react-ui";
By continuing, you agree to our Terms of Service and{" "}
Privacy Policy
}
appearance={{
spacingUnit: "8px",
borderRadius: "12px",
colors: {
inputBackground: "#fffdf9",
buttonBackground: "#fffaf2",
border: "#835911",
background: "#FAF5EC",
textPrimary: "#5f2c1b",
textSecondary: "#835911",
textLink: "#1400cb",
danger: "#ff3333",
accent: "#602C1B",
},
}}
>
{children}
```
### Modal props
| Prop | Type | Description |
| - | - | - |
| `authModalTitle` | `string` | Custom title displayed at the top of the authentication modal |
| `termsOfServiceText` | `string \| ReactNode` | Custom terms of service text displayed below the authentication form |
| `appearance` | `UIConfig` | Styling configuration for the modal (see example above) |
The modal will also use your display name configured in the Crossmint Console. To learn more, see the "Email Customization" section below.
## Embedded Login
For a more integrated experience, you can use the embedded login component, which offers flexibility to display the login form in your own modal or as part of a split login screen.
```tsx theme={null}
import { EmbeddedAuthForm, useAuth } from "@crossmint/client-sdk-react-ui";
export default function Home() {
const { user, status } = useAuth(); // "in-progress" | "logged-in" | "logged-out"
return
{status === "logged-in" ? user?.email : }
;
}
```
## Email Customization
Email OTP is a login method that allows users to sign in to your app using their email address. They receive a one-time code via email that they can use to log in.
You can customize the email template to align with your brand's identity. Crossmint strongly recommends doing so, as it increases user trust and security.
To modify the email template:
1. In the Crossmint Console, click on Settings, and navigate to the **Branding** tab.
2. Here, you can customize:
* The **logo** displayed in the email with your logo.
* The **display name** textbox to include your brand's name.
When customizing email text, avoid using terms like "airdrop", "token", or "crypto" as these can trigger spam
filters and hurt email deliverability.
# Introduction
Source: https://docs.crossmint.com/authentication/introduction
Authenticate users and register customer identities for regulated activities
Crossmint provides two complementary capabilities for managing your users:
* **Identity** — Link a `userLocator` to additional customer information (KYC data, documents) required for regulated activities such as onramp, offramp, and payouts.
* **Auth** — Link a `userLocator` to an authentication system (Crossmint's built-in auth or your own). This lets users sign in, authorize API calls, and access Crossmint products.
Choose the path that matches your needs, or use both together.
# Login Methods
Source: https://docs.crossmint.com/authentication/login-methods
Customize how users can log in to your app
Crossmint Auth supports the following login methods:
* **Email OTP**: passwordless sign-in using a one time code delivered to the user's email.
* **Social Accounts**: Sign in with Google, X, Farcaster, and more.
* **External wallets**: connect with crypto wallets for Web3 authentication. Use `"web3"` for all wallets, `"web3:evm-only"` for Ethereum-compatible wallets only, or `"web3:solana-only"` for Solana wallets only.
To customize which login methods are shown to your users, use the `loginMethods` prop when initializing Crossmint Auth.
By default, only email and Google are enabled.
```tsx app/providers/Providers.tsx theme={null}
"use client";
import { CrossmintProvider, CrossmintAuthProvider } from "@crossmint/client-sdk-react-ui";
export default function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
# Overview
Source: https://docs.crossmint.com/authentication/overview
Authenticate users into your app with Crossmint Auth or your own provider
Crossmint Auth is a quick-to-integrate authentication solution, ideal for staging environments, demos, and prototyping. It lets you authenticate users using web3 or traditional sign-in methods:
* **Email OTP**: passwordless sign-in using a one time code delivered to the user's email.
* **Social Accounts**: Sign in with Google, Apple, X, and more.
* **Farcaster**: using the [Sign In With Farcaster (SIWF) standard](https://github.com/farcasterxyz/protocol/discussions/110).
* **External wallets**: connect with crypto wallets for Web3 authentication.
### When to Use Crossmint Auth
| Scenario | Recommendation |
| - | - |
| **Staging / prototyping** | Use Crossmint Auth. Get up and running in minutes |
| **Demos and quickstarts** | Use Crossmint Auth. Minimal setup, great for testing |
| **Production applications** | [Bring your own auth provider](/wallets/guides/bring-your-own-auth) for full control over user management |
### Key Characteristics
Authorize calls to your backend services and any Crossmint product.
Unified identities across your backend and the blockchain.
Launch your app in under 5 minutes.
### Get Started with Auth
Get started with Crossmint Auth in a React web app in under 5 minutes.
Get started with Crossmint Auth in an Expo app in under 5 minutes.
Recommended for production. Use Auth0, Firebase, Supabase, or any JWT-based provider with Crossmint.
Detailed wallet creation guide.
Get and update user information.
Contact our sales team for advanced support.
# Flutter
Source: https://docs.crossmint.com/authentication/quickstarts/flutter
Authenticate users in your Flutter app in under 5 minutes
Crossmint Auth on Flutter is **headless** — there is no pre-built login modal.
You build your own UI and call the SDK directly. This guide walks through both
**email OTP** and **OAuth** login flows. Optional pre-built widgets are available
via `crossmint_flutter_ui.dart`.
See a full working example with auth and wallets.
Install the Crossmint Flutter SDK:
The SDK bundles the following dependencies:
| Package | Purpose |
| - | - |
| `flutter_secure_storage` | Encrypted token storage (Keychain on iOS, Keystore on Android) |
| `url_launcher` | Opens the OAuth consent screen in a system browser |
| `app_links` | Listens for OAuth deep-link redirects back to your app |
OAuth redirects require a **deep link scheme** so the browser can return to your app after login.
**Android** — add to your `AndroidManifest.xml`:
```xml android/app/src/main/AndroidManifest.xml theme={null}
```
**iOS** — add to your `Info.plist`:
```xml ios/Runner/Info.plist theme={null}
CFBundleURLTypesCFBundleURLSchemesmyapp
```
Pass the same scheme to `CrossmintClientConfig.appScheme`.
Initialize `CrossmintClient` and restore any persisted session on app launch.
```dart lib/crossmint.dart theme={null}
import 'package:crossmint_flutter/crossmint_client.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
/// Shared Crossmint client instance — import this file from your screens.
final client = CrossmintClient(
config: CrossmintClientConfig(
apiKey: const String.fromEnvironment('CROSSMINT_API_KEY'),
appScheme: 'myapp',
),
);
```
```dart main.dart theme={null}
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
import 'crossmint.dart'; // shared client instance
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await client.initialize();
await client.auth.restoreSession();
// Start the OAuth callback router
final authRouter = CrossmintAuthCallbackRouter(auth: client.auth);
await authRouter.start();
runApp(const MyApp());
}
```
Pass the API key via `--dart-define` when running the app:
```bash theme={null}
flutter run --dart-define=CROSSMINT_API_KEY=YOUR_CLIENT_API_KEY
```
Use the `CrossmintAuthClient` to implement email OTP and OAuth login flows.
### Email OTP Login
```dart login_screen.dart theme={null}
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
import 'crossmint.dart'; // shared client instance
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State createState() => _LoginScreenState();
}
class _LoginScreenState extends State {
final _emailController = TextEditingController();
final _otpController = TextEditingController();
String? _emailId;
bool _otpSent = false;
bool _isPending = false;
Future _sendOtp() async {
final email = _emailController.text.trim();
if (email.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enter a valid email address')),
);
return;
}
setState(() => _isPending = true);
try {
final challenge = await client.auth.sendEmailOtp(email);
setState(() {
_emailId = challenge.id;
_otpSent = true;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to send OTP: $e')),
);
} finally {
setState(() => _isPending = false);
}
}
Future _verifyOtp() async {
final otp = _otpController.text.trim();
if (otp.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Enter the OTP code')),
);
return;
}
setState(() => _isPending = true);
try {
await client.auth.confirmEmailOtp(
email: _emailController.text.trim(),
emailId: _emailId!,
token: otp,
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Invalid OTP: $e')),
);
} finally {
setState(() => _isPending = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _emailController,
decoration: const InputDecoration(
hintText: 'Enter your email',
border: OutlineInputBorder(),
),
enabled: !_otpSent,
),
const SizedBox(height: 10),
if (!_otpSent)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isPending ? null : _sendOtp,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF05B959),
),
child: _isPending
? const SizedBox(
height: 20, width: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2,
),
)
: const Text('Send OTP',
style: TextStyle(color: Colors.white)),
),
)
else ...[
TextField(
controller: _otpController,
decoration: const InputDecoration(
hintText: 'Enter OTP code',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isPending ? null : _verifyOtp,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF05B959),
),
child: _isPending
? const SizedBox(
height: 20, width: 20,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2,
),
)
: const Text('Verify OTP',
style: TextStyle(color: Colors.white)),
),
),
],
],
),
),
);
}
@override
void dispose() {
_emailController.dispose();
_otpController.dispose();
super.dispose();
}
}
```
### OAuth Login (Google, Twitter)
```dart oauth_button.dart theme={null}
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
import 'crossmint.dart'; // shared client instance
class GoogleSignInButton extends StatelessWidget {
const GoogleSignInButton({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => client.auth.loginWithOAuth(
CrossmintOAuthProvider.google,
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4285F4),
),
child: const Text(
'Sign in with Google',
style: TextStyle(color: Colors.white),
),
),
);
}
}
```
The SDK handles platform differences automatically via `CrossmintAuthCallbackRouter`:
* **iOS**: Uses `url_launcher` to open an in-app authentication session. The redirect URL is
captured by the auth callback router via `app_links`.
* **Android**: Opens the system browser. The OAuth provider redirects back to your app via the
deep link scheme configured in `AndroidManifest.xml`.
The `CrossmintAuthCallbackRouter` listens for incoming deep links on both platforms and automatically
completes the OAuth flow — no manual URL handling required.
```dart home_screen.dart theme={null}
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_auth.dart';
import 'crossmint.dart'; // shared client instance
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableBuilder(
valueListenable: client.auth.stateListenable,
builder: (context, state, _) {
final user = state.user;
if (user == null) return const LoginScreen();
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Welcome!',
style: TextStyle(fontSize: 20)),
TextButton(
onPressed: () => client.auth.logout(),
child: const Text('Logout',
style: TextStyle(color: Colors.red)),
),
],
),
const SizedBox(height: 20),
Text('Email: ${user.email}'),
Text('User ID: ${user.id}'),
],
),
);
},
),
);
}
}
```
## Auth State Values
The `CrossmintAuthState` provides the following status information:
| Status | Description |
| - | - |
| `initializing` | The SDK is loading stored tokens and checking session validity |
| `loggedOut` | No active session. Show your login screen |
| `inProgress` | An OAuth or OTP flow is currently in progress |
| `loggedIn` | The user is authenticated. `user` and `jwt` are available |
## Token Storage
By default, the SDK stores authentication tokens using `flutter_secure_storage`, which encrypts data
at rest using the platform keychain (iOS) or keystore (Android). Tokens persist across app restarts
and are protected by the device's hardware-backed security.
You can provide a custom storage implementation via `CrossmintClientConfig.authStorage` if you need
a different storage backend. The SDK also provides `InMemoryCrossmintAuthStorage` for testing.
## Differences from the React Native SDK
| Feature | React Native | Flutter |
| - | - | - |
| **Login UI** | Headless — build your own UI | Headless — build your own UI (optional pre-built widgets available) |
| **State management** | React hooks (`useCrossmintAuth`) | `ChangeNotifier` / `ValueListenable` |
| **OAuth flow** | System browser via `expo-web-browser` | System browser via `url_launcher` |
| **OAuth callback** | `Linking.useURL()` + `createAuthSession()` | `CrossmintAuthCallbackRouter` (automatic) |
| **Token storage** | `expo-secure-store` (encrypted) | `flutter_secure_storage` (encrypted) |
| **Package** | `@crossmint/client-sdk-react-native-ui` | `crossmint_flutter` |
| **Deep link setup** | `scheme` in `app.json` | `AndroidManifest.xml` + `Info.plist` |
| **OTP prompts** | Manual via `useWalletOtpSigner` hook | Automatic via `CrossmintOtpSignerListener` widget |
## Moving to Production
**Crossmint Auth is designed for staging and getting started quickly.** For production applications,
Crossmint strongly recommends migrating to your own authentication provider for full control
over user management.
When you are ready to go to production, Crossmint recommends:
1. **Set up your own auth provider** (Auth0, Firebase, Supabase, Stytch, etc.) and follow the [Bring Your Own Auth guide](/wallets/guides/bring-your-own-auth) to integrate it with Crossmint via JWT.
2. Create a developer account on the production console.
3. Create a production client API key on the API Keys page with the API scopes `users.create`, `users.read`, `wallets.read`.
4. [Configure JWT authentication](/introduction/platform/api-keys/jwt-authentication) for your auth provider in the Crossmint Console.
## Next Steps
Install and configure the Flutter SDK, then explore the full API reference
Recommended for production — integrate your own auth provider via JWT
Get notified when a user signs up or updates their profile
Provision wallets automatically when users sign up
# React
Source: https://docs.crossmint.com/authentication/quickstarts/react
Sign up your first user in 5 minutes with React
See a full working example with auth and wallets.
Run the following command to install the SDK:
Add the necessary Crossmint providers to your app.
```tsx next.js theme={null}
"use client";
import {
CrossmintProvider,
CrossmintAuthProvider,
} from "@crossmint/client-sdk-react-ui";
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
```tsx next.js theme={null}
"use client";
import { useAuth } from "@crossmint/client-sdk-react-ui";
export function AuthButton() {
const { login, logout, user, jwt } = useAuth();
return (
);
}
```
## Moving to Production
**Crossmint Auth is designed for staging and getting started quickly.** For production applications, Crossmint strongly recommends migrating to your own authentication provider for full control over user management.
When you are ready to go to production, Crossmint recommends:
1. **Set up your own auth provider** (Auth0, Firebase, Supabase, Stytch, etc.) and follow the [Bring Your Own Auth guide](/wallets/guides/bring-your-own-auth) to integrate it with Crossmint via JWT.
2. Create a developer account on the production console.
3. Create a production client API key on the API Keys page with the API scopes `users.create`, `users.read`, `wallets.read`.
4. [Configure JWT authentication](/introduction/platform/api-keys/jwt-authentication) for your auth provider in the Crossmint Console.
## Next Steps
Recommended for production — integrate your own auth provider via JWT
Get notified when a user signs up or updates their profile
Provision wallets automatically when users sign up
# React Native
Source: https://docs.crossmint.com/authentication/quickstarts/react-native
Authenticate users in your Expo app in under 5 minutes
Crossmint Auth on React Native is **headless** — there is no pre-built login modal.
You build your own UI and call the SDK hooks directly. This guide walks through both
**email OTP** and **OAuth** login flows.
See a full working example with auth and wallets.
Install the Crossmint React Native SDK and its required Expo peer dependencies:
The SDK requires the following Expo packages:
| Package | Purpose |
| - | - |
| `expo-secure-store` | Encrypted token storage (Keychain on iOS, Keystore on Android) |
| `expo-web-browser` | Opens the OAuth consent screen in a system browser |
| `expo-device` | Detects device capabilities for secure storage availability |
| `expo-linking` | Listens for OAuth deep-link redirects back to your app |
OAuth redirects require a **deep link scheme** so the browser can return to your app after login.
Add a `scheme` to your `app.json` (or `app.config.ts`):
```json app.json theme={null}
{
"expo": {
"scheme": "myapp"
}
}
```
**Expo Go** uses a different scheme at runtime (`exp://127.0.0.1:8081`). The SDK
detects Expo Go automatically and adjusts the redirect URI, so OAuth works during
development without extra configuration. For production builds, the `scheme` value
above is used.
Wrap your application with `CrossmintProvider` and `CrossmintAuthProvider`.
```tsx providers.tsx theme={null}
import {
CrossmintProvider,
CrossmintAuthProvider,
} from "@crossmint/client-sdk-react-native-ui";
type ProvidersProps = {
children: React.ReactNode;
};
export default function Providers({ children }: ProvidersProps) {
return (
{children}
);
}
```
| Prop | Type | Description |
| - | - | - |
| `appSchema` | `string \| string[]` | Override the deep link scheme. Defaults to the `scheme` in your Expo config. |
| `storageProvider` | `StorageProvider` | Custom token storage implementation. Defaults to `expo-secure-store`. |
| `onLoginSuccess` | `(user) => void` | Callback fired after a successful login. |
Use the `useCrossmintAuth` hook to access auth functions. Unlike the React web SDK, there is no
pre-built modal — you call `loginWithOAuth` and `crossmintAuth.sendEmailOtp` / `crossmintAuth.confirmEmailOtp` directly.
### Email OTP Login
```tsx LoginScreen.tsx theme={null}
import React, { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
ActivityIndicator,
Alert,
} from "react-native";
import { useCrossmintAuth } from "@crossmint/client-sdk-react-native-ui";
export default function LoginScreen() {
const { crossmintAuth, createAuthSession, status } =
useCrossmintAuth();
const [email, setEmail] = useState("");
const [emailId, setEmailId] = useState("");
const [otp, setOtp] = useState("");
const [otpSent, setOtpSent] = useState(false);
const [isPending, setIsPending] = useState(false);
const sendOtp = async () => {
if (!email.trim()) {
Alert.alert("Error", "Enter a valid email address");
return;
}
setIsPending(true);
try {
const res =
await crossmintAuth?.sendEmailOtp(email);
if (res == null) {
throw new Error("Auth client not ready");
}
setEmailId(res.emailId);
setOtpSent(true);
} catch (error) {
const message =
error instanceof Error
? error.message
: "Failed to send OTP.";
Alert.alert("Error", message);
} finally {
setIsPending(false);
}
};
const verifyOtp = async () => {
if (!otp.trim()) {
Alert.alert("Error", "Enter the OTP code");
return;
}
setIsPending(true);
try {
const oneTimeSecret =
await crossmintAuth?.confirmEmailOtp(
email,
emailId,
otp
);
if (oneTimeSecret == null) {
throw new Error("Auth client not ready");
}
await createAuthSession(oneTimeSecret);
} catch (error) {
const message =
error instanceof Error
? error.message
: "Invalid OTP. Try again.";
Alert.alert("Error", message);
} finally {
setIsPending(false);
}
};
if (status === "initializing") {
return (
);
}
return (
{!otpSent ? (
{isPending ? (
) : (
Send OTP
)}
) : (
<>
{isPending ? (
) : (
Verify OTP
)}
>
)}
);
}
```
### OAuth Login (Google, Twitter)
```tsx OAuthButton.tsx theme={null}
import React, { useEffect } from "react";
import { TouchableOpacity, Text } from "react-native";
import { useCrossmintAuth } from "@crossmint/client-sdk-react-native-ui";
import * as Linking from "expo-linking";
export default function OAuthButton() {
const { loginWithOAuth, createAuthSession } =
useCrossmintAuth();
const url = Linking.useURL();
useEffect(() => {
if (url != null) {
createAuthSession(url);
}
}, [url, createAuthSession]);
return (
loginWithOAuth("google")}
>
Sign in with Google
);
}
```
The SDK handles platform differences automatically:
* **iOS**: Uses `WebBrowser.openAuthSessionAsync`, which opens an in-app authentication
session (ASWebAuthenticationSession). The redirect URL is intercepted by the authentication
session before it reaches your app's URL routing, so `Linking.useURL()` remains `null` after
the OAuth flow. There is no double invocation — `createAuthSession` is called only once by
the SDK internally.
* **Android**: Uses `WebBrowser.openBrowserAsync`, which opens the system browser. The OAuth
provider redirects back to your app via the deep link scheme. You must listen for the
incoming URL with `Linking.useURL()` and pass it to `createAuthSession` yourself, as shown
in the example above.
The `Linking.useURL()` + `createAuthSession` pattern works on both platforms, so a single
code path is sufficient.
```tsx HomeScreen.tsx theme={null}
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { useCrossmintAuth } from "@crossmint/client-sdk-react-native-ui";
export default function HomeScreen() {
const { user, jwt, logout, status } = useCrossmintAuth();
return (
Welcome!LogoutEmail: {user?.email}User ID: {user?.id}Status: {status}JWT: {jwt ? "Present" : "None"}
);
}
```
## Auth Status Values
The `status` field returned by `useCrossmintAuth` can be one of the following:
| Status | Description |
| - | - |
| `"initializing"` | The SDK is loading stored tokens and checking session validity |
| `"logged-out"` | No active session. Show your login screen |
| `"in-progress"` | An OAuth or OTP flow is currently in progress |
| `"logged-in"` | The user is authenticated. `user` and `jwt` are available |
## Token Storage
By default, the SDK stores authentication tokens using `expo-secure-store`, which encrypts data
at rest using the platform keychain (iOS) or keystore (Android). This means tokens persist
across app restarts and are protected by the device's hardware-backed security.
You can provide a custom storage implementation via the `storageProvider` prop on
`CrossmintAuthProvider` if you need a different storage backend.
## Differences from the React Web SDK
| Feature | React (Web) | React Native |
| - | - | - |
| **Login UI** | Pre-built modal via `login()` | Headless — build your own UI |
| **Auth hook** | `useAuth()` | `useCrossmintAuth()` |
| **OAuth flow** | Popup / redirect in browser | System browser via `expo-web-browser` |
| **OAuth callback** | Handled automatically | Listen with `Linking.useURL()` + `createAuthSession()` |
| **Token storage** | Browser `localStorage` / cookies | `expo-secure-store` (encrypted) |
| **Package** | `@crossmint/client-sdk-react-ui` | `@crossmint/client-sdk-react-native-ui` |
| **Deep link scheme** | Not required | Required in `app.json` for OAuth |
| **Email OTP** | Handled by modal | Call `sendEmailOtp` / `confirmEmailOtp` manually |
## Moving to Production
**Crossmint Auth is designed for staging and getting started quickly.** For production applications,
Crossmint strongly recommends migrating to your own authentication provider for full control
over user management.
When you are ready to go to production, Crossmint recommends:
1. **Set up your own auth provider** (Auth0, Firebase, Supabase, Stytch, etc.) and follow the [Bring Your Own Auth guide](/wallets/guides/bring-your-own-auth) to integrate it with Crossmint via JWT.
2. Create a developer account on the production console.
3. Create a production client API key on the API Keys page with the API scopes `users.create`, `users.read`, `wallets.read`.
4. [Configure JWT authentication](/introduction/platform/api-keys/jwt-authentication) for your auth provider in the Crossmint Console.
## Next Steps
* [Create Smart Wallets on sign up](/wallets/quickstarts/react-native) using the wallets React Native quickstart
* [Bring Your Own Auth](/wallets/guides/bring-your-own-auth), recommended for production
* Read and update [user information](/authentication/user-profile)
* Use [webhooks](/authentication/webhooks) to get notified when a user signs up
* [React Native SDK Reference](/sdk-reference/wallets/react-native/get-started) for the full API documentation
# Secure Cookies
Source: https://docs.crossmint.com/authentication/security
Learn how to securely store Crossmint Auth cookies in your application
Authentication tokens are accessible to client-side JavaScript by default through non-HttpOnly cookies. For stronger security, you can store tokens in HttpOnly cookies, which are accessible only on the server side. This setup requires custom routes for refreshing tokens and logging out, using utilities from `@crossmint/server-sdk`.
When initializing the server SDK, configure secure cookie options:
```typescript theme={null}
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
const crossmint = createCrossmint({ apiKey: process.env.SERVER_CROSSMINT_API_KEY });
const crossmintAuth = CrossmintAuth.from(crossmint, {
cookieOptions: {
httpOnly: true,
secure: true, // Only send cookies over HTTPS
domain: ".yourdomain.com", // Optional: specify cookie domain
},
});
```
**Note:** The `httpOnly` flag only applies to the refresh token. The session JWT remains accessible to client-side JavaScript since it's needed for API calls.
```typescript theme={null}
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
import { NextRequest } from "next/server";
const crossmint = createCrossmint({ apiKey: process.env.SERVER_CROSSMINT_API_KEY! });
const crossmintAuth = CrossmintAuth.from(crossmint);
export async function POST(request: NextRequest) {
return await crossmintAuth.handleCustomRefresh(request);
}
```
```typescript theme={null}
import express from "express";
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
const app = express()
const crossmint = createCrossmint({ apiKey: process.env.SERVER_CROSSMINT_API_KEY! });
const crossmintAuth = CrossmintAuth.from(crossmint);
app.post("/api/auth/refresh", async (req, res) => {
await crossmintAuth.handleCustomRefresh(req, res);
res.end();
});
```
```typescript theme={null}
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
import { NextRequest } from "next/server";
const crossmint = createCrossmint({ apiKey: process.env.SERVER_CROSSMINT_API_KEY! });
const crossmintAuth = CrossmintAuth.from(crossmint);
export async function POST(request: NextRequest) {
return await crossmintAuth.logout(request);
}
```
```typescript theme={null}
import express from "express";
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
const app = express()
const crossmint = createCrossmint({ apiKey: process.env.SERVER_CROSSMINT_API_KEY! });
const crossmintAuth = CrossmintAuth.from(crossmint);
app.post("/api/auth/logout", async (req, res) => {
await crossmintAuth.logout(req, res);
res.end();
});
```
Configure the client SDK to use your custom routes:
```typescript theme={null}
import { createCrossmint, CrossmintAuth } from "@crossmint/client-sdk-auth";
const crossmint = createCrossmint({ apiKey: process.env.NEXT_PUBLIC_CLIENT_CROSSMINT_API_KEY! });
const crossmintAuth = CrossmintAuth.from(crossmint, {
refreshRoute: "/api/auth/refresh",
logoutRoute: "/api/auth/logout"
});
```
```tsx theme={null}
import { CrossmintProvider, CrossmintAuthProvider } from "@crossmint/client-sdk-react-ui";
{children}
```
**Note:** Depending on the framework you're using, you might need to set the whole URL in the `refreshRoute` and `logoutRoute` options.
# Server-Side Rendering (SSR)
Source: https://docs.crossmint.com/authentication/ssr
Integrate Crossmint Auth on the server-side for user authentication and management
Crossmint Auth provides a flexible and simple authentication solution for your crypto server-side applications. This guide covers how to integrate and use Crossmint Auth across various server-side frameworks.
## Overview
The Crossmint server SDK allows you to:
* Manage user sessions
* Retrieve user profiles
* Verify JSON Web Tokens (JWTs)
## Installation
First, install the Crossmint Server SDK:
```bash npm theme={null}
npm i @crossmint/server-sdk
```
```bash yarn theme={null}
yarn add @crossmint/server-sdk
```
```bash pnpm theme={null}
pnpm add @crossmint/server-sdk
```
```bash bun theme={null}
bun add @crossmint/server-sdk
```
## Initialization
To use Crossmint Auth, you need to initialize it with your Server API key. This API requires the `users.read` scope.
```typescript theme={null}
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
const crossmint = createCrossmint({ apiKey: process.env.SERVER_CROSSMINT_API_KEY });
const crossmintAuth = CrossmintAuth.from(crossmint);
```
## Core Functionality
### Session Management
The `getSession` method validates or refreshes a user's session based on their JWT and refresh token.
```typescript theme={null}
const { jwt, refreshToken, userId } = await crossmintAuth.getSession(req, res);
```
This method:
1. Fetches the current JWT and refresh token from the cookies with keys `crossmint-jwt` and `crossmint-refresh-token`.
2. Checks if the current JWT is valid
3. Refreshes the session if needed
4. Stores the new JWT and refresh token in cookies
5. Returns new auth materials and the user ID
For other frameworks that do not expose standard request and response objects, such as Next.js using the App Router, you can pass in an object with `jwt` and `refreshToken` properties instead:
```typescript middleware.ts theme={null}
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
export async function middleware(request: NextRequest) {
// Skip middleware for API routes and static files
if (request.nextUrl.pathname.startsWith("/api") || request.nextUrl.pathname.startsWith("/_next")) {
return NextResponse.next();
}
const response = NextResponse.next();
const jwt = request.cookies.get("crossmint-jwt")?.value;
const refreshToken = request.cookies.get("crossmint-refresh-token")?.value;
if (refreshToken == null) {
return response;
}
try {
const crossmint = createCrossmint({
apiKey: process.env.SERVER_CROSSMINT_API_KEY || "",
});
const crossmintAuth = CrossmintAuth.from(crossmint);
const { jwt: newJwt, refreshToken: newRefreshToken } = await crossmintAuth.getSession({
jwt,
refreshToken,
});
// Only update response cookies if tokens have changed
if (newJwt !== jwt || newRefreshToken.secret !== refreshToken) {
response.cookies.set("crossmint-jwt", newJwt);
response.cookies.set("crossmint-refresh-token", newRefreshToken.secret);
}
} catch (_) {
// If auth fails, clear cookies and redirect to home
response.cookies.delete("crossmint-jwt");
response.cookies.delete("crossmint-refresh-token");
}
return response;
}
```
```typescript getAuthSession.ts theme={null}
import { cookies } from "next/headers";
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
export async function getAuthSession() {
const cookieStore = await cookies();
const jwt = cookieStore.get("crossmint-jwt")?.value;
const refreshToken = cookieStore.get("crossmint-refresh-token")?.value;
if (refreshToken == null) {
return;
}
try {
const crossmint = createCrossmint({
apiKey: process.env.SERVER_CROSSMINT_API_KEY || "",
});
const crossmintAuth = CrossmintAuth.from(crossmint);
const session = await crossmintAuth.getSession({
jwt,
refreshToken,
});
return session;
} catch (_) {
return;
}
}
```
```typescript page.tsx theme={null}
import { getAuthSession } from "@/hooks/auth";
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
export default async function ProtectedRoute() {
const session = await getAuthSession();
const userId = session?.userId;
if (userId != null) {
// Fetch user data or perform authorized actions
const crossmint = createCrossmint({
apiKey: process.env.SERVER_CROSSMINT_API_KEY || "",
});
const crossmintAuth = CrossmintAuth.from(crossmint);
const userData = await crossmintAuth.getUser(userId);
return
Welcome, {userData.email}!
;
}
// Handle unauthenticated state
return
Please log in to access this page.
;
}
```
For Express.js applications, you can create middleware to handle authentication:
```typescript theme={null}
import express from "express";
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
const app = express();
const crossmint = createCrossmint({
apiKey: process.env.SERVER_CROSSMINT_API_KEY || "",
});
const crossmintAuth = CrossmintAuth.from(crossmint);
const authMiddleware = async (req, res, next) => {
try {
const { jwt, refreshToken, userId } = await crossmintAuth.getSession(req, res);
req.user = { userId };
next();
} catch (error) {
res.status(401).json({ error: "Authentication failed" });
}
};
app.use(authMiddleware);
app.get("/protected", (req, res) => {
res.json({ message: "Protected route", userId: req.user.userId });
});
```
For a basic Node.js server:
```typescript theme={null}
import http from "http";
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
const crossmint = createCrossmint({
apiKey: process.env.SERVER_CROSSMINT_API_KEY || "",
});
const crossmintAuth = CrossmintAuth.from(crossmint);
const server = http.createServer(async (req, res) => {
if (req.url === "/protected") {
try {
const { userId } = await crossmintAuth.getSession(req, res);
const userData = await crossmintAuth.getUser(userId);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Authenticated", user: userData }));
} catch (error) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Authentication failed" }));
}
} else {
res.writeHead(404);
res.end();
}
});
server.listen(3000);
```
### User Profile Retrieval
Fetch user details using the `getUser` method:
```typescript theme={null}
const user = await crossmintAuth.getUser(userId);
```
This provides access to user information such as email, phone number, and connected accounts (e.g., Twitter, Farcaster).
### JWT Verification
Verify JWTs independently using the `verifyCrossmintJwt` method:
```typescript theme={null}
const decodedJwt = crossmintAuth.verifyCrossmintJwt(token);
```
This is useful for validating tokens in middleware or specific endpoints. We expose our public keys for this purpose at [https://www.crossmint.com/.well-known/jwks.json](https://www.crossmint.com/.well-known/jwks.json).
# User Profile
Source: https://docs.crossmint.com/authentication/user-profile
Retrieve user profile data such as email or social login metadata
Crossmint Auth provides a way to retrieve and update user profile data, such as email, social login metadata, and more.
The user object contains the following information:
* `id`: User's unique identifier.
* `email`: User's email address.
* `phoneNumber`: User's phone number.
* `farcaster`: User's Farcaster account data.
* `twitter`: User's Twitter (X) account data.
Farcaster Account data contains:
* `fid`: User's Farcaster ID.
* `username`: User's Farcaster username.
* `bio`: User's Farcaster bio.
* `displayName`: User's Farcaster display name.
* `pfpUrl`: User's Farcaster profile image URL.
* `custody`: User's FID custody address.
* `verifications`: List of the user's verified addresses.
Twitter Account data contains:
* `id`: User's Twitter ID.
* `username`: User's Twitter username.
## Reading User Data
This API requires the `users.read` scope.
* **Server side**: Requires a server-side API key.
* **Client side**: Requires a client-side API key, and a logged in user.
### Server-side
For server-side operations, use the `@crossmint/server-sdk` that provides a `getUser` function to fetch user information.
```typescript theme={null}
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
const crossmint = createCrossmint({ apiKey: "" });
const crossmintAuth = CrossmintAuth.from(crossmint);
async function getUser(userId: string) {
try {
const user = await crossmintAuth.getUser(userId);
console.log("User data:", user);
} catch (error) {
console.error("Error fetching user data:", error);
}
}
```
### Response Format
```json theme={null}
{
"id": "123",
"email": "test@test.com", // Optional (if user was created with email)
"phoneNumber": "123456789", // Optional (if user was created with phone number)
"farcaster": {
"fid": "123",
"username": "johndoe",
"bio": "Hello, I'm John Doe",
"displayName": "John Doe",
"pfpUrl": "https://example.com/pfp.jpg",
"custody": "0x1234567890123456789012345678901234567890",
"verifications": ["0x1234567890123456789012345678901234567890"]
},
"twitter": {
"id": "456",
"username": "johndoe"
}
}
```
### Client-side
Use the `useAuth` react hook from `@crossmint/client-sdk-react-ui` to access the user object.
If you would like to use a different framework, [contact support](https://help.crossmint.com/hc/en-us/requests/new)
```tsx React theme={null}
import { useAuth } from "@crossmint/client-sdk-react-ui";
function User() {
const { user } = useAuth();
if (!user) {
return
More information [here](/stablecoin-orchestration/regulated-transfers/overview). |
| [Wallets](/wallets/overview) | Not required. |
See the data requirements per product [here](/identity/data-requirements).
## KYC Collection Methods
Crossmint supports three ways to collect the required identity data. Choose the method that best fits your compliance model and UX requirements:
| Method | Best For | How It Works |
| - | - | - |
| **Crossmint-hosted** | Fastest integration | Users complete KYC on Crossmint's screens, shown inside the embedded checkout or [in your own view](/onramp/guides/decouple-identity-verification). The screens follow your checkout `appearance` in both cases; see [UI Customization](/identity/ui-customization) |
| **API data imports** | Already verifying users with your own identity provider | Pass verified user data to Crossmint via API. Crossmint will use the information you have already collected to perform the required compliance checks |
| **Token-sharing (coming soon)** | Reusing a verified user token | Share a verified user token from a supported IDV provider |
## Getting Started
Register your first user
See required data by product
Style the Crossmint-hosted KYC screens
## FAQs
Crossmint supports Persona and Sumsub as identity verification (IDV) vendors. To use any other vendor, you must get it approved first—raise it with your Customer Success Engineer (CSE).
Token sharing is not yet supported. If this is something you need, reach out to your Customer Success Engineer (CSE).
A hybrid approach is not yet supported. You either run your own identity verification and pass that data to Crossmint, or use Crossmint's embedded KYC in the checkout.
Not yet. The [Create or Update User](/api-reference/users/upsert-user) endpoint accepts identity data for individuals only (name, date of birth, nationality, identity document, and so on) — there is no business or entity user type. Support for business users is coming soon. If your users are companies, reach out to your Customer Success Engineer (CSE).
# Quickstart ⚡
Source: https://docs.crossmint.com/identity/quickstart
Register users and upload documents to enable regulated product access.
This quickstart guide walks you through registering a user, uploading documents, and fetching document details using Crossmint's APIs. These steps enable your users to access regulated products like onramp, offramp, and payouts.
User locators use the `userId:` format, a unique identifier you choose for the user, for example `userId:johnd-123`.
Before sharing any user KYC data with Crossmint, you must ensure your users have accepted Crossmint's Privacy Policy. This is a prerequisite for companies sharing their customer's KYC data with Crossmint.
Consent must be collected in your app through a compliant flow (explicit checkbox, exact wording, visible Crossmint logo, re-consent on policy updates). See [Register User Data](/identity/register-user-data) for the full consent-collection requirements.
Meeting these requirements is mandatory for sharing user KYC data with Crossmint. Only record acceptance through the API below after the user has granted consent this way. Crossmint audits data-sharing integrations for compliance with these requirements.
Once the user has accepted the privacy policy, record their acceptance using the following API call:
```bash cURL theme={null}
curl --request PUT \
--url 'https://staging.crossmint.com/api/2025-06-09/users/userId:johnd-123/legal-documents' \
--header 'X-API-KEY: ' \
--header 'Content-Type: application/json' \
--data '{
"type": "crossmint-privacy-policy",
"acceptedAt": "2025-10-05T14:48:00Z"
}'
```
```javascript Node.js theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'PUT',
headers: {'X-API-KEY': '', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: "crossmint-privacy-policy",
acceptedAt: "2025-10-05T14:48:00Z" // ISO 8601 date string
})
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}/legal-documents`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python theme={null}
import requests
user_locator = "userId:johnd-123"
url = f"https://staging.crossmint.com/api/2025-06-09/users/{user_locator}/legal-documents"
payload = {
"type": "crossmint-privacy-policy",
"acceptedAt": "2025-10-05T14:48:00Z" # ISO 8601 date string
}
headers = {
"X-API-KEY": "",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.json())
```
This call also creates a user with the specified `userLocator` if one does not already exist.
Register a user with Crossmint by specifying their `userLocator`, their personal details, and KYC data using the [Create User](/api-reference/users/upsert-user) endpoint
```bash cURL theme={null}
curl --request PUT \
--url 'https://staging.crossmint.com/api/2025-06-09/users/userId:johnd-123' \
--header 'X-API-KEY: ' \
--header 'Content-Type: application/json' \
--data '{
"userDetails": {
"firstName": "John",
"lastName": "Doe",
"dateOfBirth": "1995-01-01",
"countryOfResidence": "DE"
}
}'
```
```javascript Node.js theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'PUT',
headers: {'X-API-KEY': '', 'Content-Type': 'application/json'},
body: JSON.stringify({
userDetails: {
firstName: "John",
lastName: "Doe",
dateOfBirth: "1995-01-01",
countryOfResidence: "DE"
}
})
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python theme={null}
import requests
user_locator = "userId:johnd-123"
url = f"https://staging.crossmint.com/api/2025-06-09/users/{user_locator}"
payload = {
"userDetails": {
"firstName": "John",
"lastName": "Doe",
"dateOfBirth": "1995-01-01",
"countryOfResidence": "DE"
}
}
headers = {
"X-API-KEY": "",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.json())
```
Once you have registered relevant user information you can trigger checks via the [Trigger Identity Verification](/api-reference/users/trigger-identity-verification) endpoint.
```bash cURL theme={null}
curl --request PUT \
--url 'https://staging.crossmint.com/api/2025-06-09/users/userId:johnd-123/identity-verification' \
--header 'X-API-KEY: '
```
```javascript Node.js theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'PUT',
headers: {
'X-API-KEY': ''
}
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}/identity-verification`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python theme={null}
import requests
user_locator = "userId:johnd-123"
url = f"https://staging.crossmint.com/api/2025-06-09/users/{user_locator}/identity-verification"
headers = {
"X-API-KEY": ""
}
response = requests.put(url, headers=headers)
print(response.json())
```
The response will include the eligibility status for each verification type (`regulated-transfer`, `onramp`, `onramp-light`, `offramp`, `offramp-light`):
* `not-started`: Verification has not been initiated
* `pending-privacy-policy`: User has not yet accepted Crossmint's Privacy Policy
* `requires-data`: Additional user data or documents are needed
* `pending-review`: Verification is in progress
* `pending-manual-review`: Verification requires manual review by Crossmint's compliance team
* `verified`: User has passed verification
* `rejected`: User has failed verification. May include a `failureReasons` array with a machine-readable cause; see [Failure Reasons](/identity/failure-reasons).
Once a product reads `verified`, the user can use it. A `verified` `offramp` status, for example, means the user can cash out through [Offramp](/offramp/overview).
If any of them return `requires-data`, you must aggregate the `missingData` and `missingDocuments` arrays from those specific eligibility objects. Then, update the user's information using the [Update User](/api-reference/users/upsert-user) endpoint and/or upload additional documents using the [Upload Document](/api-reference/users/upload-document) endpoint, and finally re-trigger the verification.
```json theme={null}
{
"eligibility": [
{
"type": "regulated-transfer",
"status": "pending-review" // or "verified"
},
{
"type": "onramp",
"status": "requires-data",
"missingData": ["kyc-data", "due-diligence", "verification-history"],
"missingDocuments": ["id", "selfie-front"] // Crossmint may also request "proof-of-address" or "source-of-wealth" for specific users
},
{
"type": "onramp-light",
"status": "requires-data",
"missingData": ["kyc-data"] // light KYC requires less data and no document uploads
},
{
"type": "offramp",
"status": "requires-data",
"missingData": ["kyc-data", "due-diligence", "verification-history"],
"missingDocuments": ["id", "selfie-front"] // Crossmint may also request "proof-of-address" or "source-of-wealth" for specific users
},
{
"type": "offramp-light",
"status": "requires-data",
"missingData": ["kyc-data"] // light KYC requires less data and no document uploads
}
]
}
```
Register additional user information with Crossmint so the user can access onramp and offramp services.
Calling this endpoint again with the same `userLocator` will update the existing user's information. The endpoint is not additive so specify all relevant user information when updating the user.
```bash cURL theme={null}
curl --request PUT \
--url 'https://staging.crossmint.com/api/2025-06-09/users/userId:johnd-123' \
--header 'X-API-KEY: ' \
--header 'Content-Type: application/json' \
--data '{
"userDetails": {
"firstName": "John",
"lastName": "Doe",
"dateOfBirth": "1995-01-01",
"countryOfResidence": "DE"
},
"kycData": {
"nationality": "DE",
"addressOfResidence": {
"line1": "123 Hauptstrasse",
"line2": "Apt 5",
"city": "Berlin",
"stateOrRegion": "Berlin",
"postalCode": "10115"
},
"email": "johnd@example.com",
"identityDocument": {
"type": "passport",
"number": "AS12321",
"issuingCountryCode": "DE"
},
"ipAddresses": ["203.0.113.1"]
},
"dueDiligence": {
"employmentStatus": "full-time",
"sourceOfFunds": "salary-disbursement",
"industry": "financial-institution",
"expectedYearlyTxVolume": "volume-0-25k"
},
"verificationHistory": {
"idVerificationTimestamp": "2024-01-15T10:30:00Z",
"livenessVerificationTimestamp": "2024-01-15T10:32:00Z"
}
}'
```
```javascript Node.js theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'PUT',
headers: {'X-API-KEY': '', 'Content-Type': 'application/json'},
body: JSON.stringify({
userDetails: {
firstName: "John",
lastName: "Doe",
dateOfBirth: "1995-01-01",
countryOfResidence: "DE"
},
kycData: {
nationality: "DE",
addressOfResidence: {
line1: "123 Hauptstrasse",
line2: "Apt 5",
city: "Berlin",
stateOrRegion: "Berlin",
postalCode: "10115"
},
email: "johnd@example.com",
identityDocument: {
type: "passport",
number: "AS12321",
issuingCountryCode: "DE"
},
ipAddresses: ["203.0.113.1"]
},
dueDiligence: {
employmentStatus: "full-time",
sourceOfFunds: "salary-disbursement",
industry: "financial-institution",
expectedYearlyTxVolume: "volume-0-25k"
},
verificationHistory: {
idVerificationTimestamp: "2024-01-15T10:30:00Z",
livenessVerificationTimestamp: "2024-01-15T10:32:00Z"
}
})
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python theme={null}
import requests
user_locator = "userId:johnd-123"
url = f"https://staging.crossmint.com/api/2025-06-09/users/{user_locator}"
payload = {
"userDetails": {
"firstName": "John",
"lastName": "Doe",
"dateOfBirth": "1995-01-01",
"countryOfResidence": "DE"
},
"kycData": {
"nationality": "DE",
"addressOfResidence": {
"line1": "123 Hauptstrasse",
"line2": "Apt 5",
"city": "Berlin",
"stateOrRegion": "Berlin",
"postalCode": "10115"
},
"email": "johnd@example.com",
"identityDocument": {
"type": "passport",
"number": "AS12321",
"issuingCountryCode": "DE"
},
"ipAddresses": ["203.0.113.1"]
},
"dueDiligence": {
"employmentStatus": "full-time",
"sourceOfFunds": "salary-disbursement",
"industry": "financial-institution",
"expectedYearlyTxVolume": "volume-0-25k"
},
"verificationHistory": {
"idVerificationTimestamp": "2024-01-15T10:30:00Z",
"livenessVerificationTimestamp": "2024-01-15T10:32:00Z"
}
}
headers = {
"X-API-KEY": "",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.json())
```
Upload identity or supporting documents for the user using the [Upload Document](/api-reference/users/upload-document) endpoint. Documents are associated with the user via their `userLocator`.
```bash cURL theme={null}
curl --request POST \
--url 'https://staging.crossmint.com/api/2025-06-09/documents' \
--header 'X-API-KEY: ' \
--header 'Content-Type: application/json' \
--data '{
"reference": {
"userLocator": "userId:johnd-123"
},
"documentType": "id-passport",
"data": "",
"expiresAt": "2030-12-31"
}'
```
```javascript Node.js theme={null}
const options = {
method: 'POST',
headers: {'X-API-KEY': '', 'Content-Type': 'application/json'},
body: JSON.stringify({
reference: {
userLocator: "userId:johnd-123"
},
documentType: "id-passport",
data: "",
expiresAt: "2030-12-31"
})
};
fetch('https://staging.crossmint.com/api/2025-06-09/documents', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python theme={null}
import requests
url = "https://staging.crossmint.com/api/2025-06-09/documents"
payload = {
"reference": {
"userLocator": "userId:johnd-123"
},
"documentType": "id-passport",
"data": "",
"expiresAt": "2030-12-31"
}
headers = {
"X-API-KEY": "",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
**Supported document types:**
* Identity documents: `id-passport`, `id-idcard-front`, `id-idcard-back`, `id-residency-permit` (Spain residents only)
* Supporting documents: `proof-of-address`, `source-of-wealth`
* Selfie documents: `selfie-front`, `selfie-left`, `selfie-right`
US residents must include `kycData.phoneNumber`, and provide their identity via SSN in `kycData.identityDocument`. No identity document upload or selfie is needed for standard on/offramp. If Crossmint's compliance team requests an identity document from a US user, upload a passport (`id-passport`) or driver's license (`id-idcard-front`, `id-idcard-back`). Source of wealth is generally not requested from US users.Uploading again with the same `userLocator` and `documentType` replaces the previous upload — the most recent document is the one used for verification.
Once you have registered the user's information and uploaded the required documents, trigger the KYC verification process using the [Trigger Identity Verification](/api-reference/users/trigger-identity-verification) endpoint.
```bash cURL theme={null}
curl --request PUT \
--url 'https://staging.crossmint.com/api/2025-06-09/users/userId:johnd-123/identity-verification' \
--header 'X-API-KEY: '
```
```javascript Node.js theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'PUT',
headers: {
'X-API-KEY': ''
}
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}/identity-verification`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python theme={null}
import requests
user_locator = "userId:johnd-123"
url = f"https://staging.crossmint.com/api/2025-06-09/users/{user_locator}/identity-verification"
headers = {
"X-API-KEY": ""
}
response = requests.put(url, headers=headers)
print(response.json())
```
The response will include the eligibility status for each verification type (`regulated-transfer`, `onramp`, `onramp-light`, `offramp`, `offramp-light`):
```json theme={null}
{
"eligibility": [
{
"type": "regulated-transfer",
"status": "verified"
},
{
"type": "onramp",
"status": "pending-review"
},
{
"type": "onramp-light",
"status": "pending-review"
},
{
"type": "offramp",
"status": "pending-review"
},
{
"type": "offramp-light",
"status": "pending-review"
}
]
}
```
Verification runs asynchronously. After triggering it, poll the [Get Identity Verification Status](/api-reference/users/get-identity-verification) endpoint until the relevant product status reads `verified`. It usually completes within a minute, and the transient state may be `not-started` or `pending-review`.
```bash cURL theme={null}
curl --request GET \
--url 'https://staging.crossmint.com/api/2025-06-09/users/userId:johnd-123/identity-verification' \
--header 'X-API-KEY: '
```
```javascript Node.js theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'GET',
headers: {
'X-API-KEY': ''
}
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}/identity-verification`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python theme={null}
import requests
user_locator = "userId:johnd-123"
url = f"https://staging.crossmint.com/api/2025-06-09/users/{user_locator}/identity-verification"
headers = {
"X-API-KEY": ""
}
response = requests.get(url, headers=headers)
print(response.json())
```
You can fetch a user's information at any time to see their current status, including when they last accepted valid legal documents. Use the Get User endpoint to retrieve this information:
```bash cURL theme={null}
curl --request GET \
--url 'https://staging.crossmint.com/api/2025-06-09/users/userId:johnd-123' \
--header 'X-API-KEY: '
```
```javascript Node.js theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'GET',
headers: {
'X-API-KEY': ''
}
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python theme={null}
import requests
user_locator = "userId:johnd-123"
url = f"https://staging.crossmint.com/api/2025-06-09/users/{user_locator}"
headers = {
"X-API-KEY": ""
}
response = requests.get(url, headers=headers)
print(response.json())
```
The response includes the `legalDocuments` array, which shows the status of legal document acceptances:
```json theme={null}
{
"email": "john.doe@example.com",
"phoneNumber": "+1234567890",
"userId": "usr_1234567890",
"userDetails": true,
"kycData": false,
"dueDiligence": false,
"verificationHistory": false,
"legalDocuments": [
{
"type": "crossmint-privacy-policy",
"acceptedAt": "2024-01-15T10:30:00Z",
"validVersion": true
}
]
}
```
The `validVersion` field indicates whether the user has accepted the current version of the legal document. If `validVersion` is `false`, you should prompt the user to accept the updated terms.
## Additional Document Requests
Crossmint's compliance team may request a specific additional document from a user at any time, even after verification — a proof of address, a source of wealth document, or an identity document. Each is requested independently, only for the users that need it.
When a document is requested:
1. The verification status for the affected products reads `requires-data`, with the document listed in `missingDocuments` (e.g. `["proof-of-address"]`).
2. Upload the document via the [Upload Document](/api-reference/users/upload-document) endpoint (`documentType: "proof-of-address"`, `"source-of-wealth"`, or an identity document type).
3. Re-trigger verification via the [Trigger Identity Verification](/api-reference/users/trigger-identity-verification) endpoint — the same call as after any other data update. The status moves to `pending-review` while the document is reviewed.
4. Poll the [Get Identity Verification Status](/api-reference/users/get-identity-verification) endpoint. If the document is accepted, the status moves on to whatever else is required for that product. If it is not accepted, the status becomes `rejected` — for document requests this is retryable: upload a fresh document and re-trigger verification to start a new review.
If a document of the requested type is already on file, nothing is listed as missing and the status reads `not-started` until you re-trigger verification.
## Handling a `rejected` status
A `rejected` entry may include a `failureReasons` array with more detail about why the verification failed, when available. See [Failure Reasons](/identity/failure-reasons) for the full list of codes and how to handle each one.
## Launching in Production
For production, the steps are almost identical, but some changes are required:
1. Create a developer account on the production console
2. Create a production server API key on the API Keys page with the API scopes `users.create`, `users.read`
3. Replace your test API key with the production key
4. Replace `staging.crossmint.com` with `www.crossmint.com` in the API URLs
## Learn More
See what data is required for each activity and region.
Contact the Crossmint sales team for support.
# Register User Data
Source: https://docs.crossmint.com/identity/register-user-data
Provide personal information about recipients in order to comply with regulation
Register the transfer recipient as a user in your Crossmint project, then attach the recipient data required for payouts.
Crossmint uses this data to screen recipients against Office of Foreign Assets Control (OFAC) sanctions lists.
## Requirements
* An API key
* A stable user identifier in your system (used as `userLocator`)
* A Crossmint wallet address for the recipient (or the ability to create one)
## Step 1: Accept Crossmint's Privacy Policy
Before sharing any user KYC data with Crossmint, you must ensure your users have accepted Crossmint's Privacy Policy. This is a prerequisite for any company sharing their customers' KYC data with Crossmint.
**Requirements for collecting consent**
Consent must be obtained following the guidelines below:
1. The user must have the ability to see the terms they must accept
2. The user must grant consent with a checkbox, unchecked by default
3. The user does not need to open the hyperlink to the privacy policy to view the terms
4. The user needs to see Crossmint's logo in the flow
5. The specific text that you must present to your end user is: **"I consent to my KYC data being processed by Crossmint in accordance with its Privacy Policy"**
6. The text can be localized but must link to the same privacy policy link provided above
7. If the privacy policy is updated, you must ask the user to agree to the above terms again
Meeting these requirements is mandatory for sharing user KYC data with Crossmint. Only record acceptance through the API below after the user has granted consent this way. Crossmint audits data-sharing integrations for compliance with these requirements.
Once the user has accepted the privacy policy, record their acceptance using the [Accept Legal Document](/api-reference/users/accept-legal-document) endpoint:
```bash cURL theme={null}
curl --request PUT \
--url 'https://staging.crossmint.com/api/2025-06-09/users/userId:johnd-123/legal-documents' \
--header 'X-API-KEY: ' \
--header 'Content-Type: application/json' \
--data '{
"type": "crossmint-privacy-policy",
"acceptedAt": "2025-10-05T14:48:00Z"
}'
```
```javascript Node.js theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'PUT',
headers: {'X-API-KEY': '', 'Content-Type': 'application/json'},
body: JSON.stringify({
type: "crossmint-privacy-policy",
acceptedAt: "2025-10-05T14:48:00Z" // ISO 8601 date string
})
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}/legal-documents`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```python Python theme={null}
import requests
user_locator = "userId:johnd-123"
url = f"https://staging.crossmint.com/api/2025-06-09/users/{user_locator}/legal-documents"
payload = {
"type": "crossmint-privacy-policy",
"acceptedAt": "2025-10-05T14:48:00Z" # ISO 8601 date string
}
headers = {
"X-API-KEY": "",
"Content-Type": "application/json"
}
response = requests.put(url, json=payload, headers=headers)
print(response.json())
```
This call also creates a user with the specified `userLocator` if one does not already exist.
## Step 2: Create a User
Register the transfer recipient as a user in your Crossmint project and associate the user with a Crossmint wallet address.
There are two ways to create a user:
### Choose a Method
* Use Method 1 if you create the wallet and user in one API call
* Use Method 2 if you register the user first and create the wallet later
Create a wallet for a user and associate it to a `userLocator` by setting the `owner` property. This method creates in a
single API call both the wallet, and the user associated with it.
The `owner` property accepts a `userLocator`. This associates the wallet with the user identifier in your system.
```javascript theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'POST',
headers: {'X-API-KEY': '', 'Content-Type': 'application/json'},
body: JSON.stringify({
chainType: "evm",
config: {
recoveryMethods: [
{
type: "email",
email: "johnd@example.com"
}
]
},
owner: userLocator
})
};
fetch('https://staging.crossmint.com/api/2025-06-09/wallets', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```javascript theme={null}
import { useWallet } from '@crossmint/client-sdk-react-ui';
const { getOrCreateWallet } = useWallet();
// The "owner" property is inferred by the JWT token, no need to specify it
const wallet = await getOrCreateWallet({
chain: "evm",
signer: {
type: "email",
email: "johnd@example.com"
}
});
```
Register a user with Crossmint without creating a wallet yet. Later, when you create a wallet for that registered user,
you can link the wallet's `owner` property to that user's registered `userLocator` value. This will associate the wallet with the user identifier in your system.
**Step 1: Register the user**
```javascript theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'PUT',
headers: {'X-API-KEY': '', 'Content-Type': 'application/json'}
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
**Step 2: Create wallet and link to registered user**
When you are ready to create a wallet for the registered user, use the same `userLocator` in the `owner` field:
```javascript theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'POST',
headers: {'X-API-KEY': '', 'Content-Type': 'application/json'},
body: JSON.stringify({
chainType: "evm",
config: {
recoveryMethods: [
{
type: "email",
email: "johnd@example.com"
}
]
},
owner: userLocator
})
};
fetch('https://staging.crossmint.com/api/2025-06-09/wallets', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```javascript theme={null}
import { useWallet } from '@crossmint/client-sdk-react-ui';
const { getOrCreateWallet } = useWallet();
// The "owner" property is inferred by the JWT token, no need to specify it
const wallet = await getOrCreateWallet({
chain: "evm",
signer: {
type: "email",
email: "johnd@example.com"
}
});
```
## Step 3: Register User Data
Use the [Create User API](/api-reference/users/upsert-user) to attach `userDetails` to the user.
To see data requirements by activity and region, see [Data Requirements](/identity/data-requirements).
For payouts, the only information needed is `userDetails`.
```javascript REST theme={null}
const userLocator = "userId:johnd-123";
const options = {
method: 'PUT',
headers: {'X-API-KEY': '', 'Content-Type': 'application/json'},
body: JSON.stringify({
userDetails: {
firstName: "John",
lastName: "Doe",
dateOfBirth: "2007-01-01",
countryOfResidence: "DE"
}
})
};
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}/`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
## Guides
# UI Customization
Source: https://docs.crossmint.com/identity/ui-customization
Style the identity verification screens that Crossmint shows inside your app with the appearance settings you already use for checkout
When a user needs to verify their identity, Crossmint shows a set of KYC screens inside your app: a consent screen, personal details, address, and, for full verification, an ID document and selfie step. This guide covers which `appearance` settings apply to those screens, what each one changes, the default values, and how to place the screens on your page.
## Prerequisites
* **An integration that shows the KYC screens**: the [embedded checkout](/onramp/quickstarts/react) for onramp or offramp, or the [standalone verification component](/onramp/guides/decouple-identity-verification).
## Before You Start
* The KYC screens read the same `appearance` object that you pass to the embedded checkout. If checkout already uses your brand, the KYC screens use it too, and there is nothing else to configure.
* Every setting is optional. A setting that you do not set uses the Crossmint default.
* If one value is not valid (for example, a color with a typo), the screens ignore all of your settings and show the default look. No error message shows, so test after each change.
* The ID document and selfie step (full verification only) has one light look and one dark look. Your colors and fonts do not apply to it. See [Light and Dark Mode](#light-and-dark-mode).
## Apply Your Theme
Pass the `appearance` prop to the embedded checkout as described in the [embedded checkout UI customization guide](/payments/embedded/guides/ui-customization). The KYC screens pick it up automatically when the order requires verification.
```tsx theme={null}
import { CrossmintEmbeddedCheckout } from "@crossmint/client-sdk-react-ui";
// Order created on your server with POST /api/2022-06-09/orders
const order = { orderId: "YOUR_ORDER_ID", clientSecret: "YOUR_ORDER_CLIENT_SECRET" };
;
```
If you [render the KYC step in your own view](/onramp/guides/decouple-identity-verification), pass the same object to the `appearance` prop of `CrossmintIdentityVerification` (`@crossmint/client-sdk-react-ui` 4.10.0 or later). Earlier versions show the default look.
## Appearance Settings and Where They Apply
Each setting controls one part of the KYC screens. The numbers in the table match the labels on the example screen. Color settings live under `appearance.variables.colors`, and component rules under `appearance.rules`.
| # | Element | What it changes | Setting |
| :- | :- | :- | :- |
| 1 | Background | The background of all screens | `colors.backgroundPrimary` |
| 2 | Main text | Titles, field labels, and the text that the user types | `colors.textPrimary` |
| 3 | Secondary text | Subtitles, hints, and consent text | `colors.textSecondary` |
| 4 | Accent | Icons, links, the border of the selected field, and selected options. It does not change the button | `colors.accent` |
| 5 | Borders | Field borders | `colors.borderPrimary` |
| 6 | Errors | Error messages and the border of a field with an error | `colors.danger` |
| 7 | Main button | Background, text, hover, and disabled colors. Set your button color here | `rules.PrimaryButton` |
| 8 | Fields (optional) | Background, border, shadow, and placeholder of the fields. Without it, fields use the colors above | `rules.Input` |
| | Field labels (optional) | Font and color of the field labels. Without it, labels use `textPrimary` | `rules.Label` |
The checkout settings `variables.colors.warning`, `rules.Tab`, `rules.DestinationInput`, and `rules.ReceiptEmailInput` have no effect on the KYC screens.
## Default Colors
The screens use these colors when you do not set a value. Change only the colors that differ from your brand.
| Element | Setting | Default |
| :- | :- | :- |
| Background | `backgroundPrimary` | `#FFFFFF` |
| Main text | `textPrimary` | `#00150D` |
| Secondary text | `textSecondary` | `#67797F` |
| Accent | `accent` | `#04AA6D` |
| Borders | `borderPrimary` | `#D1D5DB` |
| Error | `danger` | `#DC2626` |
| Button | `PrimaryButton.colors.background` | `#05B959` |
| Button text | `PrimaryButton.colors.text` | `#FFFFFF` |
| Button hover | `PrimaryButton.hover.colors.background` | `#0BAF5C` |
| Disabled button | `PrimaryButton.disabled.colors.background` | `#E7E9ED` |
| Disabled button text | `PrimaryButton.disabled.colors.text` | `#67797F` |
| Icon tile fill | Calculated, not a setting | `#EDEFEE` |
| Element | Setting | Default |
| :- | :- | :- |
| Background | `backgroundPrimary` | `#1E2023` |
| Main text | `textPrimary` | `#FFFFFF` |
| Secondary text | `textSecondary` | `#939596` |
| Accent | `accent` | `#04AA6D` |
| Borders | `borderPrimary` | `#3E4043` |
| Error | `danger` | `#DC2626` |
| Button | `PrimaryButton.colors.background` | `#05B959` |
| Button text | `PrimaryButton.colors.text` | `#FFFFFF` |
| Button hover | `PrimaryButton.hover.colors.background` | `#0BAF5C` |
| Disabled button | `PrimaryButton.disabled.colors.background` | `#282C2F` |
| Disabled button text | `PrimaryButton.disabled.colors.text` | `#939596` |
| Icon tile fill | Calculated, not a setting | `#2E3032` |
Both modes use the Inter font, 12px corners, and 16px field text. Fields use `backgroundPrimary` unless `rules.Input.colors.background` sets another color. The fill behind the icon on the first screen is 7% of `textPrimary` mixed into `backgroundPrimary`.
## Light and Dark Mode
There is no `theme` setting. The screens pick the light or dark defaults from `variables.colors.backgroundPrimary`:
* An opaque dark color (for example `#1E2023`) selects the dark defaults.
* A light color, a transparent or semi-transparent color, or no value selects the light defaults.
The colors that you set replace only those values. If your app has a light mode and a dark mode, send the `appearance` that matches the mode the user sees.
The ID document and selfie step reads the same `backgroundPrimary` to pick its light or dark look, so it matches the other screens. It does not apply your other colors or fonts.
To get the dark look without picking every color yourself, set `backgroundPrimary` to a dark color and leave the rest unset.
## Fonts, Sizes, and Corners
| Setting | What it changes | Default |
| :- | :- | :- |
| `variables.fontFamily`, `fonts` | The typeface. Fonts load from Google Fonts only; a font served from your own domain does not load | Inter |
| `variables.borderRadius` | Corners of fields and the button | `12px` |
| `variables.fontSizeUnit` | Text size in fields and the button. The screens keep it between `4px` and `5px`: smaller field text makes iPhones zoom in on focus | `4px` (16px field text) |
| `variables.spacingUnit` | Space inside fields. The screens keep it between `2.5px` and `5px` | `3.33px` |
## Good Practices
Users type their name, date of birth, address, and ID number on these screens, often on a phone. These rules keep the screens easy to read.
* **Check contrast against the background.** Text colors need a contrast of at least 4.5:1 against `backgroundPrimary`. Accent and borders need at least 3:1.
* **Use a darker accent for a pale brand color.** Use the pale color on the button, with dark text. For `accent`, use a darker shade so links and field borders stay visible.
* **Set all button states.** Set background, text, hover, and disabled. The button stays disabled until the user fills the fields, so users see that state often.
* **Use a font made for body text.** The font shows at 16px in fields and smaller in hints. Thin or decorative fonts are hard to read at that size. Load the regular, medium, and semibold weights.
* **Keep the error color red.** Users must see a field with an error immediately. Do not use your brand color for `danger`.
* **Use one plain background color.** `backgroundPrimary` takes one color. Gradients and images are not possible. Put brand images on your page, outside the KYC screens.
## Place the Screens on Your Page
The KYC screens show inside an iframe on your page. The iframe shows one screen at a time. Its height changes from screen to screen, and each screen has its Continue button at the bottom.
* **Safe area.** Leave 24px of space on all four sides of the iframe. The screens do not include it; your page must add it.
* **Height.** Let the iframe grow with the screen inside it. Do not set a fixed height, or the Continue button gets cut off.
* **Overlays.** Nothing should cover the Continue button, such as a cookie banner or a bottom bar.
* **Background.** Give your page the same background color as `backgroundPrimary`, or the edge of the iframe shows.
* **Width.** On phones, keep the 24px safe area on each side inside the screen width and let the iframe fill the remaining space. On desktop, use the same width as your checkout.
* **Camera.** The ID document and selfie step uses the camera. The Crossmint SDK allows it on the iframe. If you render the iframe without the SDK, allow the camera on it with `allow="camera"`.
* **Narrow phones.** Test on a screen 360px wide. Many Android phones have this width.
## What You Cannot Change
| Item | How it works today |
| :- | :- |
| Icon on the first screen | Crossmint icon, in the `accent` color |
| Text on the screens | Not editable. The language follows the user's browser, or the `locale` that you set |
| Order and layout of screens | The screens change with the user's country and the amount |
| ID document and selfie step | Light or dark only. For additional customizations, ask your Crossmint contact |
| Icon tile fill | Calculated from your text and background colors |
## Test Your Theme
Run these checks in staging before you go live.
* Your checkout settings show on the KYC screens.
* Light mode and dark mode, if your app has both.
* A full verification with a higher amount: the ID document and selfie step shows in the correct mode.
* An empty field and an incorrect value: the error color shows.
* The hover and disabled states of the button.
* A phone 360px wide and a desktop: the button is fully visible and the page does not scroll sideways.
## Next Steps
Full list of appearance settings, with code
Render the verification step in your own view
# Using AI Assistants
Source: https://docs.crossmint.com/introduction/ai-assistants
Integrate faster with Crossmint by ingesting the docs into your AI-based code helpers (like Cursor, GitHub Copilot, etc.).
AI assistant ready docs
## Integrate with Cursor
1. Navigate to Cursor Settings > Features > Docs
2. Select "Add new doc" and paste the following URL:
```text theme={null}
https://docs.crossmint.com/llms-full.txt
```
3. Use `@docs -> Crossmint` to reference Crossmint's docs in your code.
***
> To find navigation and other pages in this documentation, fetch the llms.txt file at: [https://docs.crossmint.com/llms.txt](https://docs.crossmint.com/llms.txt)
## Crossmint Docs MCP Server
The Crossmint Docs MCP server searches the documentation. To let an AI assistant manage your console projects,
organizations, and API keys, use the [Crossmint MCP server](/mcp-server/connect).
The Crossmint Docs MCP server lets your AI assistant search and read the Crossmint docs when it needs them. It returns only the sections that match the query, not the full docs.
* **URL:** `https://docs.crossmint.com/mcp`
* **Transport:** Streamable HTTP
* **Authentication:** none
### Installation
Run this command in your terminal:
```bash theme={null}
claude mcp add --transport http crossmint-docs https://docs.crossmint.com/mcp
```
Add the server to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):
```json mcp.json theme={null}
{
"mcpServers": {
"crossmint-docs": {
"url": "https://docs.crossmint.com/mcp"
}
}
}
```
Open the command palette (`Cmd/Ctrl + Shift + P`), select `MCP: Open User Configuration`, and add the server:
```json mcp.json theme={null}
{
"servers": {
"crossmint-docs": {
"type": "http",
"url": "https://docs.crossmint.com/mcp"
}
}
}
```
### Tools
The server gives your agent three tools. The agent selects the tool to use from your prompt.
| Tool | What it does |
| - | - |
| `search_crossmint_docs` | Searches the docs and returns the matching sections, each with a title, link, and content. |
| `query_docs_filesystem_crossmint_docs` | Reads full pages and OpenAPI specs with read-only commands such as `rg`, `cat`, `ls`, and `tree`. |
| `submit_feedback` | Reports an incorrect, outdated, or incomplete page to the Crossmint docs team. |
The server also exposes the Crossmint skill file as an MCP resource.
Example prompts:
* "How do I create non-custodial wallets for my users?"
* "How do I get a staging API key?"
* "Does Crossmint support Solana?"
* "How do I set up transfer webhooks?"
# Getting Started
Source: https://docs.crossmint.com/introduction/getting-started
Before you can start integrating Crossmint into your application, you need to create a Crossmint account and set up a new project in the Staging Console. This guide will walk you through those steps.
Open Crossmint's [staging console](https://staging.crossmint.com/console) and create an account
**Ready for launch?**
When it's time to go live, simply create an account in the [production console](https://www.crossmint.com/console) and replicate the resources you need.
If you've just created an account for the first time, you'll be taken directly to project creation process. Add the name to your project and continue.
In the [overview of your project](https://staging.crossmint.com/console/overview) you will find the client and server API keys you need to integrate Crossmint into your application.
Now that your project is created in the Console, you can start integrating Crossmint products into your application.
Create wallets for users, agents, and companies.
Authenticate users with email, socials, or wallets.
Accept card and cryptocurrency payments.
Fund wallets with stablecoins or crypto.
Mint, distribute, and manage tokens at scale.
Issue and verify W3C credentials.
# Glossary
Source: https://docs.crossmint.com/introduction/glossary
Key terms and definitions used across the Crossmint documentation.
A reference of key terms used throughout the Crossmint documentation. If you encounter an unfamiliar term in any guide or concept page, check here first.
| Term | Definition |
| - | - |
| **Smart contract wallet** | A wallet deployed as a smart contract on a public blockchain. It lives onchain — not inside Crossmint's servers — and its behavior is governed by auditable code. On EVM chains, Crossmint wallets implement [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) with [ERC-7579](https://eips.ethereum.org/EIPS/eip-7579) modular extensions. See [Architecture](/wallets/architecture). |
| **Signer** | A cryptographic identity authorized to approve actions on a wallet. Signers are separate from the wallet itself and can be added, removed, or rotated without changing the wallet's address. See [Signers](/wallets/concepts/signers). |
| **Operational signer** | A signer added to a wallet for day-to-day transaction authorization. Operational signers are registered by a recovery method and can be scoped with specific permissions. Examples: a device key for user wallets, a server signer for company wallets. See [Signers](/wallets/concepts/signers). |
| **Delegated signer** | The previous term for **operational signer**. Still used in current SDK method names (`addDelegatedSigner`, `delegatedSigners`) but being renamed in the next breaking release. |
| **Recovery method** | A way for the wallet owner to prove ownership and regain access when the operational signer is lost — for example, when a user gets a new phone. Recovery methods authorize the enrollment of a new operational signer without changing the wallet address. See [Recovery Methods](/wallets/concepts/recovery). |
| **Recovery signer** | A technical synonym for **recovery method**, retained in SDK and onchain terminology. |
| **Admin signer** | The legacy/API field name for **recovery method**. You may still encounter this term in API responses. |
| **Custody** | An entity has custody of a wallet if it can **unilaterally execute transactions** on the wallet or **unilaterally block transactions** on the wallet. See [Custody Models](/wallets/concepts/custody-models) for full details. |
| **Non-custodial** | A wallet configuration where your company cannot unilaterally move or block funds. Users control their own signers. See [Custody Models](/wallets/concepts/custody-models). |
| **Wallet locator** | A human-readable identifier for a wallet, following the format `email:user@example.com:evm` or `userId:abc123:solana`. Used in API calls instead of raw wallet addresses. |
| **Signer locator** | A human-readable identifier for a signer, such as `external-wallet:0x1234...` or `email:user@example.com`. Used when registering or referencing signers in API calls. |
| **Secure enclave** | The secure storage built into a user's phone, computer, or browser — such as the iOS Secure Enclave, Android Keystore, or the browser's built-in credential store. Cryptographic keys generated inside a secure enclave are non-extractable and never leave the device. Crossmint uses device secure enclaves to back [device key signers](/wallets/guides/signers/device-signer). |
| **TEE (Trusted Execution Environment)** | A hardware-attested secure enclave (e.g., Intel TDX) where sensitive cryptographic operations occur. Crossmint uses TEEs for email and SMS recovery methods — master secrets are derived inside the TEE and never exposed to Crossmint or the host application. |
| **Onchain** | Deployed to or enforced by the blockchain. In Crossmint wallets, signer permissions and wallet logic are onchain and publicly auditable. |
# Platform Overview
Source: https://docs.crossmint.com/introduction/platform-overview
Crossmint is an all-in-one platform to integrate [wallets](/wallets/overview), [stablecoins](/stablecoin-orchestration/overview), and other blockchain primitives into your product. Trusted by the world's largest financial institutions including MoneyGram and Santander Bank, and thousands of start-ups.
At its core, Crossmint abstracts the hardest parts of blockchain—wallets, keys, gas, compliance, and payments—into simple APIs, SDKs, and dashboards. This allows teams to focus on user experience and business logic while Crossmint handles the underlying complexity.
## How the Platform Is Structured
Crossmint is designed as a modular platform composed of interoperable building blocks. You can use them independently or combine them into an end-to-end system:
| Product | Description |
| :- | :- |
| [Wallets](/wallets/overview) | Embedded wallets for user-facing applications and [treasury wallets](/wallets/concepts/treasury) for businesses. Wallets support flexible custody models, programmable permissions, and abstract away private keys and gas. Extensions such as [yield](/wallets/guides/wallet-extensions/yield-xyz), trading, and [cards](/wallets/guides/wallet-extensions/credit-cards) can be added on top of core wallet functionality. |
| [Payments](/stablecoin-orchestration/overview) | Stablecoin infrastructure, including [onramps](/onramp/overview) and offramps, [payouts and transfers](/stablecoin-orchestration/regulated-transfers/overview), and [checkout](/payments/introduction). These APIs let your platform onramp, offramp, convert, and settle onchain across 150+ countries through a single integration. |
| [Tokenization](/minting/introduction) | Issuance and distribution of tokens at scale. |
The platform includes built-in compliance infrastructure across the regulated products, covering [KYC, AML and sanctions screening](/identity/overview), and travel rule compliance—so you can build compliant products without running these systems yourself.
## What You Can Build
Crossmint's building blocks are designed to be combined into complete, end-to-end solutions. Teams can mix and match wallets, payments, and embedded finance modules to support a wide range of products and workflows, like remittances and consumer fintech apps.
Browse guides tailored to your business vertical or use case
## How Teams Integrate
Crossmint is API-first, with dashboards for monitoring and daily operations.
For customers building complex systems, Crossmint provides **forward-deployed engineers** and **solution architects** who partner closely with teams to design architecture, checkout experiences, and compliance setup, reducing implementation risk and time to launch.
## What Makes Crossmint Unique
* **All-in-one platform**: You don't have to orchestrate 5-10 vendors. In addition to the products above, Crossmint saves you from separate integrations with RPCs, data APIs, AML screening providers, KYC, and other ancillary providers, offering everything you need in a fully integrated platform.
* **Make the blockchain invisible to your users**: No gas fees, passphrases, transaction approval prompts, or other blockchain gimmicks.
* **Easy-to-use APIs**: Don't require blockchain experience to get you started in minutes. Crossmint optionally takes care of gas fees and bills you in fiat, so you don't have to touch crypto or set up a treasury wallet.
* **No vendor lock-in**: Change providers anytime with minimal user disruption.
* **Bank-grade security**: Battle-tested and trusted by some of the largest financial institutions in the world.
## Learn more
View all 50+ supported chains
Get started in minutes
Talk to our team
# Account Verification
Source: https://docs.crossmint.com/introduction/platform/account-verification
Learn how to get your account verified
Access to certain Crossmint products is subject to compliance review.
| Product | Requirement |
| - | - |
| Wallets | No KYB required. |
| Stablecoin Orchestration | Signed Order Form and KYB required. |
| Onramps | Signed Order Form and KYB required. |
| Checkout | Signed Order Form and KYB required. Collection whitelisting also required for memecoins and primary NFT sales. |
| Tokenization | No KYB required. |
Reviews are only required in production. Staging does not require an Order Form, KYB, or collection review for any product.
Each distinct project you create via the Crossmint Console requires a separate review.
## How Production Access Works
For products that require it, production access follows the same sequence:
Work with the Crossmint sales team to sign an Order Form. The Order Form is the commercial agreement and includes the KYB obligation.
Crossmint's compliance team runs KYB (Know Your Business) verification as part of onboarding. The review covers entity formation, beneficial ownership, and business purpose.
Production is enabled once KYB passes. There is no separate request to make — a successful KYB unlocks the production environment for your account.
Staging is self-serve. You can build and test against staging with only a staging console account — no Order Form, no KYB, and no manual approval.
## KYB for Sole Proprietors and Individual Sellers
KYB is designed for registered legal entities. If you do not operate through one (for example, you sell as a sole proprietor or individual entrepreneur), eligibility and the documents you need are confirmed case by case and depend on your jurisdiction. Raise your situation with the sales team before signing the Order Form so the compliance team can tell you what applies to you.
In the meantime, you can build and test your full integration in staging, which does not require KYB. See [Staging vs Production Environments](/introduction/platform/staging-vs-production).
# Client-side Keys
Source: https://docs.crossmint.com/introduction/platform/api-keys/client-side
How to create and use client-side API keys
## Create a Client-side Key
Navigate to the API Keys section of the developer console and click the "Create new key" button in the client-side keys section.
* [Staging API Keys](https://staging.crossmint.com/console/projects/apiKeys)
* [Production API Keys](https://www.crossmint.com/console/projects/apiKeys)
### Setting Authorized Origins
Client-side keys are exposed in the application and thus require additional security measures. The minimum requirement is to whitelist URLs that requests can be sent from. Client-side keys support two types of origins:
You can only register either web origins or mobile app identifiers for a single API key, not both. If you need both
types, you will need to create separate API keys.
#### Web Origins
For web applications, you need to add the domains that requests will be sent from. In development, you will need to add the local domain you test your application from. This is commonly `http://localhost` followed by a port number such as `3000`, `5173`, etc. For example, when developing with NextJS, the default origin you need to authorize is `http://localhost:3000`.
The expected format for web origins is a full URL with protocol, such as `https://www.yourdomain.com` or `http://localhost:3000`.
#### Mobile App Identifiers
For mobile applications, you need to add the bundle identifiers for iOS apps or package names for Android apps. The expected format is:
* iOS: Bundle ID (e.g., `com.company.appname`)
* Android: Package name (e.g., `com.company.appname`)
Mobile app identifiers can be arbitrarily spoofed by malicious actors, as the headers sent from mobile applications can be modified. Do not rely solely on mobile app identifiers for security. Consider implementing additional authentication mechanisms such as JWT authentication for sensitive operations.
#### Desktop/CLI Applications
For desktop applications and CLI tools where origin headers cannot be reliably set, you can select the "Desktop/CLI" app type when creating your client-side API key. This option allows the key to be used without origin restrictions.
**Security Warning**: Desktop/CLI keys have no origin-based protection. This means:
* The key can be used from any origin or application
* If the key is leaked, it can be used by anyone without restriction
* Consider enabling JWT authentication for additional security
* Only use this option when absolutely necessary (e.g., CLI tools, desktop apps, AI agents)
For most use cases, Crossmint recommends using web origins or mobile app identifiers instead.
To create a Desktop/CLI key:
1. Select "Desktop/CLI" as the app type when creating or editing a client-side key
2. Consider enabling JWT authentication for additional security
3. Store the key securely and rotate it regularly
When you create a production API key you will need to authorize your production domain or app identifiers to use the API key. You can add multiple authorized domains or app identifiers for an API key to make requests from. Type in the domain or app identifier that you want to authorize and then click the "+ Add new origin" button.
### Select Scopes
Within the modal that opens, toggle the required scopes you want to enable. You may need to expand an accordion for the product area you are working on to expose additional scope options.
For more information on API Key scopes visit the scopes page or the API Reference.
Complete list of available API scopes
Detailed docs for all API endpoints
### JWT Authentication
Finally, select the option to require a JWT if your application or use case requires it. Enabing this setting will require that users are authenticated to permit API requests.
The [Wallets SDK](/sdk-reference/wallets/typescript/overview) requires this option to be enabled. It is optional for other
client side APIs. For more information on the options, refer to the [JWT
Authentication](/introduction/platform/api-keys/jwt-authentication) section.
If you choose to enable JWT Authentication for your client-side API key, there are additional configurations that must be made. You can choose between Crossmint authentication (ideal for staging and getting started fast), third party auth providers such as Auth0, Stytch, Firebase, Kinde, or Supabase (recommended for production), or custom solutions where you generate your own JWTs (also recommended for production).
You can find more information and guidance in the [JWT Authentication](/introduction/platform/api-keys/jwt-authentication) section.
***
## Using a Client-side Key
There are a few different approaches to using a client-side key. The most common option is passing it to the `init` function for a supported SDK. There are also some cases where you will pass the key as a header in a raw API call from custom code, similar to how a server-side key works.
### Initializing an SDK
The most common way you will leverage a client-side API key is by passing it to the `init` function for a supported SDK. See the examples below.
```typescript crossmint-wallets-sdk.ts theme={null}
import { CrossmintWallets, createCrossmint } from "@crossmint/wallets-sdk";
const crossmint = createCrossmint({
apiKey: process.env.NEXT_PUBLIC_CLIENT_SIDE_KEY ?? "",
});
const crossmintWallets = CrossmintWallets.from(crossmint);
const wallet = await crossmintWallets.getOrCreateWallet({
chain: "",
signer: {
type: "",
},
});
```
```typescript credentials-init.ts theme={null}
import * as sdk from '@crossmint/client-sdk-verifiable-credentials';
sdk.CrossmintAPI.init(process.env.NEXT_PUBLIC_CLIENT_SIDE_KEY);
```
```tsx auth-example.tsx theme={null}
import { CrossmintProvider, CrossmintAuthProvider } from "@crossmint/client-sdk-react-ui";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
{children}
);
}
```
### Direct API Call From Client
The Headless Checkout is one example where you may be writing custom API calls from your application to create orders. In this case, you set the client-side API key as a header named `X-API-KEY`, much like you would when making a server-side API call.
```typescript theme={null}
const createOrder = async (orderInput: any) => {
try {
const res = await fetch(`https://staging.crossmint.com/api/2022-06-09/orders`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.NEXT_PUBLIC_CLIENT_SIDE_KEY,
},
body: JSON.stringify(orderInput),
});
const order = await res.json();
setOrder(order.order);
setClientSecret(order.clientSecret);
} catch (e) {
console.error(e);
throw new Error("Failed to create order");
}
};
```
# JWT Authentication
Source: https://docs.crossmint.com/introduction/platform/api-keys/jwt-authentication
Enable advanced security for client-side API Keys
JWT-based authentication is a mechanism in which you can authorize a call to a Crossmint API, scoping such calls to impact resources related to a single end-user on your platform. You have three options available when enabling this option.
1. **Crossmint Authentication** - A quick-to-integrate auth solution, ideal for staging environments and getting started fast. Crossmint handles the generation of JWTs and you're able to quickly create a working application with user access controls. **For production, options 2 or 3 below are recommended.**
2. **Third Party Authentication providers** *(recommended for production)* - There are several quality authentication providers in the market and you may already be using one of them. You can continue to use your existing auth tooling and enable Crossmint Wallets on top of them with minimal changes. Supported providers include Auth0, Firebase, Supabase, Stytch, and Kinde.
3. **Custom JWT** *(recommended for production)* - This option is for providers not yet included with turn-key support or custom built solutions where you issue your own JWTs. You must be able to provide a JWKS endpoint to enable Crossmint to authenticate tokens.
Crossmint's Wallets SDK **requires** JWT Authentication be enabled.
## Crossmint Authentication
This option makes the most sense if you're getting started quickly and want to prototype or test in a staging environment. Crossmint Auth is great for demos and quickstarts, but for production applications a [third-party auth provider](/wallets/guides/bring-your-own-auth) or custom JWT is recommended for full control over your user management.
To use this option, select the Crossmint Auth option within the JWT Authentication section of the API keys page in the Developer Console.
## Third Party Authentication
For projects that have already implemented authentication using one of the supported providers, you can provide Crossmint with the appropriate project/environment/app ID from the settings page of the third party. Supported providers include: Dynamic, Auth0, Stytch, Privy, Firebase, Kinde, and Supabase.
To use this option, select the 3P Auth providers option, then choose your provider from the dropdown. Next, you'll need to find the appropriate ID that your provider gives you to identify your project. This can typically be found within the settings pane of their web interface.
## Custom JWT Auth
If your project is using an auth provider not listed in the 3P providers list or is a home-grown JWT solution, you can choose the Custom tokens option. You will need to be able to provide a JWKS endpoint that Crossmint can use to authenticate the JWTs.
For a detailed guide on implementing this approach, check out the [Custom JWT Authentication Guide](/introduction/platform/api-keys/custom-jwt-auth-guide).
# Overview
Source: https://docs.crossmint.com/introduction/platform/api-keys/overview
Get your keys in seconds and start building
API keys are required to authorize requests against the Crossmint APIs. By using an API key, Crossmint knows which project is making the call, and can deduct credits from your balance.
## Staging vs. Production Keys
First, determine if you need a staging (testing) or production API key.
Generate API keys for testing in the staging developer console
Generate API keys for going live in the production developer console
## Server-side vs. Client-side Keys
Server-side API keys are used in server-to-server communications or in code running on a server. These keys are not exposed to the end users and can have broader permissions because they are considered more secure, being stored and used in controlled environments.
The majority of Crossmint APIs require a server-side API key. For a comprehensive list of APIs available refer to
the [API Reference](/api-reference/introduction).
Client-side API keys are used in code that runs on the client-side, such as in web browsers or mobile apps. These keys are exposed to the end user and are therefore less secure. They typically have more restrictive permissions to minimize security risks. When creating a client-side API key, you also need to configure authorized origins that are allowed to make calls to the endpoint.
Client-side keys are required for building with the [Wallets SDK](/sdk-reference/wallets/typescript/overview), Authentication, and [Headless Checkout](/payments/headless/overview).
You can also perform some custodial wallet actions with these key types. Finally, the [Verifiable Credentials SDK](/verifiable-credentials/introduction) also offers some features via client-side keys.
## More information
Information about standard rate limits and exceptions
Complete list of available API scopes
How to create and use server-side API keys
How to create and use client-side API keys
# Rate Limits
Source: https://docs.crossmint.com/introduction/platform/api-keys/rate-limits
Understand the throughput available for API calls
Rate limits prevent individual users from clogging the network. If a limit is exceeded, Crossmint will respond with HTTP 429 and require a brief waiting period before making additional requests.
The rate limits on self-serve plans are the following:
To increase the base rate limits, contact us.
### Base Limits
| Method(s) | Rate Limit |
| :- | :- |
| POST | 120 requests per minute per project |
| PUT | Same as above |
| PATCH | Same as above |
| DELETE | Same as above |
| GET | 360 requests per minute per project |
### Exceptions
| API | Route | Method | Rate Limit |
| :- | :- | :- | :- |
| [Mint NFT](/api-reference/minting/nfts/mint-nft) | /api/2022-06-09/collections/\{collectionId}/nfts | POST | 600 req/min/project |
| [Mint NFT (Idempotent)](/api-reference/minting/nfts/mint-nft-idempotent) | /api/2022-06-09/collections/\{collectionId}/nfts/\{nftName} | PUT | 600 req/min/project |
| [Collection Info](/api-reference/minting/collection/get-collection) | /api/2022-06-09/collections/\{collectionId} | GET | 600 req/min/project |
| [Mint Status](/api-reference/minting/nfts/mint-status) | /api/2022-06-09/collections/\{collectionId}/nfts/\{id} | GET | 600 req/min/project |
| [Get All Tokens](/api-reference/headless/get-all-tokens) | /api/2024-09-26/tokens | GET | 3000 req/min/project |
| [Check Token Support](/api-reference/headless/check-token-support) | /api/2024-09-26/tokens/\{tokenLocator} | GET | 3000 req/min/project |
| [Get User](/api-reference/users/get-user) | /api/2025-06-09/users/\{userLocator} | GET | 80 req/min/project |
| [Create or Update User](/api-reference/users/upsert-user) | /api/2025-06-09/users/\{userLocator} | PUT | 50 req/min/project |
| [Get Identity Verification](/api-reference/users/get-identity-verification) | /api/2025-06-09/users/\{userLocator}/identity-verification | GET | 80 req/min/project |
| [Trigger Identity Verification](/api-reference/users/trigger-identity-verification) | /api/2025-06-09/users/\{userLocator}/identity-verification | PUT | 50 req/min/project |
| [Accept Legal Document](/api-reference/users/accept-legal-document) | /api/2025-06-09/users/\{userLocator}/legal-documents | PUT | 50 req/min/project |
| [Upload User Document](/api-reference/users/upload-document) | /api/2025-06-09/documents | POST | 50 req/min/project |
| [Get User Document](/api-reference/users/get-document) | /api/2025-06-09/documents/\{documentId} | GET | 80 req/min/project |
# Scopes
Source: https://docs.crossmint.com/introduction/platform/api-keys/scopes
Enabling required permissions for API calls
Below is a complete list of the API scopes available. You can also find the scope a specific API requires in the [API Reference](/api-reference/introduction) section.
## Wallet APIs
| Scope | Description | Server Key | Client Key |
| - | - | - | - |
| `wallets.read` | Retrieve all wallets for a user. | ✅ | ✅ |
| `wallets.create` | Create a wallet for a user. | ✅ | ✅ |
| `wallets:nfts.read` | Fetch the NFTs owned by a specific wallet address. | ✅ | ✅ |
| `wallets:balance.read` | Get the balance of a specific wallet address. | ✅ | ✅ |
| `wallets:transactions.create` | Create a transaction from a user's wallet. | ✅ | ✅ |
| `wallets:transactions.sign` | Sign a transaction from a user's wallet. | ✅ | ✅ |
| `wallets:transactions.read` | Read transactions from a user's wallet. | ✅ | ✅ |
| `wallets:signatures.create` | Create a signature for a wallet. | ✅ | ✅ |
| `wallets:signatures.read` | Read a signature for a wallet. | ✅ | ✅ |
| `wallets.fund` | Send funds to a wallet. | ✅ | ✅ |
| `wallets.export` | Export a wallet's encrypted private key material. | | ✅ |
| `wallets:nfts.transfer` | Transfer an NFT from a user's wallet. | ✅ | |
| `wallets:messages.sign` | Sign a message from a user's wallet. | ✅ | |
When using the [Wallets SDK](/sdk-reference/wallets/typescript/overview) you **must** use a client-side API key.
## Authentication
| Scope | Description | Server Key | Client Key |
| - | - | - | - |
| `users.create` | Create users / allow them to sign up. | | ✅ |
| `users.read` | Get profile info for user accounts. | | ✅ |
## Tokenization (Minting) APIs
| Scope | Description | Server Key | Client Key |
| - | - | - | - |
| `nfts.create` | Mint your NFTs and deliver them to a wallet or to an email address. | ✅ | |
| `nfts.update` | Update a minted NFT's metadata on IPFS (image, description, name...). | ✅ | |
| `nfts.read` | Retrieve all metadata for an NFT. | ✅ | |
| `nfts.delete` | Burn a specific NFT within a collection. | ✅ | |
| `nfts.transfer` | Transfer an NFT to a different wallet. | ✅ | |
| `collections.create` | Create a collection of NFTs. | ✅ | |
| `collections.update` | Update information for an existing collection (image, name, royalties...). | ✅ | |
| `collections.read` | Retrieve the information about a specific collection. | ✅ | |
| `credentials.read` | Fetch credentials, some endpoints will only work with a server side key. | ✅ | ✅ |
| `credentials.decrypt` | Decrypt credentials, mainly used by the client-side SDK. | ✅ | ✅ |
| `credentials:templates.create` | Create a template for your credentials. | ✅ | |
| `credentials.create` | Issue your credentials and create credential types | ✅ | |
| `credentials.delete` | Revoke a credential issued to a subject. | ✅ | |
## Checkout APIs
| Scope | Description | Server Key | Client Key |
| - | - | - | - |
| `orders.create` | Create an order for headless checkout. | ✅ | ✅ |
| `orders.read` | Get an existing order for headless checkout. | ✅ | ❌ |
| `orders.update` | Update an existing order for headless checkout. | ✅ | ❌ |
Client-side API keys only have access to the `orders.create` scope. When reading or updating order status from the
client-side, you must pass the `clientSecret` returned in the create-order call as an `authorization` header for
subsequent order operations (get-order, update-order). The `clientSecret` provides the authorization and an API Key
is not required in this use case. See [this
guide](/payments/headless/guides/client-or-server#client-side-example-code) in the Headless Checkout docs.
## Project Administration
| Scope | Description | Server Key | Client Key |
| - | - | - | - |
| `billing.readonly` | Get balance in credits for a project. | ✅ | |
| `projects:usage.read` | Get usage for the different products in a project | ✅ | |
# Server-side Keys
Source: https://docs.crossmint.com/introduction/platform/api-keys/server-side
How to create and use server-side API keys
**Server-side API keys must be stored securely!**
Enable only the scopes you need, and no more, and do NOT expose your keys on the frontend of your app, or your github code repository.
## Create a Server-side Key
Navigate to the API Keys section of the developer console and click the "Create new key" button in the server-side keys section.
* [Staging API Keys](https://staging.crossmint.com/console/projects/apiKeys)
* [Production API Keys](https://www.crossmint.com/console/projects/apiKeys)
### Select Scopes
**Production server-side keys follow a "view once" policy**
When you create a server-side API key in the production environment, the key secret will only be shown once during creation. After you close the dialog or navigate away, **you will not be able to view the key secret again.**
Within the modal that opens, toggle the required scopes on and click the "Create server key" button at the bottom. You may need to expand an accordion for the product area you're working on to expose additional scope options.
You can determine the scopes you need by visiting the API reference page for the API(s) you need to interact with.
Complete list of available API scopes
Detailed docs for all API endpoints
## Using a Server-side Key
If you're using the [API Playground](/api-reference) you can add your API key in the Authorization section and this will set it as a header when making the request.
When calling the APIs from server-side code, you set the `X-API-KEY` header however the target language or library expects it. See some examples below:
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/v1-alpha1/wallets \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: YOUR_API_KEY' \
--data '{
"email": "test@test.com",
"chain": "polygon-amoy"
}'
```
```javascript Node.js theme={null}
const options = {
method: 'POST',
headers: {
'X-API-KEY': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: '{
"email":"test@test.com",
"chain":"polygon-amoy"
}'
};
fetch('https://staging.crossmint.com/api/v1-alpha1/wallets', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
All of the API routes in the [API Reference](/api-reference/introduction) include code examples for many popular programming languages.
# Organizations
Source: https://docs.crossmint.com/introduction/platform/organizations
How an organization groups your projects, your team, and the security settings that apply to them
An organization is the top-level container in the Crossmint console. It holds your projects and the team that works on them.
* A **project** contains the resources you build with: token collections, user wallets, API keys, billing information, and more.
* An **organization** contains one or more projects, plus the members, roles, and security settings that apply to all of them.
Every project belongs to exactly one organization. You invite people to the organization once, and they get access to every project it contains.
## Personal and Team Organizations
Every account starts with a personal organization. It is your own space, you are its only member, and it never changes hands.
A team organization is the one you create to work with other people. Create it from the organization menu in the console, name it, and invite your teammates. You can belong to several organizations at once, for example your own and your employer's, and switch between them from the same menu. Each organization keeps its own projects, and the project list in the sidebar always shows the organization you are in.
Organizations follow the environment boundary described in [Staging vs Production](/introduction/platform/staging-vs-production): an organization in staging and an organization in production are separate, each with its own members, invitations, and authentication setup.
## Roles
Each member holds one role in each organization. The same person can be an admin in one organization and a developer in another.
| | Developer | Admin | Owner |
| - | :-: | :-: | :-: |
| Work in the organization's projects and manage their API keys | ✅ | ✅ | ✅ |
| Create projects | ✅ | ✅ | ✅ |
| Invite members, change their role, and remove them | ❌ | ✅ | ✅ |
| Edit the organization's settings and security | ❌ | ✅ | ✅ |
| Transfer ownership | ❌ | ❌ | ✅ |
Developer is the least privileged role and the default for a new invitation. Ownership is never handed out as a role: an organization has exactly one owner at a time.
## Project Access Follows the Organization
Every member of an organization has access to all of its projects; there are no per-project invitations. Adding someone to the organization grants that access, removing them revokes it, and their organization role decides what they can do in each project. Admins and owners can also manage who is listed on a project.
A project can additionally be shared with someone outside the organization. That person is listed on the project itself, with a Developer or an Admin role, and sees only that project.
The Members tab of a project therefore shows two kinds of rows:
* **Members of the organization**: they have access to the project through the organization. Change their role or remove them in the organization settings, not on the project.
* **Members listed on the project**: shared with this project alone. Change their role or remove them on the project.
A member of the organization can also be listed on a project itself, and that row decides what they can do there: an admin of the organization listed as a developer on a project is a developer in that project.
## Transferring Projects Between Organizations
A project moves to another organization from the Transfer card in its **Settings > General** page. It can go between any two organizations you belong to in the same environment, personal or team, one project at a time.
Transferring a project takes two roles:
* **Admin of the project**: an owner or admin of the organization the project is in, or a member listed on the project as an admin.
* **Owner or admin of the destination organization**: only these organizations appear in the list of destinations.
After you confirm, the console switches to the destination organization, where the project is now listed.
A transfer changes who reaches the project through an organization, and nothing else:
* The API keys, wallets, and billing of the project stay the same.
* Everyone listed on the project keeps their access and their role.
* Every member of the destination organization gets access, with the role they hold there.
* Members of the previous team organization who are not listed on the project lose access.
You cannot transfer the only project in an organization. Create another project there first.
## Invitations
Admins and owners invite people by email address from the members section of the organization settings, and pick the role each one starts with.
* The invited person does not need a Crossmint account yet. The email links to the console, where they sign in or create the account that accepts the invitation.
* A pending invitation waits in the organization menu of the person invited, so they can accept it whenever they next sign in.
* Admins and owners resend or revoke a pending invitation from the same members section.
## MFA and SSO support
Crossmint offers extra authentication features to enterprise organizations: two-factor authentication with an authenticator app (TOTP), and single sign-on through your own identity provider.
## Ownership
A team organization has exactly one owner, and only the owner transfers ownership, from the members list. The member who receives it becomes the owner, and the previous owner stays in the organization as an admin. A personal organization cannot be transferred.
## See Also
What a project holds, and the privacy policy it needs to show user data
Why your staging and production accounts are entirely separate
The keys a project issues, and the scopes they carry
Verifying your account before going live in production
# Project and Team Management
Source: https://docs.crossmint.com/introduction/platform/projects-and-user-management
Manage your projects and their members
## Managing Projects
A project contains the resources you build with: token collections, user wallets, API keys, billing information, and more.
Every project belongs to one [organization](/introduction/platform/organizations), which holds the team that has access to it.
If you are working with different teams or clients, you can create different projects to seggregate sensitive information and tool access. Alternatively, you can issue different API keys within the same project, but bear in mind they will share the same billing account.
Every developer account automatically includes a project named `Default Project`. You can create new projects and alternate between them using the selector within the console, as illustrated in the screenshot below.
## Privacy Policy
Projects have an option to register a link to your company's privacy policy. If set, this link will be shown to your users when they're using any Crossmint-managed user interface, such as the Checkout, where user information is collected. A privacy policy is required by law if you want to later those users' personally identifiable information, such as their email address.
If you don't already have a privacy policy, you can generate and host one using tools like [Termly](https://termly.io/).
> **Note:** Failure to add a privacy policy will result in email addresses being hidden.
#### Cases Requiring a Privacy Policy to Access User Data
| Use Case | Privacy Policy Required |
| - | - |
| Payments (payment button) | Yes |
| Payments (embedded checkout) | Yes |
| Payments (headless checkout) | No |
| Mint API or console | No |
| Wallet APIs or console | No |
### Steps to Add a Privacy Policy Link
1. **Go to the General Settings tab**
* Access the Settings page from the sidear.
2. **Include the Privacy Policy Link**
* In the project settings, locate the section for adding a privacy policy.
* Paste the URL of your privacy policy generated from Termly or any other source.
4. **Verify Email Visibility**
* After adding the privacy policy link, check to ensure that email addresses are now visible.
* If the link is not included, emails will remain hidden to protect user data.
Following these steps will ensure compliance with data protection laws and enable the visibility of email addresses in your project settings.
***
## FAQs
Crossmint requires your project's privacy policy to comply with data protection laws and ensure the privacy and
security of user data. Without it, email addresses and other sensitive information will be hidden.
Older users who joined before the privacy policy was added haven't accepted the new policy. Therefore, their
identifiers remain hidden.
Deleting the privacy policy is restricted to ensure compliance with data protection laws and maintain user
privacy and security.
## Who Has Access to a Project
Access to a project comes from its organization: every member of the organization has access to every project it contains. You invite people once, to the organization, and their role there decides what they can do in its projects. See [Organizations](/introduction/platform/organizations) for the roles and the invitation flow.
A project can additionally be shared with someone outside the organization. Invite them by email from the Members tab of the project, and give them one of two roles:
* **Developer**: works in the project, including its settings, API keys, collections, and integrations
* **Admin**: everything a developer can do, and invites, removes, and assigns roles to the members listed on the project
The Members tab lists both kinds of people. A member who has access to the project through the organization is managed in the organization settings, so the project offers no role change or removal for that row.
# Staging vs Production Environments
Source: https://docs.crossmint.com/introduction/platform/staging-vs-production
Understand the difference between staging and production environments before building
Crossmint's console and APIs operate in two fully separate environments:
* **Staging:** free, for development and testing. Connected to blockchain **testnets**.
* **Production:** for your live application. Connected to blockchain **mainnets**. Real assets and real costs apply.
You access them at [https://staging.crossmint.com](https://staging.crossmint.com) and [https://www.crossmint.com](https://www.crossmint.com).
The two environments are isolated: each has its own console login, its own API keys, and its own wallets. Data does not cross between them. You typically build your integration in staging, then replicate it in production when you are ready to launch.
Staging provides free self-serve access to most functionality, so you can build a complete integration before spending anything.
## Differences Between the Environments
| | **Staging** | **Production** |
| - | - | - |
| URL | staging.crossmint.com | [www.crossmint.com](http://www.crossmint.com) |
| API keys | Start with `ck_staging_` (client) or `sk_staging_` (server) | Start with `ck_production_` (client) or `sk_production_` (server) |
| Blockchains | Testnets like Base Sepolia and Polygon Amoy | Mainnets like Base and Polygon |
| Wallets | Live on testnets only | Live on mainnets only |
| Gas fees | Sponsored by Crossmint at no cost | Sponsored by Crossmint, billed to your account |
| Tokens | Test tokens with no value (for example, USDXM) | Real assets (for example, USDC) |
| Payments | Test credit cards | Real credit cards |
| Verification | No KYC or collection verification required | KYC and collection verification required |
## Blockchain Networks and Wallet Addresses
Staging operates on testnets, and production operates on mainnets. When you create a wallet in staging on a chain like Polygon, it is deployed on the corresponding testnet (Polygon Amoy) rather than Polygon mainnet. See [Supported Chains](/introduction/supported-chains) for the mainnet and testnet targeted by each chain.
Because the two environments are isolated, a wallet you create in staging **does not share its address with production**. Addresses are derived per environment, so the same signer or configuration produces different addresses in staging and production.
This has one important consequence:
Never send real (mainnet) funds to a wallet address generated in staging. Staging wallets exist only on testnets, and Crossmint cannot access or recover assets sent to that address on a mainnet. Use only test tokens with staging wallets.
## Gas Fees
Crossmint sponsors gas fees for wallet transactions in both environments, so your users never need to hold native tokens. In staging, this sponsorship is free. In production, Crossmint still sponsors the gas but bills the cost to your account, so it becomes a real operating expense to plan for. You can adjust or disable this behavior in the [gas sponsorship](/wallets/guides/gas-sponsorship) settings.
## Test Tokens vs Real Assets
In staging you fund wallets with test tokens that have no monetary value, so you can simulate transfers, balances, and gas without financial risk. USDXM is Crossmint's staging stablecoin — think of it as the USDC of testnets. See [Fund a Staging Wallet](/wallets/guides/fund-staging-wallet) for the ways to get it.
In production, wallets hold real assets such as USDC. USDXM is not available in production. When you move over, update any token references (for example, from `usdxm` to `usdc`) alongside the chain and API key changes below.
## Moving to Production
When your integration works in staging, launching in production requires a few deliberate changes:
1. Create a production account and project at [www.crossmint.com/console](http://www.crossmint.com/console)
2. Create a production API key with the scopes your integration needs
3. Update your API endpoint from `staging.crossmint.com` to `www.crossmint.com`
4. Replace your staging API key with the production key
5. Switch chains from testnets to their mainnet equivalents (for example, `base-sepolia` to `base`)
6. Replace test tokens with real assets (for example, `usdxm` to `usdc`)
7. Re-create your wallets in production, since staging wallets and their addresses do not carry over
8. Account for gas sponsorship as a real cost, and fund your wallets with real assets
## FAQ
No. The environments are isolated. Wallets, collections, and API keys created in one environment do not appear in the other. You typically develop in staging, then replicate and launch in production when you are ready.
No. Wallet addresses are derived per environment, so a staging wallet has a different address than a production wallet, even with the same signer or configuration. Re-create your wallets when you move to production.
Staging wallets live only on testnets. If you send mainnet assets to a staging wallet address, Crossmint has no wallet at that address on the mainnet and cannot access or recover the funds. Only use test tokens with staging wallets.
No. Gas is sponsored for free in staging. In production, Crossmint sponsors gas by default but bills the cost to your account.
# Add an Endpoint
Source: https://docs.crossmint.com/introduction/platform/webhooks/add-endpoint
Configure an endpoint to receive webhook messages.
To add an endpoint, provide a URL you control and select the event types you want to listen to.
1. Navigate to the Webhooks page in the console.
2. Click **Add Endpoint**.
3. Provide the URL where you want to receive messages.
4. Select the event types you want to listen to.
5. Click **Create**.
### FAQs
There are many events available to subscribe to from all Crossmint products. Some examples include:
* Collection creation
* NFT minting
* NFT edits
* Transaction confirmations
* Transaction failures
You can see the full list of events in the Crossmint Console, under Integrate > Webhooks and the **Event Catalog** tab.
To verify a webhook request is legitimate, you need to verify the signature and timestamp. You can learn more about it in the [Verify webhooks](/introduction/platform/webhooks/verify-webhooks) section.
Crossmint automatically retries webhooks if your endpoint doesn't acknowledge its receipt, or throws an error. Crossmint attempts to deliver the webhook 8 times:
* Immediately
* 5 seconds
* 5 minutes
* 30 minutes
* 2 hours
* 5 hours
* 10 hours
* 10 hours (in addition to the previous)
If the message cannot be delivered after these attempts, it is marked as failed. Inside the Webhooks page, you can manually resend the webhook.
To indicate that a webhook has been processed, return a 2xx (status code 200-299) response to the webhook message within a 15 seconds timeframe.
Some typical reasons why webhooks fail are:
* Check that the enpdpoint URL is correct and that it's expecting a POST request
* Check that the endpoint is reachable from the public internet. Make sure that CSRF protection is disabled for this endpoint.
* Check that the endpoint is returning a 2xx response code
* Check that the payload signature and timestamp are verified correctly. Remember not to modify the body string of the webhook before processing it.
# Best Practices
Source: https://docs.crossmint.com/introduction/platform/webhooks/best-practices
Best practices for implementing reliable webhook handlers
Follow these best practices to ensure your webhook implementation is reliable, secure, and performant.
## Idempotency
Crossmint webhooks follow an at-least-once delivery guarantee. Duplicate deliveries can occur (for example, if a previous attempt didn't receive a 2xx response quickly enough). Implement idempotency by:
1. Using the webhook `id` field as a unique identifier
2. Storing processed webhook IDs in your database
3. Checking if a webhook has already been processed before taking action
> **Note:** The following code examples are pseudocode for illustration purposes.
```javascript theme={null}
async function handleWebhook(webhookId, data) {
// Check if we've already processed this webhook
const existing = await db.webhooks.findOne({ id: webhookId });
if (existing) {
console.log('Webhook already processed:', webhookId);
return;
}
// Process the webhook
await processWebhook(data);
// Mark as processed
await db.webhooks.create({ id: webhookId, processedAt: new Date() });
}
```
## Response Time
Your webhook endpoint must respond within 15 seconds, otherwise the webhook will be resent. For long-running operations:
If your endpoint doesn't return a 2xx status code within 15 seconds, the delivery attempt is marked as failed and Crossmint will automatically retry with exponential backoff. This can result in duplicate deliveries (at-least-once semantics). After repeated failures, the message is marked as undeliverable and you can manually trigger redelivery from the Console. See [Add an Endpoint](/introduction/platform/webhooks/add-endpoint) for the full retry schedule.
1. Immediately acknowledge the webhook with a 200 response
2. Queue the processing work for asynchronous execution
3. Process the webhook data in a background job
> **Note:** The following code examples are pseudocode for illustration purposes.
```javascript theme={null}
app.post('/webhooks/your-endpoint', async (req, res) => {
// Verify signature first
const payload = verifyWebhook(req);
// Immediately acknowledge receipt
res.status(200).json({ received: true });
// Queue for background processing
await jobQueue.add('process-webhook', {
webhookId: payload.id,
type: payload.type,
data: payload.data,
});
});
```
## Error Handling
Implement proper error handling to ensure reliable webhook delivery:
* **Return 2xx within 15 seconds** after verifying the signature to acknowledge receipt
* **Return 400** for invalid signatures to reject the webhook
* **Return 5xx** for transient internal errors to trigger automatic retries
See [Verify Webhooks](/introduction/platform/webhooks/verify-webhooks) for more details on signature verification and error handling.
## Signature Verification
Always verify webhook signatures to ensure requests are legitimate and come from Crossmint. Never process webhooks without verifying their signatures first.
See [Verify Webhooks](/introduction/platform/webhooks/verify-webhooks) for detailed instructions on signature verification.
## Related Resources
* [Add an Endpoint](/introduction/platform/webhooks/add-endpoint) - Configure webhook endpoints
* [Verify Webhooks](/introduction/platform/webhooks/verify-webhooks) - Verify webhook signatures
* [Overview](/introduction/platform/webhooks/overview) - Learn about webhooks
# Overview
Source: https://docs.crossmint.com/introduction/platform/webhooks/overview
Listen for events across wallets, payments, minting, and credentials to automatically trigger reactions in your application
Crossmint's platform operations—including wallet transactions, checkout, NFT minting, and credential issuance—often require blockchain interactions or other asynchronous processing. These operations can take anywhere from a few seconds to several minutes depending on network conditions. Webhooks allow you to listen for events across Crossmint's platform and automatically trigger reactions in your application when these operations complete.
Some cases where you may want to listen for event notifications include:
* **Wallet operations**: Track transfer approvals, transaction completions, failures, or private key exports
* **Checkout**: Monitor checkout lifecycle from quote creation to delivery completion
* **Minting operations**: Get notified when mints succeed, collections are created, or tokens are transferred
* **Credential issuance**: Receive updates when verifiable credentials are successfully issued
* **Database updates**: Automatically update your records with transaction IDs, token details, or user information
* **Customer notifications**: Send emails or push notifications when operations complete
Webhooks are how services notify each other of events. At their core, they are simply POST requests to a pre-determined endpoint. The endpoint can be any URL you choose, and you can [add them from the console](/introduction/platform/webhooks/add-endpoint).
Your server must return a 2xx HTTP status quickly so the webhook is marked as delivered.
## Next Steps
* [Add an Endpoint](/introduction/platform/webhooks/add-endpoint) - Configure your webhook endpoint
* [Verify Webhooks](/introduction/platform/webhooks/verify-webhooks) - Learn how to verify webhook signatures
* [Best Practices](/introduction/platform/webhooks/best-practices) - Implement reliable webhook handlers
# Verify Webhooks
Source: https://docs.crossmint.com/introduction/platform/webhooks/verify-webhooks
Verify the signature and timestamp when processing webhooks
Because of the way webhooks work, attackers can impersonate services by simply sending a fake webhook to an endpoint. Think about it: it’s just an HTTP POST from an unknown source. This can create a potential security vulnerability for many applications, or at the very least, a source of errors.
To prevent this, Crossmint signs every webhook and its metadata with a unique key for each endpoint. Use this signature to verify that the webhook indeed comes from Crossmint, and process it only if it does.
You can get the signing key secret from the Webhooks page in the console. To find it, go to the endpoint details and look for the Signing Secret section.
This guide uses the Svix open-source library to verify webhooks. First, install the relevant libraries for your language:
```js JavaScript theme={null}
npm install svix
// Or
yarn add svix
```
```python Python theme={null}
pip install svix
```
```rust Rust theme={null}
http = "1.0.0"
svix = "1.20.0"
```
```go Go theme={null}
go get github.com/svix/svix-webhooks/go
```
```java Java theme={null}
// Gradle: Add this dependency to your project's build file:
implementation "com.svix:svix:0.x.y"
// Maven: Add this dependency to your project's POM file:
com.svixsvix0.x.y
```
```kotlin Kotlin theme={null}
// Gradle: Add this dependency to your project's build file:
implementation "com.svix.kotlin:svix-kotlin:0.x.y"
// Maven: Add this dependency to your project's POM file:
com.svix.kotlinsvix-kotlin0.x.y
```
```ruby Ruby theme={null}
gem install svix
```
```csharp C# theme={null}
dotnet add package Svix
```
```php PHP theme={null}
composer require svix/svix
```
```bash CLI theme={null}
# On macOS install via Homebrew:
brew install svix/svix/svix
# On Linux install via Scoop:
scoop bucket add svix https://github.com/svix/scoop-svix.git
scoop install svix
```
Next, verify webhooks using the code below. The payload is the raw (string) body of the request, and the headers are the headers passed in the request.
You need to use the **raw request body** when verifying webhooks, as the cryptographic signature is sensitive to even the slightest changes. Watch out for frameworks that parse the request as JSON and then stringify it, as this will break the signature verification.
See examples below for how to get the raw request body with different frameworks.
Remember to get the signature secret from the endpoint details in the console.
```js JavaScript theme={null}
import { Webhook } from "svix";
const secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// These were all sent from the server
const headers = {
"svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp": "1614265330",
"svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
};
const payload = '{"test": 2432232314}';
const wh = new Webhook(secret);
// Throws on error, returns the verified content on success
const payload = wh.verify(payload, headers);
```
```python Python theme={null}
from svix.webhooks import Webhook
secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
# These were all sent from the server
headers = {
"svix-id": "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp": "1614265330",
"svix-signature": "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
}
payload = '{"test": 2432232314}'
wh = Webhook(secret)
# Throws on error, returns the verified content on success
payload = wh.verify(payload, headers)
```
```rust Rust theme={null}
use svix::webhooks::Webhook;
let secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw".to_string();
let mut headers = http::HeaderMap::new();
headers.insert("svix-id", "msg_p5jXN8AQM9LWM0D4loKWxJek");
headers.insert("svix-timestamp", "1614265330");
headers.insert(
"svix-signature",
"v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=",
);
let payload = b"{\"test\": 2432232314}";
let wh = Webhook::new(&secret)?;
wh.verify(&payload, &headers)?;
// returns Ok on success, Err otherwise
```
```go Go theme={null}
import (
svix "github.com/svix/svix-webhooks/go"
)
secret := "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
// These were all sent from the server
headers := http.Header{}
headers.Set("svix-id", "msg_p5jXN8AQM9LWM0D4loKWxJek")
headers.Set("svix-timestamp", "1614265330")
headers.Set("svix-signature", "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=")
payload := []byte(`{"test": 2432232314}`)
wh, err := svix.NewWebhook(secret)
err := wh.Verify(payload, headers)
// returns nil on success, error otherwise
```
```java Java theme={null}
import com.svix.Webhook;
String secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// These were all sent from the server
HashMap> headerMap = new HashMap>();
headerMap.put("svix-id", Arrays.asList("msg_p5jXN8AQM9LWM0D4loKWxJek"));
headerMap.put("svix-timestamp", Arrays.asList("1614265330"));
headerMap.put("svix-signature", Arrays.asList("v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="));
HttpHeaders headers = HttpHeaders.of(headerMap, BiPredicate)
String payload = "{\"test\": 2432232314}";
Webhook webhook = new Webhook(secret);
webhook.verify(payload, headers)
// throws WebhookVerificationError exception on failure.
```
```kotlin Kotlin theme={null}
import com.svix.kotlin.Webhook
val secret = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw";
// These were all sent from the server
val headersMap = mapOf(
"svix-id" to listOf("msg_p5jXN8AQM9LWM0D4loKWxJek"),
"svix-timestamp" to listOf("1614265330"),
"svix-signature" to listOf("v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=")
)
val headers = HttpHeaders.of(headersMap) { _, _ -> true }
val payload = "{\"test\": 2432232314}";
val webhook = Webhook(secret);
webhook.verify(payload, headers)
// throws WebhookVerificationError exception on failure.
```
```ruby Ruby theme={null}
require 'svix'
# These were all sent from the server
headers = {
"svix-id" => "msg_p5jXN8AQM9LWM0D4loKWxJek",
"svix-timestamp" => "1614265330",
"svix-signature" => "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="
}
payload = '{"test": 2432232314}'
wh = Svix::Webhook.new("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw")
# Raises on error, returns the verified content on success
json = wh.verify(payload, headers)
```
```csharp C# theme={null}
using Svix;
using System.Net;
// These were all sent from the server
var headers = new WebHeaderCollection();
headers.Set("svix-id", "msg_p5jXN8AQM9LWM0D4loKWxJek");
headers.Set("svix-timestamp", "1614265330");
headers.Set("svix-signature", "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=");
var payload = "{\"test\": 2432232314}";
var wh = new Webhook("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw/Je4ZJEGP1QFb");
// Throws on error
wh.Verify(payload, headers);
```
```php PHP theme={null}
// import using composers autoload
require_once('vendor/autoload.php');
// or manually
require_once('/path/to/svix/php/init.php');
// These were all sent from the server
$payload = '{"test": 2432232314}';
$header = array(
'svix-id' => 'msg_p5jXN8AQM9LWM0D4loKWxJek',
'svix-timestamp' => '1614265330',
'svix-signature' => 'v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=',
);
// Throws on error, returns the verified content on success
$wh = new \Svix\Webhook('whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw');
$json = $wh->verify($payload, $header);
```
```bash CLI theme={null}
export SVIX_AUTH_TOKEN="AUTH_TOKEN"
svix verify --secret whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw --msg-id msg_p5jXN8AQM9LWM0D4loKWxJek --timestamp 1614265330 --signature v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= '{"test": 2432232314}'
```
This is a manual verification process. Crossmint recommends using a library to verify webhooks automatically.
If you need to verify manually, follow these instructions.
Each webhook call includes three headers with additional information that are used for verification:
* `svix-id`: The unique message identifier for the webhook message. This identifier is unique across all messages, but will be the same when the same webhook is resent (e.g., due to a previous failure).
* `svix-timestamp`: Timestamp in seconds since epoch.
* `svix-signature`: The Base64 encoded list of signatures (space delimited).
### Constructing the Signed Content
The content to sign is composed by concatenating the ID, timestamp, and payload, separated by the full-stop character (`.`). In code, it will look something like this:
```js theme={null}
const signedContent = `${svix_id}.${svix_timestamp}.${body}`;
```
Where `body` is the raw body of the request. The signature is sensitive to any changes, so even a small change in the body will cause the signature to be completely different. This means that you should not change the body in any way before verifying.
Ensure you use the **raw request body** for verification. Parsing and re-stringifying JSON will alter the content and cause verification failure.
### Determining the Expected Signature
Crossmint uses HMAC with SHA-256 to sign webhooks.
To calculate the expected signature, HMAC the `signedContent` from above using the base64 portion of your signing secret (this is the part after the `whsec_` prefix) as the key. For example, if your secret is `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw`, use `MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw`.
For example, this is how you can calculate the signature in Node.js:
```js theme={null}
const crypto = require('crypto');
const signedContent = `${svix_id}.${svix_timestamp}.${body}`;
const secret = "whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH";
// Need to base64 decode the secret
const secretBytes = Buffer.from(secret.split('_')[1], "base64");
const signature = crypto
.createHmac('sha256', secretBytes)
.update(signedContent)
.digest('base64');
console.log(signature);
```
This generated signature should match one of the ones sent in the `svix-signature` header.
The `svix-signature` header is a list of space-delimited signatures with version identifiers. The list is typically one item but can contain multiple signatures, like this:
```text theme={null}
v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= v1,bm9ldHUjKzFob2VudXRob2VodWUzMjRvdWVvdW9ldQo= v2,MzJsNDk4MzI0K2VvdSMjMTEjQEBAQDEyMzMzMzEyMwo=
```
Make sure to remove the version prefix and delimiter (e.g. `v1,`) before verifying the signature.
To compare the signatures, use a constant-time string comparison method to prevent timing attacks.
### Verify timestamp
Crossmint includes the timestamp of the attempt in the `svix-timestamp` header. Compare this timestamp against your system timestamp to ensure it’s within an acceptable tolerance to prevent replay attacks.
# Supported Chains
Source: https://docs.crossmint.com/introduction/supported-chains
Tap into 50+ blockchains in one single integration
Refer to the table below to see which chains are supported for which product categories.
| Blockchain | Wallets | Checkout | Onramp | Tokenization | Offramp | Mainnet Label | Testnet Label |
| :- | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| ApeChain | ✅ | ✅ | ✱ | ✅ | - | `apechain` | `curtis` |
| Arc | ✅ | ✱ | ✱ | ✱ | - | `arc` | `arc-testnet` |
| Aptos | ✱ | ✱ | ✱ | ✅ | - | `aptos` | `aptos` |
| Arbitrum One | ✅ | ✅ | ✱ | ✅ | - | `arbitrum` | `arbitrum-sepolia` |
| Arbitrum Nova | ✅ | ✅ | ✱ | ✅ | - | `arbitrumnova` | `arbitrum-sepolia` |
| Avalanche | ✱ | ✅ | ✱ | ✅ | - | `avalanche` | `avalanche-fuji` |
| Avalanche Subnets | ✱ | ✱ | ✱ | ✱ | - | `avalanche-subnet` | `avalanche-subnet-testnet` |
| Base | ✅ | ✅ | ✅ | ✅ | ✅ | `base` | `base-sepolia` |
| BSC | ✅ | ✱ | ✱ | ✱ | - | `bsc` | `bsc-testnet` |
| Chiliz | ✱ | ✅ | ✱ | ✅ | - | `chiliz` | `chiliz-spicy-testnet` |
| Ethereum | ✱ | ✅ | ✱ | ✱ | - | `ethereum` | `ethereum-sepolia` |
| Flow | ✱ | ✅ | ✱ | ✅ | - | `flow` | `flow-testnet` |
| Mantle | ✅ | ✅ | ✱ | ✅ | - | `mantle` | `mantle-testnet` |
| Monad | ✱ | ✱ | ✱ | ✱ | - | `monad` | `monad-testnet` |
| Optimism | ✅ | ✅ | ✱ | ✅ | - | `optimism` | `optimism-sepolia` |
| Polygon | ✅ | ✅ | ✅ | ✅ | ✅ | `polygon` | `polygon-amoy` |
| Robinhood Chain | ✱ | ✱ | ✱ | ✱ | - | `robinhood-chain` | `robinhood-chain-testnet` |
| Sei | ✅ | ✅ | ✱ | ✅ | - | `sei-pacific-1` | `sei-atlantic-2-testnet` |
| Scroll | ✅ | ✅ | ✱ | ✅ | - | `scroll` | `scroll-sepolia` |
| Solana | ✅ | ✅ | ✅ | ✅ | ✅ | `solana` | `solana` |
| Stellar | ✅ | ✱ | ✅ | ✱ | - | `stellar` | `stellar-testnet` |
| Shape | ✅ | ✅ | ✱ | ✅ | - | `shape` | `shape-sepolia` |
| SKALE | ✱ | ✅ | ✱ | ✅ | - | `skale-nebula` | `skale-nebula-testnet` |
| Story | ✅ | ✱ | ✱ | ✅ | - | `story-mainnet` | `story-testnet` |
| Sui | ✱ | ✱ | ✱ | ✅ | - | `sui` | - |
| Tempo | ✅ | ✱ | ✱ | ✱ | - | `tempo` | `tempo-testnet` |
| Xion | ✱ | ✅ | ✱ | ✅ | - | `xion` | `xion-testnet` |
| Zora | ✅ | ✅ | ✱ | ✅ | - | `zora` | `zora-sepolia` |
| Other EVM chains | ✱ | ✱ | ✱ | ✱ | - | | |
**Legend:**
* `✅` Supported
* `✱` Available, but not self-serve. Contact us to request it for your project.
* `-` Not supported
Notes:
* Fungible token checkout available self service only in Solana. Contact us for other chains.
* **Offramp availability:** Solana, Polygon, and Base.
* **Onramp availability:** Solana, Polygon, and Base are supported in all regions (EU, US, and Rest of World). Stellar is supported in the EU and Rest of World, but not the US. For all other chains, contact sales to request onramp support.
Contact us. Crossmint launches new chains regularly and yours is likely in the pipeline.
## Testnets
You can access testnets by using the staging console. Read more about the different environments [here](/introduction/platform/staging-vs-production).
Please refer to the supported chains table above for details about which testnet the staging environment targets. If there is not a specific testnet listed for a chain it is not supported in staging.
# Connect the Crossmint MCP Server
Source: https://docs.crossmint.com/mcp-server/connect
Let AI assistants manage your Crossmint console projects, organizations, and API keys
The Crossmint MCP server lets AI assistants such as Claude, ChatGPT, Cursor, Claude Code, and Gemini manage your Crossmint console for you. Once connected, an assistant or AI agent can list and create projects, switch organizations, and create, edit, and delete API keys on your behalf.
The server is hosted by Crossmint. There is nothing to install: you add its URL to your AI client and sign in with your Crossmint console account.
This server manages your console. To let an AI assistant search the Crossmint documentation instead, use the [Crossmint Docs MCP server](/introduction/ai-assistants#crossmint-docs-mcp-server).
## Prerequisites
* **Crossmint console account**: An account in the staging console, the production console, or both. See [Staging vs Production](/introduction/platform/staging-vs-production).
* **MCP client**: An AI client that supports remote MCP servers with OAuth.
## Server Details
| Property | Value |
| - | - |
| URL | `https://mcp.crossmint.com/mcp` |
| Transport | Streamable HTTP |
| Authentication | OAuth with dynamic client registration |
| OAuth metadata | `https://mcp.crossmint.com/.well-known/oauth-authorization-server` |
Your client registers itself and opens the Crossmint sign-in in your browser. You do not need an API key to connect.
## Add the Server to Your Client
Claude.ai and Claude Desktop add remote MCP servers as custom connectors. Free plans are limited to one custom connector.
Go to **Customize > Connectors**, select **+ Add**, then **Add custom connector**. On Team and Enterprise plans, an Owner first adds the connector in **Organization settings > Connectors**.
Name the connector `Crossmint`, enter `https://mcp.crossmint.com/mcp`, and select **Continue**. Keep the authentication settings Claude detects.
Finish adding the connector and [sign in](#sign-in). In a conversation, enable the connector from the **+** button, then **Connectors**.
Source: Claude custom connectors.
ChatGPT adds remote MCP servers as custom MCP servers. Account and workspace policies apply.
Go to ChatGPT Plugins, select the plus button, then **Add custom MCP server**.
Enter `Crossmint` as the name and `https://mcp.crossmint.com/mcp` as the URL under **Connection**. Choose OAuth authentication, review the risk warning, and select **I understand and want to continue**.
Select **Create as a plugin** and [sign in](#sign-in). In a conversation, type `@` and select the Crossmint plugin.
Source: Connect from ChatGPT.
Add the Crossmint MCP server to Cursor with one click, or add it to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):
```json mcp.json theme={null}
{
"mcpServers": {
"crossmint": {
"url": "https://mcp.crossmint.com/mcp"
}
}
}
```
When Cursor connects to the server for the first time, complete the [sign-in](#sign-in).
Source: Cursor MCP and Cursor install links.
Add the server:
```bash theme={null}
claude mcp add --transport http crossmint https://mcp.crossmint.com/mcp
```
Start Claude Code, run `/mcp`, select `crossmint`, and [sign in](#sign-in). You can also sign in from your shell with `claude mcp login crossmint`.
Source: Claude Code MCP.
Run **MCP: Add Server** from the Command Palette, choose **HTTP**, enter `https://mcp.crossmint.com/mcp`, and name the server `crossmint`. To share the server with your team, add it to `.vscode/mcp.json` in your project:
```json .vscode/mcp.json theme={null}
{
"servers": {
"crossmint": {
"type": "http",
"url": "https://mcp.crossmint.com/mcp"
}
}
}
```
When VS Code starts the server for the first time, complete the [sign-in](#sign-in).
Source: VS Code MCP servers.
Add the server and sign in:
```bash theme={null}
codex mcp add crossmint --url https://mcp.crossmint.com/mcp
codex mcp login crossmint
```
Source: Codex MCP.
Add the server:
```bash theme={null}
gemini mcp add --transport http crossmint https://mcp.crossmint.com/mcp
```
Start Gemini CLI and run `/mcp auth crossmint` to [sign in](#sign-in).
Source: Gemini CLI MCP servers.
Open the MCP settings in Windsurf and add the server to `~/.codeium/windsurf/mcp_config.json`:
```json mcp_config.json theme={null}
{
"mcpServers": {
"crossmint": {
"serverUrl": "https://mcp.crossmint.com/mcp"
}
}
}
```
When Windsurf connects to the server for the first time, complete the [sign-in](#sign-in).
Source: Windsurf MCP.
Clients that support remote MCP servers with OAuth connect to `https://mcp.crossmint.com/mcp` directly. For clients that only run local MCP servers, use the mcp-remote proxy, which runs locally and opens the [sign-in](#sign-in) in your browser:
```json theme={null}
{
"mcpServers": {
"crossmint": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.crossmint.com/mcp"]
}
}
}
```
## Sign In
When your client connects for the first time, it opens the Crossmint sign-in in your browser.
Select **Production** or **Staging**. The connection works in that environment only. See [Staging vs Production](/introduction/platform/staging-vs-production).
Sign in to the console of the environment you selected, as you do in the browser.
Select the organization the connection starts in, then select **Authorize**. If the organization requires MFA or SSO, the console asks you to complete it.
When the browser shows **You are all set**, return to your client. To check the connection, ask the assistant which Crossmint account it is connected to. The assistant calls `whoami`, which returns your email, environment, and organization.
The connection has full access to the organization you select: read and write access to all of its projects, with the permissions of your console account. That includes creating and deleting API keys.
## Switch Organization or Environment
A connection works in one environment and one organization at a time.
* **Organization**: Ask the assistant to switch organization. It calls `orgs_list` and `orgs_select`. If the organization requires MFA or SSO, the assistant returns a link instead: open it in your browser and complete the step in the console.
* **Environment**: Ask the assistant to work in the other environment. It calls `env_select`, which returns a link: open it in your browser, sign in to that environment with the same email, and choose an organization. You can also remove the server from your client and connect again.
Links from `orgs_select` and `env_select` work once and expire after 10 minutes. After a switch, project IDs from the previous organization or environment no longer apply: ask the assistant to list the projects again.
## Example Prompts
* "Which Crossmint organization am I connected to?"
* "List my Crossmint projects."
* "Create a project called Loyalty App and a server API key in it with the `wallets.read` and `wallets.create` scopes."
* "Show me the client keys of my Loyalty App project."
* "Switch to my Acme organization."
* "Delete the API key I created yesterday in Loyalty App."
For the full list of tools and parameters, see [Crossmint MCP Server Tools](/mcp-server/tools).
## Security
* **Review tool calls**: Keep your client set to ask for approval before it runs tools, and review each call before you approve it, especially in production.
* **Deleting keys cannot be undone**: `keys_delete` only deletes a key when called with `confirm: true`. Any application that uses a deleted key stops working.
* **Production server secrets are shown once**: `keys_create` returns the secret of a production server key, and `keys_list` masks it afterwards. Store the secret before you close the conversation, and treat the conversation history as sensitive.
* **Prompt injection**: Content from other tools, files, or websites can contain instructions aimed at your assistant. Do not connect the Crossmint MCP server alongside sources you do not trust.
* **Use staging to experiment**: Connect to staging when you try the server out or automate tasks for the first time.
* **Use the official URL**: Only connect to `https://mcp.crossmint.com/mcp`.
A connection stays signed in for up to 30 days. Removing the server from your client stops that client from using it, but the server only revokes the connection credentials when the client sends a revocation request. Credentials that are not revoked expire within 30 days.
## Troubleshooting
The sign-in must finish in the browser where it started, within 10 minutes. Connect again from your client and complete the sign-in in the browser it opens.
Links from `orgs_select` and `env_select` work once and expire after 10 minutes. Ask the assistant for a new link.
An environment switch link belongs to the email the connection is signed in with. Sign out of the console of that environment, sign in with the same email, and open the link again.
Project IDs belong to one organization and environment. Ask the assistant to call `whoami` and list the projects again.
A connection stays signed in for up to 30 days. When the console session ends, connect again from your client.
Wait a few minutes, then connect again.
## Next Steps
Every tool the Crossmint MCP server exposes
Manage projects and API keys from your terminal
# Crossmint MCP Server Tools
Source: https://docs.crossmint.com/mcp-server/tools
Reference for the tools the Crossmint MCP server exposes to AI assistants
The Crossmint MCP server exposes the following tools. Your AI assistant chooses which tool to call based on your request. To connect a client, see [Connect the Crossmint MCP Server](/mcp-server/connect).
A connection is signed in to one environment (production or staging) and one organization. Every tool runs in that environment and organization.
## Session
| Tool | Description | Parameters |
| - | - | - |
| `whoami` | Returns the signed-in email, the environment, and the active organization. | None |
| `env_select` | Returns a link to open in a browser to move the connection to another environment. In the browser, you sign in to that environment and choose an organization. | `environment`: `production` or `staging` |
## Organizations
| Tool | Description | Parameters |
| - | - | - |
| `orgs_list` | Lists the organizations you belong to and the ID of the active one. | None |
| `orgs_current` | Returns the active organization. | None |
| `orgs_select` | Switches the connection to another organization. For an organization that requires MFA or SSO, returns a link to open in a browser instead. | `organizationId`: as listed by `orgs_list` |
## Projects
The server does not keep a selected project. `projects_details` and every API key tool take the `projectId` of the project to work on, as returned by `projects_list` or `projects_create`. Project IDs belong to one organization and environment: after a switch, list the projects again.
| Tool | Description | Parameters |
| - | - | - |
| `projects_list` | Lists the projects in the active organization. | None |
| `projects_details` | Returns the details of a project. | `projectId` |
| `projects_create` | Creates a Smart Wallets project in the active organization and returns its ID. | `name` |
## API Keys
| Tool | Description | Parameters |
| - | - | - |
| `keys_list` | Lists the API keys of a project. Production server key secrets are masked. | `projectId`, `type` (optional): `server` or `client` |
| `keys_create` | Creates an API key in a project. A production server key secret is only returned in this response. | `projectId`, `usageOrigin`, `scopes`, `clientType`, `whitelistedOrigins`, `whitelistedAppIdentifiers` |
| `keys_edit` | Replaces the scopes of an API key. | `projectId`, `keyId`, `scopes` |
| `keys_delete` | Deletes an API key. The deletion cannot be undone. | `projectId`, `keyId`, `confirm`: must be `true` |
### `keys_create` Parameters
| Parameter | Description |
| - | - |
| `projectId` | The project to create the key in. |
| `usageOrigin` | `server` for a key used from a backend, or `client` for a key used from an app. |
| `scopes` | The [API key scopes](/introduction/platform/api-keys/scopes), for example `wallets.read`. The server rejects scopes the key cannot have and returns the allowed list. |
| `clientType` | Client keys only: `web`, `mobile`, or `desktop`. |
| `whitelistedOrigins` | Web client keys only: the allowed origins, for example `https://www.yourdomain.com`. |
| `whitelistedAppIdentifiers` | Mobile client keys only: the iOS bundle IDs or Android package names allowed to use the key. |
`keys_edit` replaces the full list of scopes. To add a scope, pass the existing scopes and the new one.
## See Also
Add the Crossmint MCP server to your AI client
Scopes available to server and client keys
# Overview
Source: https://docs.crossmint.com/minting/introduction
Mint and distribute tokens at scale, reliably
**It takes months to build secure and reliable minting infrastructure:**
1. Write and maintain token smart contracts
2. Build a backend for orchestrating all blockchain transactions
3. Securely administer keys and crypto balances to pay for gas fees
4. Add queueing, batching, RPC redundancy, priority gas fee estimations, and e2e observability
**Crossmint manages all of this for you via a suite of APIs to create, update, delete and manage tokens of any type through REST APIs, available on any chain.**
## Key Characteristics
Simple APIs to help you launch faster.
Forget about smart contracts, IPFS, wallets, and admin key management.
Let Crossmint handle all the infrastructure, or combine it with your own.
SOC-II certified with audited smart contracts.
Handle millions of tokens an hour and save on customer support.
Tap into 50+ chains with one single integration.
## Quickstarts
Mint and verify unique digital assets you can own, transfer, and verify in under minutes.
Mint limited sets of identical digital assets in under 5 minutes.
Mint credentials that users control, share, and verify anywhere in under 5 minutes.
Mint IP credentials for creators to secure their work, in under 5 minutes.
## Get Started
Get API keys and manage collections.
Test any API in seconds directly from the docs.
Contact our sales team for advanced support.
## FAQs
No, you can migrate providers anytime, transfering ownership of the smart contracts to an external wallet.
Yes, you can transfer ownership at any time. However, note that Crossmint APIs (eg. minting, editing NFTs) may
stop working on that contract unless you whitelist Crossmint on the contract to continue executing those
actions. If you transfer ownership of the contract to your own wallet, you are responsible for ensuring the
security of that wallet.
When you create an account, Crossmint automatically creates a gas station to manage network fees on your behalf. You will be billed in fiat and Crossmint will ensure you never run out of funds to support customer operations. This eliminates the need to build a cryptoaccounting system, execute recurring and expensive crypto purchases, holding the FX risk, and automating treasury operations.
When you create an account, Crossmint will automatically create a dedicated MPC vault to secure your token contracts which you can manage via API. You get one vault per project, in case you are working with multiple teams. You can transfer ownership of the contracts to an external wallet at any time.
# Bring Your Own Contract
Source: https://docs.crossmint.com/minting/nfts/integrate/bring-your-own-contract
Use Crossmint's infrastructure with your own smart contract
Crossmint allows you to use your own smart contract while leveraging our infrastructure for minting NFTs. This guide will walk you through the process of integrating your custom smart contract with Crossmint.
## Prerequisites
Before you can use your own smart contract with Crossmint, ensure that:
1. Your smart contract has a **free minting function** that allows Crossmint to mint NFTs without paying any fees.
2. Your smart contract complies with the **ERC721 standard** (for NFTs) or **ERC1155 standard** (for SFTs).
Currently, this feature is only available for EVM chains. Support for Solana and other chains is coming soon.
## Integration Guide
### 1. Register Your Smart Contract
First, you need to register your smart contract at the Crossmint Console:
1. Navigate to **Token collections** > **New Collection**
2. Enter the required collection information
3. Select **Import an existing contract**
4. Pick the chain, provide the contract address, and fill in the necessary information (i.e. ABI, mint function)
5. Keep track of the Crossmint collection ID generated for future mint calls
### 2. Contract Review
After registering your contract, the Crossmint team will review it to ensure compatibility with our infrastructure. This typically takes 1-2 business days.
During the review process, we'll verify that your contract has the necessary free minting function and complies with
the required standards.
### 3. Mint NFTs
Once your contract is approved, you can start minting NFTs using Crossmint's [Mint API](/api-reference/minting/nfts/mint-nft) and passing the following:
1. `recipient`, which is the recipient's wallet/email/X account user locators
2. `contractArguments`, which is an object where the keys are the argument names from the mint function, and the values are their values. Omit the recipient argument from this object
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/collections/{collectionId}/nfts \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: ' \
--data '{
"recipient": "polygon-amoy:0x123...",
"contractArguments": {
"tokenId": "42"
}
}'
```
## Best Practices
When using your own smart contract with Crossmint:
1. **Test Thoroughly**: Test your contract integration in the staging environment before moving to production.
2. **Function Visibility**: Ensure your minting function has the appropriate visibility and access controls.
3. **Gas Optimization**: Optimize your contract to minimize gas costs for minting operations.
4. **Error Handling**: Implement proper error handling in your contract to provide meaningful error messages.
## FAQs
Yes, you can use an existing deployed contract as long as it meets the prerequisites mentioned above.
Your contract must have a function that allows Crossmint to mint NFTs without paying any fees. If your contract
doesn't have this, you'll need to modify it or deploy a new version.
Yes, you can still use Crossmint's email delivery system with your own contract. Just specify an email recipient
in the mint request.
Token ID management depends on your contract implementation. You can either pass a specific token ID as a
parameter or let your contract handle token ID assignment internally.
# Configure Admin Functions
Source: https://docs.crossmint.com/minting/nfts/integrate/configure-admin-functions
Manage token controls including admin burn and admin transfer
Crossmint provides admin functions that allow you to control your NFTs throughout their lifecycle.
## Admin Transfer
The NFT Admin Transfer API allows collection owners to transfer NFTs between user wallets.
Admin transfers only work for Solana cNFTs. [Contact us](https://www.crossmint.com/contact/sales) if you want to
access to this API.
## Admin Burn
The [Burn NFT API](/api-reference/minting/nfts/burn-nft) allows collection owners to permanently remove NFTs from circulation.
Once an NFT is burned, this action cannot be reversed.
## Requirements
To use admin functions:
1. Your collection must be of a supported type (Solana cNFTs for transfers, Solana cNFTs or EVM for burns)
2. Your API key must have the appropriate scopes (`nfts.transfer` for transfers, `nfts.delete` for burns)
3. You must be the collection owner or have admin privileges
# Create Collections
Source: https://docs.crossmint.com/minting/nfts/integrate/create-collections
Deploy smart contracts and NFT collections
A collection is a container of NFTs, used by applications like marketplaces and wallets to group NFTs together.
Crossmint allows you to create managed collections via API or directly from the console. Crossmint has a library of pre-audited smart contracts which work for most major use cases. However, you can also [bring your own contract](/minting/nfts/integrate/bring-your-own-contract) if you already have one.
Crossmint supports non-fungible and semi-fungible tokens (editions), free and paid mints, and builds on open ERC and Metaplex standards. On EVM chains, ERC-721 and ERC-1155 contracts are supported, while on Solana, Metaplex standard programs and compressed NFT programs are supported.
See the list of supported blockchains [here](/introduction/supported-chains)
## 1. Create and Deploy an NFT collection
The first time you mint an NFT on a specific blockchain, Crossmint will assign it, and any subsequent mints, to a default collection for that chain. You can create additional collections from the console or in a single API call (requires the API key scope `collections.create`):
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/collections/ \
--header 'content-type: application/json' \
--header 'x-api-key: ' \
--data '
{
"chain": "polygon",
"metadata": {
"name": "A new collection",
"imageUrl": "https://www.crossmint.com/assets/crossmint/logo.png",
"description": "A new collection with its own dedicated smart contract"
}
}
'
```
The collection details you provide will be displayed to your customers on marketplaces and other interfaces.
If you intend to sell the NFTs in your collection, read the guide on [how to enable
payments](/minting/nfts/integrate/list-for-sale) first.
## 2. Check the status of your collection
It takes a few seconds (up to a minute, depending on the blockchain and how congested it is) to deploy a collection.
You can use the following API to check what the status of a collection is. For example:
```bash cURL theme={null}
# Set your variables
API_KEY=""
ENV="staging" # or "www" for production
ACTION_ID=""
curl --request GET \
--url "https://${ENV}.crossmint.com/api/2022-06-09/actions/${ACTION_ID}" \
--header "X-API-KEY: ${API_KEY}"
```
```javascript checkStatus.js theme={null}
const apiKey = "";
const env = "staging"; // or "www" for production
const actionId = "";
const url = `https://${env}.crossmint.com/api/2022-06-09/actions/${actionId}`;
const options = {
method: "GET",
headers: { "X-API-KEY": apiKey },
};
fetch(url, options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
## 3. List all collections under your project
```bash cURL theme={null}
curl --request GET \
--url https://staging.crossmint.com/api/2022-06-09/collections/ \
--header 'x-client-secret: ' \
--header 'x-project-id: '
```
Test any API in seconds directly from the docs.
Contact our sales team for support.
# Define Metadata
Source: https://docs.crossmint.com/minting/nfts/integrate/define-metadata
Understand NFT metadata standards & storage options
Metadata in the context of NFTs includes essential information such as the name, description, and media (e.g., images, videos) associated with the asset.
### Metadata Standards
Collection and NFT metadata (e.g. title, description, image) must be specified in JSON format and follow industry conventions to ensure it renders appropriately across wallets and marketplaces.
Crossmint supports rich metadata like audio, video, and HTML. To include rich metadata, simply populate `animation_url` as specified in the links below.
Crossmint's standard plan has a **10mb** file size limit. For larger uploads, you must upgrade to a [premium
plan](https://www.crossmint.com/contact/sales) or handle the upload yourself.
Metadata standards differ slightly across blockchains:
EVM chains follow the OpenSea standard. Refer to their official documentation to see what attributes can be included and how. Note that `name` and `image` are mandatory fields.
```json EVM Metadata Example theme={null}
{
"name": "Crossmint Test NFT",
"description": "Created with the Crossmint minting API",
"image": "https://bafkreiexjl6kw4khdxkrt6dojgacscnzvrys47t472l2t7d6r2ss65kifq.ipfs.nftstorage.link/",
"external_url": "https://docs.crossmint.com",
"attributes": [
{
"trait_type": "background",
"value": "black"
},
{
"trait_type": "flavor",
"value": "minty"
}
]
}
```
Solana follows the Metaplex Metadata Standard. For more information, refer to the official Metaplex documentation - see the JSON structure section.
```json Solana Metadata Example theme={null}
{
"name": "Crossmint Test NFT",
"symbol": "XMINT",
"description": "Created with the Crossmint minting API",
"seller_fee_basis_points": 5000,
"image": "https://bafkreiexjl6kw4khdxkrt6dojgacscnzvrys47t472l2t7d6r2ss65kifq.ipfs.nftstorage.link/",
"attributes": [
{
"trait_type": "background",
"value": "black"
},
{
"trait_type": "flavor",
"value": "minty"
}
],
"properties": {
"files": [
{
"uri": "https://bafkreiexjl6kw4khdxkrt6dojgacscnzvrys47t472l2t7d6r2ss65kifq.ipfs.nftstorage.link/",
"type": "image/png"
}
],
"category": "image",
"creators": [
{
"address": "xm2Mc3HEd2bGJXeqJEBrek4bj79gxzYkBaEobnpFraH",
"share": 100
}
]
}
}
```
Aptos follows the Aptos Legacy Token Standard. For more information, refer to the official Aptos Documentation - see the JSON structure section.
```json Aptos Metadata Example theme={null}
{
"name": "Crossmint Test NFT",
"description": "Aptos Attributes Test",
"image": "https://www.arweave.net/abcd5678?ext=png",
"animation_url": "https://www.arweave.net/efgh1234?ext=mp4",
"external_url": "https://petra.app/",
"attributes": [
{
"trait_type": "web",
"value": "yes"
},
{
"trait_type": "mobile",
"value": "yes"
},
{
"trait_type": "extension",
"value": "yes"
}
],
"properties": {
"files": [
{
"uri": "https://www.arweave.net/abcd5678?ext=png",
"type": "image/png"
},
{
"uri": "https://watch.videodelivery.net/9876jkl",
"type": "unknown",
"cdn": true
},
{
"uri": "https://www.arweave.net/efgh1234?ext=mp4",
"type": "video/mp4"
}
],
"category": "video"
}
}
```
## Crossmint Metadata Storage
Metadata must be stored somewhere to be accessible and verifiable on the blockchain. Crossmint automatically uploads and validates metadata for you on decentralized storage solutions, ensuring a seamless minting experience.
### Supported Storage Providers
Crossmint supports two primary storage providers:
* **IPFS (InterPlanetary File System)** (Default): A widely adopted decentralized storage standard ensuring compatibility across blockchain ecosystems by enabling peer-to-peer file storage and retrieval.
* **Walrus Network**: A new decentralized storage solution designed for efficient and scalable metadata hosting. Walrus is currently not supported for EVM NFT templates.
If you are interested in using **Arweave** for metadata storage, please contact us for further information.
### Configuring Storage
By default, Crossmint uses IPFS to store metadata and media. However, if you prefer to use Walrus, you can configure this in the [Crossmint Console](https://staging.crossmint.com/console) under **Settings > General**.
If you prefer to upload and manage media files independently, you can pass a direct URL to the [Mint API](/api-reference/minting/nfts/mint-nft)
and set the `reuploadLinkedFiles` flag to `false`. This prevents Crossmint from automatically reuploading media files
to decentralized storage.
***
## FAQs
You may store any text and image file. Additionally, you may include multi-media files such as GLTF, GLB, WEBM, MP4, M4V, OGV, and OGG, along with the audio-only extensions MP3, WAV, and OGA. Please stored them as `animation_url`.
In addition, `animation_url` also supports HTML pages, allowing you to build rich experiences and interactive NFTs using JavaScript canvas, WebGL, and more. Scripts and relative paths within the HTML page are also supported. However, access to browser extensions is not supported.
The maximum file size is 10mb. For larger files, you must upgrade to an enterprise plan or upload the metadata yourself and pass the URI to the API.
Yes. You must handle the upload yourself and pass the URL to the API.
# List for Sale
Source: https://docs.crossmint.com/minting/nfts/integrate/list-for-sale
Configure royalties and marketplace profiles for your NFT collections
When selling NFTs on secondary markets, you can configure royalties to receive a percentage of each sale and optimize your collection's marketplace profile to attract buyers.
## Sell Tokens
The [Mint Tokens guide](/minting/nfts/integrate/mint-tokens) showed you how to create different types of non-fungible assets.
In this guide you will learn how to enable Crossmint's Checkout so you
can sell them to your customers using cryptocurrency or credit card.
### How It Works
* You can set a price in crypto (ETH, MATIC, SOL or USDC).
* For imported and secondary collections, payouts arrive instantly. For managed collections, payouts arrive after a 24-hour withholding period.
* Your customers can pay with crypto or credit card.
* You pay credits for NFT gas, user pays only for the item.
### Configure a Collection for Sale
1. [Create an NFT collection](/minting/nfts/integrate/create-collections) and navigate
to it in the console.
2. Upload NFT "templates" for all the NFTs you wish to list for sale. You
can do so from the "NFTs" tab in the console, or by using
[the API](/api-reference/minting/template/create-template).
3. Navigate to the `Checkout` entry on the navbar in the collections page.
Follow the wizard to enable payments.
4. (Production only) [Verify your account and collection](/introduction/platform/account-verification). Not required in staging.
5. Share the url or QR code with your users.
**Enable at collection creation time**
Pass a `payments` object to the [Collection Creation
API](/api-reference/minting/collection/create-collection) with the
`price`,
`recipientAddress` and `currency`. You can optionally set up a
`supplyLimit` to cap the collection supply.
The following snippet creates a new sales-enabled collection:
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/collections/ \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: ' \
--data '{
"chain": "polygon",
"fungibility": "non-fungible",
"metadata": {
"description": "This is a sample NFT collection",
"imageUrl": "https://www.crossmint.com/assets/crossmint/logo.png",
"name": "Sample NFT Collection",
},
"payments": {
"price": "",
"recipientAddress": "",
"currency": "usdc"
},
"supplyLimit": 123
}'
```
**Enable on an existing collection**
If you had already created a collection, use the [Update Collection API](/api-reference/minting/collection/update-collection) to enable sales on it.
Enabling sales in production requires [verifying your account](/introduction/platform/account-verification).
***
### Set Up a Checkout
Now that your collection accepts payments, the next step is to integrate one of
Crossmint's [Checkout](/payments/introduction) variants into your app
or website, and start accepting sales.
There are two ways to integrate:
Add a button to your site which opens a checkout in a pop-up or new tab.
Insert a checkout inside your site for maximum control over the user experience.
## Set Collection Royalties
With royalties, you can receive revenue when your NFTs are traded on secondary markets. Crossmint allows you to configure royalties for a collection via API and (soon) directly from the console. You just have to specify the % of royalties and where you want those funds to be sent.
Royalties are defined at the collection level, not at the NFT level. Updating the royalties of a collection will update all NFTs, including the ones which have already been minted.
This API follows royalty standards compatible with virtually all marketplaces. However, note that some marketplaces have determined to not honor royalties, so transactions occuring there may not generate additional revenue.
Available self-serve for ERC-721 collections in EVM chains. Please [contact
us](https://www.crossmint.com/contact/sales) for ERC-1155 or Solana support.
| Endpoint | Description | |
| :- | :- | :- |
| [Add / Edit Royalties](/api-reference/minting/collection/set-royalties) | Add or edit royalties | |
| [Get Royalty Information](/minting/nfts/integrate/list-for-sale#get-royalty-information) | Get royalty information | |
| [Disable Royalties](/minting/nfts/integrate/list-for-sale#disable-royalties) | Disable royalties | |
### Add / Edit Royalties
To add or edit royalties for a collection, use the following API endpoint:
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/v1-alpha1/minting/collections/{collectionId}/royalties \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: ' \
--data '{
"recipients": [
{
"address": "0x123...",
"basisPoints": 5
}
]
}'
```
### Get Royalty Information
To retrieve the current royalty configuration for a collection:
```bash cURL theme={null}
curl --request GET \
--url https://staging.crossmint.com/api/v1-alpha1/minting/collections/{collectionId}/royalties \
--header 'X-API-KEY: '
```
### Disable Royalties
To disable royalties for a collection:
```bash cURL theme={null}
curl --request DELETE \
--url https://staging.crossmint.com/api/v1-alpha1/minting/collections/{collectionId}/royalties \
--header 'X-API-KEY: '
```
## Implementation Examples
```javascript theme={null}
// Example: Setting up royalties for a new collection
const apiKey = "YOUR_API_KEY";
const collectionId = "your-collection-id";
const env = "staging"; // or "www" for production
// Define royalty recipients
const royaltyRecipients = [
{
address: "0x123abc...", // Creator wallet
basisPoints: 5 // 5% royalty
},
{
address: "0x456def...", // Community fund wallet
basisPoints: 2.5 // 2.5% royalty
}
];
// Set up royalties
const url = `https://${env}.crossmint.com/api/v1-alpha1/minting/collections/${collectionId}/royalties`;
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": apiKey
},
body: JSON.stringify({
recipients: royaltyRecipients
})
};
fetch(url, options)
.then(response => response.json())
.then(data => console.log("Royalties configured:", data))
.catch(error => console.error("Error configuring royalties:", error));
```
## Marketplace Profile
Optimizing your collection's profile on marketplaces like OpenSea, Rarible, and others can significantly impact your NFT sales and visibility.
At this time, updating your collection profile on OpenSea, Rarible, and other marketplaces requires manual work and is limited to enterprise clients.
### Key Marketplace Profile Elements
When working with our enterprise team to configure your marketplace profile, consider these important elements:
1. **Collection Banner**: A high-quality banner image that represents your brand
2. **Collection Logo**: A distinctive logo that's recognizable at small sizes
3. **Collection Description**: A compelling description of your project and its value
4. **External Links**: Links to your website, social media, and community channels
5. **Category Tags**: Appropriate categories that help collectors discover your NFTs
## FAQs
Royalty enforcement varies by marketplace. Some marketplaces like OpenSea have made royalties optional, while
others still enforce them. Crossmint implements royalties according to blockchain standards, but cannot
guarantee enforcement across all marketplaces.
No, royalties are set at the collection level and apply to all NFTs within that collection. If you need
different royalty structures, consider creating separate collections.
Royalty changes take effect immediately for new listings. For existing listings on marketplaces, the timing
depends on how frequently each marketplace refreshes metadata.
While technically there's no upper limit, most marketplaces and collectors expect royalties between 2.5% and
10%. Setting royalties too high may discourage secondary market activity.
# Manage Delivery
Source: https://docs.crossmint.com/minting/nfts/integrate/manage-delivery
Configure recipient options and customize email delivery notifications
Crossmint provides flexible options for delivering NFTs to recipients and customizing the delivery experience. This guide covers how to specify different types of recipients and how to customize the email notifications they receive.
## Specify Recipients
You can deliver NFTs to three kinds of recipients when using minting APIs:
* [Wallet address](#send-nft-to-wallet-address): directly specify a wallet address, which could be a user owned wallet (e.g. MetaMask, Phantom, etc.) or a wallet you manage.
* [Email address](#send-nft-to-email-address): in this modality, Crossmint automatically generates a secure MPC backed custodial wallet for the email, and delivers the NFT inside.
* [User identifier](#send-nft-by-userid): in this modality, Crossmint automatically generates a wallet and associates it with an user ID from your application, and delivers the NFT inside. You must build the interface for users to access this wallet.
Check out the [create-wallet](/api-reference/wallets/create-wallet) endpoint in the API reference if you wish to
pre-create a wallet prior to invoking the mint API.
### Send NFT to Wallet Address
To mint an NFT directly to an existing blockchain address, the following `recipient` format is used:
```bash theme={null}
:
```
For a full list of chain names, refer to the [Supported Chains](/introduction/supported-chains) page.
#### Examples
* EVM: `polygon:0x123...`
* Solana: `solana:3Q5...`
* Aptos: `aptos:0x0f07...`
### Send NFT to Email Address
To mint an NFT and send it to an email address, the following `recipient` format is used:
```bash theme={null}
email::
```
#### Examples
* EVM: `email:demo@test.com:polygon`
* Solana: `email:demo@test.com:solana`
Minting to an email address is not currently supported on Aptos.
The NFT can then be accessed by logging into Crossmint with the specified email address:
* for `staging/testnet` NFTs: [https://staging.crossmint.com/user/collection](https://staging.crossmint.com/user/collection)
* for `mainnet` NFTs: [https://www.crossmint.com/user/collection](https://www.crossmint.com/user/collection)
### Send NFT to a Twitter/X account
To mint an NFT to a Twitter/X account, the following `recipient` format is used:
```bash theme={null}
twitter::
```
#### Examples
* EVM: `twitter:@username:polygon`
* Solana: `twitter:@username:solana`
### Send NFT by `userId`
This method allows you to deliver NFTs by directly specifying the user identifier of the recipient in your system. Crossmint will fetch a custodial wallet linked to that user identifier inside your project or, if none exists, create one on the fly. Then the NFT will be delivered there. This way, you don't need to keep a mapping between your user identifiers and their wallets, just pass your user id and crossmint takes care of the rest.
Wallets created with the `userId` option cannot be accessed by logging into Crossmint.com.
To mint an NFT to this type of recipient, follow this format:
```bash theme={null}
userId::
```
#### Examples
* EVM: `userId:user1234:polygon`
* Solana: `userId:user1234:solana`
## Customize Delivery Emails
Email delivery notifications can be customized to align with your branding and communication needs. This section will walk you through how to configure your email template.
### Edit the email template
To adjust the visual presentation and branding of your email notifications:
1. In the Crossmint Console, click on Settings, and navigate to the **Branding** tab.
2. Select **Deliver NFT via email** from the dropdown options.
3. Here, you can customize:
* The **logo** displayed in the email with your logo. This will default to Crossmint's logo, unless specified otherwise.
* The main **button's color** . This will default to #04AA6D (Crossmint's green).
* The **display name** textbox to include your brand's name. This will be empty, by default.
As you make adjustments, you can preview the changes. Once satisfied, save your changes and now they will apply to all future minting email notifications, related to this project. Emails now reflect your project's brand identity while providing essential delivery information to recipients.
### Enable email delivery
You should make sure email delivery notifications are enabled for the customized email to reach your users. For new projects created after Sep 16, 2024, the "sendNotification" parameter is set to `true` by default, meaning mint recipients will automatically receive email notifications.
For legacy projects, created before Sep 16 2024, "sendNotification" is set to `false` until March 14, 2025. If you choose so, you can enable email notifications for legacy projects by explicitly turning the feature on via API or via the Console.
#### Console
To send email notifications to NFT recipients:
1. Navigate to an **unminted NFT** part of a Collection in the Crossmint Console.
2. Click on the **more options** button and select **Mint & send NFT**.
3. Ensure the **Notify recipient via email** option is selected.
4. Enter the **recipient's email address** and press Mint.
The recipient will receive an email delivery notification for their newly minted NFT.
#### API
To do the same programmatically, you can configure two new parameters:
1. **sendNotification** (boolean): Determines whether the recipient will receive an email notification.
* Default value: `true` for new projects created after Sep 16, 2024.
* For legacy projects (created before Sep 16, 2024), this feature will default to `false` until March 14, 2025, unless explicitly set to `true`.
2. **locale** (string): Specifies the language for the email content. Default is `en-US` for English. We support all locales (i.e. "es-ES" for Spanish).
## FAQs
Yes, recipients can manage their email preferences through their Crossmint account settings.
If an email address is invalid, the NFT will still be minted but the notification will fail to deliver. The NFT
will be associated with the email address in Crossmint's system, but the recipient won't receive a notification.
Currently, each API call mints to a single recipient. For bulk operations, you'll need to make multiple API
calls. See the [Mint In Bulk](/minting/nfts/integrate/mint-in-bulk) guide for more information.
Crossmint doesn't currently provide delivery tracking for email notifications. For critical notifications,
consider implementing your own email delivery system alongside Crossmint's.
# Mint In Bulk
Source: https://docs.crossmint.com/minting/nfts/integrate/mint-in-bulk
Scale your NFT operations with bulk minting capabilities
Crossmint's infrastructure is designed to handle high-volume minting operations efficiently, allowing you to scale your NFT projects to serve thousands or even millions of users.
## Enterprise Solutions
For enterprise customers with very high volume requirements, Crossmint offers additional capabilities:
Enterprise bulk minting features are available for high-volume projects. Please [contact
us](https://www.crossmint.com/contact/sales) to discuss your specific requirements.
## Bulk Minting Capabilities
Crossmint's platform offers several advantages for bulk minting operations:
* **High Throughput**: Mint thousands of NFTs per hour with optimized transaction batching
* **Cost Efficiency**: Reduce gas costs through intelligent transaction scheduling
* **Queue Management**: Automatic handling of transaction queues and retries
* **Observability**: Track the status of all minting operations in real-time
* **Gas Optimization**: Automatic gas price adjustments based on network conditions
## Bulk Minting via Crossmint Console
The Crossmint Console provides an easy-to-use interface for uploading and minting NFTs in bulk. Here's how to use it:
### Step 1: Access the Batch Upload Feature
In the Crossmint Console, navigate to your collection and select the "Batch upload" option.
### Step 2: Prepare Your Metadata
Before uploading your assets, you need to prepare them properly:
1. You can upload up to 1,000 NFT collectibles at once
2. Download the example CSV file provided by Crossmint to use as a template
3. Prepare your metadata according to the template format
### Step 3: Upload Your Metadata CSV
1. Add a CSV file named `metadata.csv` with your NFT metadata
2. Click the "Upload CSV" button to upload your prepared file
### Step 4: Upload Media Files
1. Prepare your NFT media files (images, animations, videos)
2. Crossmint supports various formats (PNG, JPEG, GIF etc.)
3. Each file must be under 10MB in size
4. All media files should be in the same folder, not in subfolders
5. Click "Upload media files" to upload your prepared media
### Step 5: Complete the Upload
After uploading both your metadata CSV and media files, click the "Upload" button to start the batch minting process.
## Monitoring Bulk Operations
When minting in bulk, it's essential to track the status of your operations:
1. **Webhooks**: Set up [webhooks](/minting/nfts/integrate/webhooks-and-status-apis) to receive real-time notifications about mint completions and failures
2. **Status API**: Use the [action status API](/minting/nfts/integrate/webhooks-and-status-apis) to check the status of individual minting operations
3. **Console Dashboard**: Monitor your minting operations through the Crossmint Console
# Mint Tokens
Source: https://docs.crossmint.com/minting/nfts/integrate/mint-tokens
Create and distribute tokens at scale
To mint and airdrop unique digital assets, you can follow the guide on the [Quickstart](/minting/quickstarts/nfts). For more detail, please check the [API reference](/api-reference/minting/nfts/mint-nft). You can watch a quick video tutorial for this [here](https://youtu.be/yYJRW35zqUQ?si=b6PQTbZ7zqYVGwXw).
SFTs (semifungible tokens) follow the ERC-1155 standard. Each token is a replica of a predefined template. Each collection (smart contract) can contain multiple templates, which can contain many tokens.
To get started, it's recommended to read the [Introduction](/minting/introduction) for general information on how the minting product works.
This API only supports EVM chains self-serve. Contact us if you need support for another chain.
## 1. Create an SFT collection to hold your templates
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/collections \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '
{
"chain": "polygon-amoy",
"fungibility": "semi-fungible",
"metadata": {
"name": "My New Collection",
"imageUrl": "https://www.crossmint.com/assets/crossmint/logo.png",
"description": "A new collection with its own dedicated smart contract"
}
}'
```
```json JSON theme={null}
{
"id": "5263650e-6d43-4ed3-9e31-0cf593d076a4",
"metadata": {
"name": "Test Collection",
"description": "Test",
"imageUrl": "https://cdn.io/metadata.json",
"symbol": "XMINT"
},
"fungibility": "semi-fungible",
"onChain": {
"chain": "polygon-amoy",
"type": "erc-1155"
},
"actionId": "5263650e-6d43-4ed3-9e31-0cf593d076a4"
}
```
## 2. Create a template within that collection
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/collections/{collectionId}/templates \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '
{
"onChain": {
"tokenId": "2"
},
"supply": {
"limit": 10
},
"metadata": {
"name": "My template",
"image": "https://www.crossmint.com/assets/crossmint/logo.png",
"description": "A new token template for my ERC1155 collection"
}
}'
```
```json JSON theme={null}
{
"templateId": "58b0c1aa-e457-48dd-bb55-5a27e6a92f74",
"metadata": {
"name": "My template",
"image": "ipfs://bafkreigbqsmxzkbjgbwtj6exfdt5z3t3swgoysf7hr6vjzddqnmykj6x2u",
"description": "A new token template for my ERC1155 collection"
},
"onChain": {
"tokenId": "1"
},
"supply": {
"limit": "10",
"minted": "0"
}
}
```
## 3. Mint an SFT from a template
Mint SFTs from the template and send them to wallets or email addresses.
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/collections/{collectionId}/sfts \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '
{
"templateId": "58b0c1aa-e457-48dd-bb55-5a27e6a92f74",
"recipient": "email:user@example.com:polygon-amoy",
"amount": 1
}'
```
```json JSON theme={null}
{
"actionId": "a91c15e3-60f2-4a45-bf1a-cee508981667",
"action": "nfts.create",
"status": "pending",
"data":
{
"chain": "polygon-amoy",
"collection":
{
"id": "84e3d617-9c1b-4e7a-9686-522a9ea7c520",
"contractAddress": "0x9b8ab8949bd7E73E61945b88F7fe12151f98ad3C"
},
"recipient":
{
"walletAddress": "0xcFDc00Cf926A5053f9Cdf004e6DF17e6dEB2E146",
"email": "testy@crossmint.com"
},
"token":
{
"id": "a91c15e3-60f2-4a45-bf1a-cee508981667"
}
},
"startedAt": "2024-01-02T22:05:01.000Z",
"resource": "https://staging.crossmint.com/api/2022-06-09/actions/a91c15e3-60f2-4a45-bf1a-cee508981667"
}
```
**All set!**
To confirm delivery, use the [transaction status API](/minting/nfts/integrate/webhooks-and-status-apis) or set up a [webhook](/minting/nfts/integrate/webhooks-and-status-apis).
## What are Compressed NFTs?
Compressed NFTs are a new standard on the Solana blockchain, for minting NFTs with the lowest cost amongst L1 and L2 blockchains, and the highest throughput (thousands of NFTs per second). Read more details about how it works [here](https://www.metaplex.com/posts/expanding-digital-assets-with-compression-for-nfts).
Using this standard to mint NFTs is extremely complicated if you manage everything on your own. On top of the intrinsic complexity usually required to mint a regular NFT, compressed NFTs require deploying and managing Merkle trees, batching, and dealing with nascent infrastructure. Crossmint also supports updating the NFT metadata after minting. Please refer to this [doc page](/minting/nfts/integrate/update-nfts).
Here's where Crossmint can help: we've done all the hard work so that minting compressed NFTs is no harder than minting regular ones: all it takes is a single API call, and you can even use one of Crossmint's no-code tools to do so.
## Current Protocol Limitations
* There's a 10-30 second delay between when an NFT is minted and it shows in wallets.
## How to use the Minting API with Compressed NFTs
The Minting API for Compressed NFTs is exactly the same as for regular NFTs, but it only works on the Solana blockchain.
POST `https://staging.crossmint.com/api/2022-06-09/collections//nfts`
You can mint both compressed and non-compressed NFTs on the same collection.
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/collections/default-solana/nfts \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: ' \
--data '{
"recipient": "email:john.doe@example.com:solana",
"metadata": {
"name": "Crossmint Example NFT",
"image": "https://www.crossmint.com/assets/crossmint/logo.png",
"description": "My NFT created via the mint API!"
},
"compressed": true,
"reuploadLinkedFiles": false
}'
```
The above will return an NFT ID. Use it in the next call to retrieve the mint status.
```bash cURL theme={null}
curl --request GET \
--url https://staging.crossmint.com/api/2022-06-09/collections/{collectionId}/nfts/{nftId} \
--header 'X-API-KEY: X_API_KEY'
```
Be sure to replace:
* `X_API_KEY`, `{collectionId}` and `{nftId}`
* Recipient: change `VALID_WALLET_ADDRESS` with the wallet you want to send the NFT to
* Metadata: add the name, image, and description.
Other explorers, like Solscan, still have not added support.
There are two ways to set the transferrability of NFTs:
1. Defining it at the smart contract level
2. If you are using custodial wallets, enabling or disabling transfers from the frontend
If you want to configure it at the smart contract level, see the following endpoints:
| Endpoint | Chain | Description |
| - | - | - |
| [Set Transferability](/api-reference/minting/collection/set-transferability) | EVM and Aptos | Set transferability to on or off |
| [Get Transferability](/api-reference/minting/collection/get-transferability) | EVM and Aptos | Get transferability configuration |
For Solana or other non-EVM chain support, [contact us](https://www.crossmint.com/contact/sales)
***
The API also has an idempotent version which prevents you from calling the same API action multiple times and
mitigates the risk of unwanted NFT duplicates.
## Advanced Guides
# Pricing
Source: https://docs.crossmint.com/minting/nfts/integrate/pricing
Crossmint Mint API Pricing
## Chain operations pricing
*All values shown in USD.*
| Chain | Create Collection | Update Collection | Mint NFTs | Update Royalty | Update Metadata |
| :- | -: | -: | -: | -: | -: |
| Aptos | \$0.10 | Not supported | \$0.01 | Not supported | Not supported |
| Arbitrum\* | \$0.03 + gas | \$0.03 + gas | \$0.03 + gas | \$0.03 + gas | \$0.03 + gas |
| Arbitrum Nova\* | \$0.01 + gas | \$0.01 + gas | \$0.01 + gas | \$0.01 + gas | \$0.01 + gas |
| Avalanche\* | \$0.10 + gas | \$0.10 + gas | \$0.10 + gas | \$0.10 + gas | \$0.10 + gas |
| Base\* | \$0.08 + gas | \$0.08 + gas | \$0.08 + gas | \$0.08 + gas | \$0.08 + gas |
| BSC\* | \$0.01 + gas | \$0.01 + gas | \$0.01 + gas | \$0.01 + gas | \$0.01 + gas |
| Chiliz | \$1 | \$0.20 | \$0.50 | \$0.20 | \$0.20 |
| Optimism\* | \$0.06 + gas | \$0.06 + gas | \$0.06 + gas | \$0.06 + gas | \$0.06 + gas |
| Polygon | \$0.50 | \$0.50 | \$0.10 | \$0.50 | \$0.05 |
| Sei | \$0.05 | \$0.01 | \$0.01 | \$0.01 | \$0.01 |
| Shape | \$2 | \$2 | \$2 | \$2 | \$2 |
| Solana | \$8 | Not supported | \$0.02 | Not supported | \$0.02 |
| Skale Nebula | \$0.01 | \$0.01 | \$0.01 | \$0.01 | \$0.01 |
| Zora\* | \$0.08 + gas | \$0.08 + gas | \$0.08 + gas | \$0.08 + gas | \$0.08 + gas |
\*For some chains (Arbitrum, Arbitrum Nova, Avalanche, Base, BSC, Optimism, and Zora), gas fees are added to the base price. Gas fees are determined at the time the operation is submitted and depend on blockchain network congestion.
For more details, or to discuss custom pricing for large operations volumes, [contact our sales team](https://www.crossmint.com/contact/sales).
# Update NFTs
Source: https://docs.crossmint.com/minting/nfts/integrate/update-nfts
Make your NFTs change over time
Dynamic NFTs are tokens whose content changes over time. There are two ways to achieve this with Crossmint:
1. Using Crossmint's [Edit NFT API](/api-reference/minting/nfts/edit-nft)
2. Storing the metadata offchain and updating it on your database. To use this option, set the property `reuploadLinkedFiles` to `false` when minting an NFT.
## Using the Edit NFT API
The [Edit NFT API](/api-reference/minting/nfts/edit-nft) allows you to update the metadata of an existing NFT, including its name, description, image, and attributes. This is useful for creating NFTs that evolve over time or need to be updated based on external events.
### Prerequisites
* Ensure your API key is a server-side key with the `nfts.update` scope
* You need the collection ID and NFT ID from the minting process
### Implementation
```bash cURL theme={null}
curl --request PATCH \
--url https://staging.crossmint.com/api/2022-06-09/collections/{collectionId}/nfts/{nftId} \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: ' \
--data '{
"metadata": {
"description": "My updated NFT description!",
"image": "https://www.crossmint.com/assets/crossmint/updated-image.png",
"name": "Evolved NFT",
"attributes": [
{
"trait_type": "level",
"value": "2"
}
]
},
"reuploadLinkedFiles": true
}'
```
## Using Off-chain Metadata
You can also store the metadata off-chain and update it directly:
1. When minting the NFT, set `reuploadLinkedFiles` to `false`
2. Use your own server to host the metadata
3. Update the metadata on your server as needed
```javascript theme={null}
// Example: Minting with off-chain metadata
const apiKey = "YOUR_API_KEY";
const chain = "polygon-amoy"; // or "ethereum-sepolia", "base-sepolia", etc.
const env = "staging"; // or "www" for production
const recipientEmail = "TEST_EMAIL_ADDRESS";
const recipientAddress = `email:${recipientEmail}:${chain}`;
// The URL to your metadata server
const metadataImgUrl = "https://your-server.com/metadata/token123.png";
const url = `https://${env}.crossmint.com/api/2022-06-09/collections/default/nfts`;
const options = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
recipient: recipientAddress,
metadata: {
name: "Dynamic NFT",
description: "This NFT will update over time",
image: metadataImgUrl,
},
reuploadLinkedFiles: false,
}),
};
fetch(url, options)
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error("Error:", error));
```
## Use Cases for Dynamic NFTs
Dynamic NFTs enable a wide range of applications:
* **Game Assets**: NFTs that evolve as players progress
* **Membership Passes**: NFTs that update based on membership status
* **Event Tickets**: NFTs that change before, during, and after events
* **Digital Collectibles**: NFTs that evolve based on real-world events
* **Loyalty Programs**: NFTs that update based on customer activity
## Best Practices
When implementing dynamic NFTs:
1. **Plan for Updates**: Design your NFT with future updates in mind
2. **Versioning**: Consider including version information in your metadata
3. **Event Triggers**: Define clear conditions for when updates should occur
4. **User Communication**: Inform users about how and when their NFTs might change
5. **Testing**: Thoroughly test update mechanisms before implementation
## FAQs
With Crossmint's Edit API, you can update NFTs as needed, but consider blockchain transaction costs and network
congestion. For very frequent updates, consider using off-chain metadata.
Updates don't change the token ID or ownership history, but the perceived value may change based on the nature
of the updates. It's important to communicate your update policy to collectors.
Crossmint doesn't provide native scheduling, but you can implement scheduled updates using your own backend
services that call the Edit API at specified times.
Most marketplaces will display the current state of your NFT's metadata, but the frequency of metadata refreshes
may vary by marketplace.
# Webhooks and Status APIs
Source: https://docs.crossmint.com/minting/nfts/integrate/webhooks-and-status-apis
Listen for updates in mints, edits, collection creations and other async events
Creating NFT collections and minting or [editing NFTs](/minting/nfts/integrate/update-nfts) are operations that must be sent to a blockchain. Transaction confirmation on the blockchain can take a few seconds, but during network congestion, it may take several minutes. Webhooks and the action status API allow you to stay up to date on the status of these asynchronous operations.
Some cases where you may want to listen to when transactions are confirmed include:
* Notifying your customers via email that their NFT is ready to access
* Updating your database with the NFT id for the user
* Showing in your website that that the mint has been successful
### Ways to check the status of an action
| Model | Best for | Mechanism |
| - | - | - |
| Pull (query for updates) | Quick testing | Action Status API |
| Push (get notified) | Scalable apps in production | Webhooks |
## Action Status APIs
Call the following API to check the status of an action:
```bash cURL theme={null}
# Set your variables
env=staging # or "www" for production
YOUR_API_KEY=
actionId=
# Execute the curl command
curl --request GET \
--url "https://${env}.crossmint.com/api/2022-06-09/actions/${actionId}" \
--header 'accept: application/json' \
--header "x-api-key: ${YOUR_API_KEY}"
```
* `actionID` is returned from any async API calls you perform.
* `YOUR_API_KEY` can be found in the `Developers -> API Keys` tab of the [Production](https://www.crossmint.com/console/projects/apiKeys) or [Staging](https://staging.crossmint.com/console/projects/apiKeys) consoles.
## Webhooks
In this guide, we will use nodejs to create an API endpoint to listen for and parse webhook events:
### 1. Create an endpoint route
Using a standard nodejs API server, create an endpoint.
You can test locally by installing [ngrok](https://ngrok.com/docs/getting-started/) and creating a routed endpoint to a specified port.
### 2. Configure the endpoint to read and parse webhook events
On this endpoint, modify the code to handle POST requests only. When a POST request comes through, parse the webhook event of the request body. Ensure your webhook listener responds with a `200` status code. Otherwise the webhook may be sent until you acknowledge it.
The snippet below is an example handler that parses webhook events:
```javascript theme={null}
// endpoint.js
// listen to webhook ingestion
export default function handler(req, res) {
if (req.method === "POST") {
console.log(`[webhook] Successfully minted ${req.body.id}`);
}
res.status(200).json({});
}
```
Don't be strict with payload validations as Crossmint may add new fields to
the webhooks as products evolve.
Here are some examples of the webhook results (with dummy data):
```json EVM theme={null}
{
"actionId": "897eadae-ee2d-43f9-a97b-1a9d9c682d6f",
"startedAt": "2023-10-04T15:48:20.000Z",
"completedAt": "2023-10-04T15:48:42.000Z",
"type": "collections.create.succeeded",
"data": {
"chain": "polygon",
"txId": "0x919ec33c1ceffd292d1d0cdd9675ffcc0af31a1f34622edd4865a9ca9fa82aa1",
"collection": {
"id": "897eadae-ee2d-43f9-a97b-1a9d9c682d6f",
"contractAddress": "0x59195995f248450267AD40CAc1d79fAAba290467"
}
},
"resource": "https://crossmint.com/api/2022-06-09/collections/897eadae-ee2d-43f9-a97b-1a9d9c682d6f"
}
```
```json Solana theme={null}
{
"actionId": "01ac4b77-9bc9-42d3-a110-b0572ec299fd",
"startedAt": "2023-10-04T15:50:12.000Z",
"completedAt": "2023-10-04T15:50:23.000Z",
"type": "collections.create.succeeded",
"data": {
"chain": "solana",
"txId": "2M4tFbGWFPWAjHmZdssoTiTRjxx43nRd3vt9RpQ3wLWsW69duUhvEQrDvsyfafnQNoh6LmDQg4KPLvffgCskkwoV",
"collection": {
"id": "01ac4b77-9bc9-42d3-a110-b0572ec299fd",
"mintAddress": "JCpAQcjqWCwTpRPsMumsXtg3A4SGMXbxVbowu8qrntqt"
}
},
"resource": "https://a5b6-181-167-232-117.ngrok-free.app/api/2022-06-09/collections/01ac4b77-9bc9-42d3-a110-b0572ec299fd"
}
```
```json EVM theme={null}
{
"actionId": "e0926e73-8212-4074-b934-4d6ac68cfcff",
"startedAt": "2023-10-04T15:57:14.000Z",
"completedAt": "2023-10-04T15:57:36.000Z",
"type": "collections.update.succeeded",
"data": {
"chain": "polygon",
"txId": "0x919ec33c1ceffd292d1d0cdd9675ffcc0af31a1f34622edd4865a9ca9fa82aa1",
"collection": {
"id": "897eadae-ee2d-43f9-a97b-1a9d9c682d6f",
"contractAddress": "0x59195995f248450267AD40CAc1d79fAAba290467"
},
"changes": ["supplyLimit"]
},
"resource": "https://crossmint.com/api/2022-06-09/collections/e0926e73-8212-4074-b934-4d6ac68cfcff",
"timestamp": 1696424259573
}
```
```json EVM theme={null}
{
"type": "nfts.create.succeeded",
"actionId": "771d7e38-2890-47d7-b733-a1462736b528",
"startedAt": "2023-10-04T19:59:47.000Z",
"completedAt": "2023-10-04T20:00:00.000Z",
"data": {
"chain": "polygon",
"txId": "0x919ec33c1ceffd292d1d0cdd9675ffcc0af31a1f34622edd4865a9ca9fa82aa1",
"collection": {
"id": "default-polygon",
"contractAddress": "0x2DdDDEe8dad8b2ec123F5ceEc3E8dA4E57C0ed29"
},
"recipient": {
"walletAddress": "0x10324e5B8879CA6662ff83617F74b0AaD251b819",
"email": "recipient@crossmint.com"
},
"token": {
"id": "771d7e38-2890-47d7-b733-a1462736b528",
"owner": {
"walletAddress": "0x10324e5B8879CA6662ff83617F74b0AaD251b819"
},
"tokenId": "15"
}
},
"resource": "https://crossmint.com/api/2022-06-09/collections/default-polygon/nfts/771d7e38-2890-47d7-b733-a1462736b528"
}
```
```json Solana theme={null}
{
"type": "nfts.create.succeeded",
"actionId": "2c5f9ec6-ae68-4e17-a671-db4af2208a2d",
"startedAt": "2023-10-04T20:01:21.000Z",
"completedAt": "2023-10-04T20:01:27.000Z",
"data": {
"chain": "solana",
"txId": "2M4tFbGWFPWAjHmZdssoTiTRjxx43nRd3vt9RpQ3wLWsW69duUhvEQrDvsyfafnQNoh6LmDQg4KPLvffgCskkwoV",
"collection": {
"id": "default-solana",
"mintAddress": "qCTMcsRBYYt1YKDAkJnstjgXJU625YX5LBfwsUi1Mn4"
},
"recipient": {
"walletAddress": "EjLpq6RAxzPuwjcwJ2C6ZjwrFZuqhu2CiVTDzkSSCE5B",
"email": "recipient@crossmint.com"
},
"token": {
"id": "2c5f9ec6-ae68-4e17-a671-db4af2208a2d",
"owner": {
"walletAddress": "EjLpq6RAxzPuwjcwJ2C6ZjwrFZuqhu2CiVTDzkSSCE5B"
},
"mintHash": "Gec66rG8Rj6Q2nqaFpGRvggiAfriMb93o28qGSeeY1gJ"
}
},
"resource": "https://staging.crossmint.com/api/2022-06-09/collections/default-solana/nfts/2c5f9ec6-ae68-4e17-a671-db4af2208a2d",
"timestamp": 1696449687501
}
```
```json EVM theme={null}
{
"actionId":"cf9985d3-3341-4fd6-bd0e-548ef97a3486",
"startedAt":"2024-02-05T21:48:17.000Z",
"type":"nfts.create.failed",
"data":{
"chain":"polygon",
"collection":{
"id":"297199ba-c763-4464-b592-26c2ca2dbe5d",
"contractAddress":"0x2ADBeb5e1976615883D3c5F07234E38b50e09edB"
},
"contractArguments":{
"key":28
},
"recipient":{
"walletAddress":"0x78359E7dF948834caFEcBE0494B010C6f7f7fA74"
},
"token":{
"id":"cf9985d3-3341-4fd6-bd0e-548ef97a3486"
},
"error":{
"reason":"execution_reverted",
"message":"Minting smart contract reverted. Check 'revertReason' for details",
"revertReason":"execution reverted: Wrong value for key",
"callInfo":{
"functionName":"mintTo",
"arguments":{
"key":28,
"recipient":"0x78359E7dF948834caFEcBE0494B010C6f7f7fA74"
},
"calldata":"0x449a52f800000000000000000000000078359e7df948834cafecbe0494b010c6f7f7fa74000000000000000000000000000000000000000000000000000000000000001c"
}
}
},
"resource":"https://1a7a-181-110-64-58.ngrok-free.app/api/2022-06-09/collections/297199ba-c763-4464-b592-26c2ca2dbe5d/nfts/cf9985d3-3341-4fd6-bd0e-548ef97a3486",
"timestamp":1707158908733
}
```
```json EVM theme={null}
{
"type": "nfts.update.succeeded",
"actionId": "f7fedd17-2feb-4bc9-88fa-8890ea8f99e8",
"startedAt": "2023-10-04T20:09:27.000Z",
"completedAt": "2023-10-04T20:09:35.000Z",
"data": {
"chain": "polygon",
"txId": "0x919ec33c1ceffd292d1d0cdd9675ffcc0af31a1f34622edd4865a9ca9fa82aa1",
"collection": {
"id": "default-polygon",
"contractAddress": "0x2DdDDEe8dad8b2ec123F5ceEc3E8dA4E57C0ed29"
},
"token": {
"id": "210a95ab-d59c-41ef-9f60-8e41550a753e",
"owner": {
"walletAddress": "0x10324e5B8879CA6662ff83617F74b0AaD251b819"
},
"tokenId": "16"
},
"changes": ["metadata"]
},
"resource": "https://crossmint.com/api/2022-06-09/collections/default-polygon/nfts/f7fedd17-2feb-4bc9-88fa-8890ea8f99e8",
"timestamp": 1696450175660
}
```
```json Solana theme={null}
{
"type": "nfts.update.succeeded",
"actionId": "57d3cedd-e8e8-4509-88bc-97d6baea478f",
"startedAt": "2023-10-04T20:07:55.000Z",
"completedAt": "2023-10-04T20:08:01.000Z",
"data": {
"chain": "solana",
"txId": "2M4tFbGWFPWAjHmZdssoTiTRjxx43nRd3vt9RpQ3wLWsW69duUhvEQrDvsyfafnQNoh6LmDQg4KPLvffgCskkwoV",
"collection": {
"id": "default-solana",
"mintAddress": "qCTMcsRBYYt1YKDAkJnstjgXJU625YX5LBfwsUi1Mn4"
},
"token": {
"id": "1f3f0959-417d-43a7-95f8-39e6f22d5573",
"owner": {
"walletAddress": "EjLpq6RAxzPuwjcwJ2C6ZjwrFZuqhu2CiVTDzkSSCE5B"
},
"mintHash": "BnyGQcmU4mXMi5NMUuKTjFdYkg79RZoBX3qCWdXZVcyb"
},
"changes": ["metadata"]
},
"resource": "https://crossmint.com/api/2022-06-09/collections/default-solana/nfts/57d3cedd-e8e8-4509-88bc-97d6baea478f",
"timestamp": 1696450081841
}
```
### 3. Pre & post processing
Add your pre and post processing logic when setting up your webhook listener. For example, you can call back to your database when a certain id has succeeded or even use Sendgrid or EmailJS to send an email to a recipient when a mint completes.
### 4. Setting Up Webhooks on the Crossmint Console
Add an endpoint for the event `mint.succeeded` by following [this guide](/introduction/platform/webhooks/add-endpoint).
Once completed, you'll be redirected to the endpoint details page. Here, you can find the signing secret for [verifying webhooks](/introduction/platform/webhooks/verify-webhooks) and view a table of all triggered webhook events.
The final step is to test the endpoint. To do so, mint one or two NFTs with the API and observe the responses to verify setup success.
## Webhook video walkthrough
# Quickstart ⚡
Source: https://docs.crossmint.com/minting/quickstarts/credentials
Mint credentials that users control, share, and verify anywhere in under 5 minutes
In this quickstart you will learn how to create, issue, and verify credentials.
## What are credentials?
A digital credential is an electronic record issued by a trusted source that certifies specific information about a person or entity. Crossmint makes it easy to issue, manage, and verify credentials onchain, following the [W3C VC standard](https://www.w3.org/TR/vc-data-model-2.0/).
Verifiable Credentials is an Enterprise feature. Contact Sales
for access.
## Integration steps
### 1. Create a Developer Account
### 2. Get an API Key
Within the "Server-side keys" section, click the "Create new key" button in the top right.
Then, check the scopes `credentials.create`, `credentials.read`, and `credentials:templates.create` under the "Verifiable Credentials"
category and create your key. Save this key for the next step.
### 3. Create a Credential Template
Every Verifiable Credential must belong to a template. Verifiable Credentials within a template share the same schema (referred to as "type" in the template definition), default-metadata, encryption, storage, and chain configurations. A credential template is equivalent to an NFT collection.
For the purpose of this quickstart, we will create a template with the following parameters:
* Type: `crossmint:bedea4c5-4cea-425b-99c7-797ecbc8bc13:CourseCompletionCertificate`. Types allow you to specify the attributes you are interested in certifying. They also act as a protective measure, preventing the addition of unauthorized fields and, as a result, the tampering of the Verifiable Credential. You can [define your own](/minting/verifiable-credentials/integrate/create-credential-types).
* Encryption: `none`. This means the Verifiable Credential data will be stored in plain text. To encrypt the data, read about the supported [encryption modalities](/minting/verifiable-credentials/integrate/encrypt-credentials).
* Storage: `crossmint`. This means the Verifiable Credential data will be stored by Crossmint. To store the data at the location of your choice, or in decentralized storage, read about the supported [storage modalities](/minting/verifiable-credentials/integrate/store-credentials).
The credential’s revocation state is stored in the chain provided (`polygon-amoy`), along with public (non-confidential) metadata related to your credential’s template.
```javascript createTemplate.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const url = `https://${env}.crossmint.com/api/v1-alpha1/credentials/templates`;
const options = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
credentials: {
type: "crossmint:bedea4c5-4cea-425b-99c7-797ecbc8bc13:CourseCompletionCertificate",
encryption: "none",
storage: "crossmint",
subject: {
properties: {
course: {
type: "string",
title: "Course",
description: "Course name",
},
grade: {
type: "string",
title: "Grade",
description: "Grade received",
},
},
required: ["course", "grade"],
},
},
metadata: {
name: "Certificate of Completion",
description: "A certificate issued upon completion of a course",
imageUrl: "https://www.crossmint.com/assets/crossmint/logo.png",
},
chain: "polygon-amoy",
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
```json JSON theme={null}
{
"id": "a9e3016b-66f0-40ab-8d72-6546fddc7b72",
"metadata": {
"description": "A certificate issued upon completion of a course",
"name": "Certificate of Completion",
"imageUrl": "ipfs://QmaToZn4VEjF7q4CAudPaNka6AD484xuuEZSXmTLJPDLVE"
},
"fungibility": "non-fungible",
"onChain": { "chain": "polygon-amoy", "type": "erc-721" },
"subscription": { "enabled": false },
"actionId": "a9e3016b-66f0-40ab-8d72-6546fddc7b72"
}
```
Save the `id` from the response as your `TEMPLATE_ID` for the next step.
### 4. Issue a Credential
With a template created, we can start issuing credentials. To do this, we need to enter the subject’s email address, or wallet address, if they have one.
Then, we need to specify the exact data required by the Verifiable Credential type, i.e. course name and grade. The credential’s contents are identified by the “subject” key within the credential.
If you don't include an expiration date, the credential will not expire. You can always revoke the credential in the
future, if necessary.
```javascript issueCredential.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const templateId = "YOUR_TEMPLATE_ID";
const recipientEmail = "TEST_EMAIL_ADDRESS";
const url = `https://${env}.crossmint.com/api/v1-alpha1/credentials/templates/${templateId}/vcs`;
const options = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
recipient: `email:${recipientEmail}:polygon-amoy`,
credential: {
// The CourseCompletionCertificate credential type requires course and grade declarations
subject: {
course: "Blockchain 101",
grade: "A",
},
expiresAt: "2034-02-02",
},
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
```json JSON theme={null}
{
"id": "6728f773-61af-4732-a34f-b5a50d8b9872",
"onChain": {
"status": "pending",
"chain": "polygon-amoy",
"contractAddress": "0x45c9597b5441289C134E554990cdb647f65df5FB"
},
"credentialId": "urn:uuid:e6b3dc76-a337-437b-ac1b-3d330e66e1a6",
"actionId": "6728f773-61af-4732-a34f-b5a50d8b9872"
}
```
Save the credential `id` from the response for the next step.
### 5. Retrieve a Credential
You can retrieve a Verifiable Credential using different identifiers associated with the credential itself or the NFT associated with the credential.
* [Get Verifiable Credential by ID](/api-reference/verifiable-credentials/credentials/retrieve-credential-by-id) uses the format: `urn:uuid:`
* [Get Verifiable Credential by NFT Locator](/api-reference/verifiable-credentials/credentials/retrieve-credential-by-nft-locator) uses the format: `::`
* [Get Verifiable Credential by NFT ID](/api-reference/verifiable-credentials/credentials/retrieve-credential-by-nft) uses crossmint's internal NFT ID
```javascript getCredential.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const credentialId = "YOUR_CREDENTIAL_ID";
const url = `https://${env}.crossmint.com/api/v1-alpha1/credentials/${credentialId}`;
const options = {
method: "GET",
headers: {
accept: "application/json",
"x-api-key": apiKey,
},
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
```json JSON theme={null}
{
"unencryptedCredential": {
"id": "urn:uuid:e6b3dc76-a337-437b-ac1b-3d330e66e1a6",
"credentialSubject": {
"course": "Blockchain 101",
"grade": "A",
"id": "did:polygon-amoy:0x2da3151C266185c4861b45277dbbe56Cb613963e"
},
"validUntil": "2034-02-02",
"nft": {
"tokenId": "1",
"chain": "polygon-amoy",
"contractAddress": "0x45c9597b5441289C134E554990cdb647f65df5FB"
},
"issuer": {
"id": "did:polygon-amoy:0x1785A5DE9e0F06791393739De496a0a2c9ACA855"
},
"type": ["VerifiableCredential", "Course completion"],
"validFrom": "2025-05-28T14:31:03.514Z",
"@context": ["https://www.w3.org/ns/credentials/v2"],
"proof": {
"verificationMethod": "did:polygon-amoy:0x1785A5DE9e0F06791393739De496a0a2c9ACA855#evmAddress",
"created": "2025-05-28T14:31:03.514Z",
"proofPurpose": "assertionMethod",
"type": "EthereumEip712Signature2021",
"proofValue": "0x17f624a23b620de2867dbefbeb5214e5693a4b470897302f91f870383d4f5ee7754600d1fb72b73c926fed56aac20724ce87d49e0078e0b13f02734a199ceaa61b",
"eip712": [Object]
}
}
}
```
### 6. Verify a Credential
Verifying a credential can be done in different ways, the easiest one is to call the verify API route. You can also accomplish
this with the [SDK](/sdk-reference/credentials/introduction).
To verify a credential's validity via API use the following script:
```javascript verifyCredential.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const credentialId = "YOUR_CREDENTIAL_ID";
const url = `https://${env}.crossmint.com/api/v1-alpha1/credentials/verification/verify`;
const options = {
method: "POST",
headers: {
"accept": "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
// paste credential object here
credential : {
id: "urn:uuid:e6b3dc76-a337-437b-ac1b-3d330e66e1a6",
...
}
})
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
```json JSON theme={null}
{ isValid: true }
```
### 7. Revoke a Credential (Optional)
To revoke a credential, you can directly use the [revoke credential API](/api-reference/verifiable-credentials/credentials/revoke-credential):
```javascript revokeCredential.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const credentialId = "YOUR_CREDENTIAL_ID";
const url = `https://${env}.crossmint.com/api/v1-alpha1/credentials/${credentialId}`;
const options = {
method: "DELETE",
headers: {
accept: "application/json",
"x-api-key": apiKey,
},
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
```json JSON theme={null}
{
"actionId": "15bfe43c-3d5d-4d03-9e20-6dbaf01f038f",
"action": "nfts.delete",
"status": "pending",
"data": {
"chain": "polygon-amoy",
"collection": {
"id": "a9e3016b-66f0-40ab-8d72-6546fddc7b72",
"contractAddress": "0x45c9597b5441289C134E554990cdb647f65df5FB"
},
"token": {
"id": "6728f773-61af-4732-a34f-b5a50d8b9872",
"tokenId": "1"
}
},
"startedAt": "2025-05-28T16:53:11.000Z",
"resource": "https://staging.crossmint.com/api/2022-06-09/collections/a9e3016b-66f0-40ab-8d72-6546fddc7b72/nfts/15bfe43c-3d5d-4d03-9e20-6dbaf01f038f"
}
```
## Learn More
For more detailed information about Verifiable Credentials, please visit the dedicated [Verifiable Credentials section](/verifiable-credentials/introduction) which includes comprehensive guides on:
Learn how to define the structure of your credentials.
Create reusable templates for your credentials.
Issue credentials to users.
Verify the authenticity of credentials.
# Quickstart ⚡
Source: https://docs.crossmint.com/minting/quickstarts/ip
Mint IP credentials for creators to secure their work in under 5 minutes
In this quickstart you will learn how to create and manage intellectual property (IP) assets.
Contact [sales](https://www.crossmint.com/contact/sales) to enable this API on your project.
## What are IP credentials?
IP credentials are onchain records of intellectual property rights and authorship, enabled by Story Protocol's infrastructure and issued using Crossmint's APIs.
## Integration steps
### 1. Create a Developer Account
### 2. Get an API Key
Within the "Server-side keys" section, click the "Create new key" button in the top right.
Then, check the necessary scopes under the "Minting API" category: `nfts.create`, `nfts.read`, `collections.create`, `nfts.update` and create your key. Save this key for the next step.
### 3. Create an IP Collection
First, let's create an IP collection to hold our IP assets:
```javascript createIPCollection.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const url = `https://${env}.crossmint.com/api/v1/ip/collections`;
const options = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
metadata: {
description: "A collection of intellectual property assets",
name: "My IP Collection",
symbol: "MPA",
},
chain: "story-testnet",
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
Here is an example response returned from the API call above:
```json JSON theme={null}
{
"id": "7f5e653d-2c9d-4025-98c8-8245c128ad10",
"actionId": "7f5e653d-2c9d-4025-98c8-8245c128ad10",
"metadata": {
"name": "My IP Collection",
"symbol": "MPA",
"description": "A collection of intellectual property assets"
},
"onChain": {
"chain": "story-testnet"
}
}
```
Save the collection `id` from the response for the next step.
### 4. Create an IP Asset
Now, let's create an IP asset within our collection:
```javascript createIPAsset.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const collectionId = "YOUR_COLLECTION_ID";
const url = `https://${env}.crossmint.com/api/v1/ip/collections/${collectionId}/ipassets`;
const options = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
owner: "email:creator@example.com:story-testnet",
nftMetadata: {
name: "Snowflake Funk",
description: "A disco song for the winter holidays",
image: "https://cdn2.suno.ai/image_large_c001fd6e-d6cd-474f-a7b6-6e6a9b3e2515.jpeg",
},
ipAssetMetadata: {
title: "Snowflake Funk",
createdAt: "2025-02-11T11:13:00",
ipType: "music",
image: "https://cdn2.suno.ai/image_large_example.jpeg",
imageHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
mediaUrl: "https://cdn1.suno.ai/c001fd6e-d6cd-474f-a7b6-6e6a9b3e2515.mp3",
mediaHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
mediaType: "audio/mpeg",
creators: [
{
name: "John Doe",
email: "john.doe@example.com",
crossmintUserLocator: "email:john.doe@example.com:story-testnet",
contributionPercent: 100,
},
],
media: [
{
name: "Snowflake Funk",
url: "https://cdn1.suno.ai/c001fd6e-d6cd-474f-a7b6-6e6a9b3e2515.mp3",
mimeType: "audio/mpeg",
},
],
attributes: [
{
key: "Suno Artist",
value: "InfluentialCoda427",
},
{
key: "Source",
value: "Suno.com",
},
],
},
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
Here is an example response returned from the API call above:
```json JSON theme={null}
{
"id": "49b9e141-8fd8-45dc-9646-13a3439c6f20",
"actionId": "49b9e141-8fd8-45dc-9646-13a3439c6f20",
"nftMetadata": {
"name": "Snowflake Funk",
"image": "https://cdn2.suno.ai/image_large_c001fd6e-d6cd-474f-a7b6-6e6a9b3e2515.jpeg",
"description": "A disco song for the winter holidays"
},
"ipAssetMetadata": {
"title": "Snowflake Funk",
"createdAt": "2025-02-11T11:13:00",
"creators": [
{
"name": "John Doe",
"email": "john.doe@example.com",
"crossmintUserLocator": "email:john.doe@example.com:story-testnet",
"contributionPercent": 100
}
],
"media": [
{
"name": "Snowflake Funk",
"url": "https://cdn1.suno.ai/c001fd6e-d6cd-474f-a7b6-6e6a9b3e2515.mp3",
"mimeType": "audio/mpeg"
}
],
"attributes": [
{
"key": "Suno Artist",
"value": "InfluentialCoda427"
},
{
"key": "Source",
"value": "Suno.com"
}
],
"ipType": "music"
},
"licenseTerms": [
{
"type": "non-commercial-social-remixing"
}
],
"onChain": {
"chain": "story-testnet",
"contractAddress": "0x82a72DfFb175DF8AB6d3C2E3888D5314a50C30D0",
"status": "pending"
}
}
```
Save the IP asset `id` from the response for the next steps.
### 5. Retrieve an IP Asset
To retrieve an IP asset:
```javascript getIPAsset.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const collectionId = "YOUR_COLLECTION_ID";
const ipAssetId = "YOUR_IP_ASSET_ID";
const url = `https://${env}.crossmint.com/api/v1/ip/collections/${collectionId}/ipassets/${ipAssetId}`;
const options = {
method: "GET",
headers: {
accept: "application/json",
"x-api-key": apiKey,
},
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
Here is an example response returned from the API call above:
```json JSON theme={null}
{
"id": "49b9e141-8fd8-45dc-9646-13a3439c6f20",
"actionId": "49b9e141-8fd8-45dc-9646-13a3439c6f20",
"nftMetadata": {
"name": "Snowflake Funk",
"image": "ipfs://QmYA9HyvoPmxvsMx216MFkoydtrA1sSxX5A5ixeu5S9VKF",
"description": "A disco song for the winter holidays"
},
"ipAssetMetadata": {
"title": "Snowflake Funk",
"createdAt": "2025-02-11T11:13:00",
"creators": [
{
"name": "John Doe",
"email": "john.doe@example.com",
"crossmintUserLocator": "email:john.doe@example.com:story-testnet",
"contributionPercent": 100
}
],
"media": [
{
"name": "Snowflake Funk",
"url": "https://cdn1.suno.ai/c001fd6e-d6cd-474f-a7b6-6e6a9b3e2515.mp3",
"mimeType": "audio/mpeg"
}
],
"attributes": [
{
"key": "Suno Artist",
"value": "InfluentialCoda427"
},
{
"key": "Source",
"value": "Suno.com"
}
],
"ipType": "music"
},
"licenseTerms": [
{
"type": "non-commercial-social-remixing",
"uri": ""
}
],
"onChain": {
"chain": "story-testnet",
"contractAddress": "0x82a72DfFb175DF8AB6d3C2E3888D5314a50C30D0",
"status": "success",
"ipAssetId": "0xe20936B1C974E7C550b483243719A1B1886D2c6c",
"tokenId": "1",
"txId": "0x4e505bcd429960d3031bd8d1f27bb7ffd2cfa251498c12c16cf2375a14d2bd51",
"owner": "0x1a042BBEFb116072E6AEF8304f9B7D15aA2b4BCF",
"explorerLink": "https://aeneid.explorer.story.foundation/ipa/0xe20936B1C974E7C550b483243719A1B1886D2c6c"
}
}
```
### 6. Update an IP Asset
If needed, you can update an IP asset:
```javascript updateIPAsset.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const collectionId = "YOUR_COLLECTION_ID";
const ipAssetId = "YOUR_IP_ASSET_ID";
const url = `https://${env}.crossmint.com/api/v1/ip/collections/${collectionId}/ipassets/${ipAssetId}`;
const options = {
method: "PATCH",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
ipAssetMetadata: {
title: "Snowflake Funk",
createdAt: "2025-02-11T11:13:00",
ipType: "music",
creators: [
{
name: "John Doe",
email: "john.doe@example.com",
crossmintUserLocator: "email:john.doe@example.com:story-testnet",
contributionPercent: 90,
},
{
name: "Frank L",
crossmintUserLocator: "email:frank.l@example.com:story-testnet",
contributionPercent: 10,
},
],
},
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
Here is an example response returned from the API call above:
```json JSON theme={null}
{
"id": "75a45061-febb-4344-84d6-959276a5696a",
"actionId": "75a45061-febb-4344-84d6-959276a5696a",
"nftMetadata": {
"name": "Snowflake Funk",
"image": "ipfs://QmYA9HyvoPmxvsMx216MFkoydtrA1sSxX5A5ixeu5S9VKF",
"description": "A disco song for the winter holidays"
},
"ipAssetMetadata": {
"title": "Snowflake Funk",
"createdAt": "2025-02-11T11:13:00",
"creators": [ ... ],
"ipType": "music"
},
"licenseTerms": [ { "type": "non-commercial-social-remixing", "uri": "" } ],
"onChain": {
"chain": "story-testnet",
"contractAddress": "0x82a72DfFb175DF8AB6d3C2E3888D5314a50C30D0",
"status": "pending"
}
}
```
## Learn More
For more detailed information about IP assets, please refer to the API reference:
Learn how to create IP collections.
Create IP assets within collections.
Update existing IP assets.
Retrieve IP assets from collections.
# Non-Fungible Tokens (NFTs)
Source: https://docs.crossmint.com/minting/quickstarts/nfts
Mint an NFT in under 5 minutes
In this quickstart you will learn how to create a unique NFT and deliver it to a wallet or email address.
## Integration steps
### 1. Create a Developer Account
### 2. Get an API Key
Within the "Server-side keys" section, click the "Create new key" button in the top right.
Then, check the scopes `nfts.create` and `nfts.read` under the "Minting API"
category and create your key. Save this key for the next step.
### 3. Mint an NFT
We're almost there! With our key created, we're now going to write a small
function that creates an NFT and delivers it to a user.
Create a file (e.g. `mintNFT.js`) and enter this code into it:
```javascript mintNFT.js theme={null}
const apiKey = "YOUR_API_KEY";
const chain = "polygon-amoy"; // or "ethereum-sepolia", "base-sepolia", etc.
const env = "staging"; // or "www"
const recipientEmail = "TEST_EMAIL_ADDRESS";
const recipientAddress = `email:${recipientEmail}:${chain}`;
const url = `https://${env}.crossmint.com/api/2022-06-09/collections/default/nfts`;
const options = {
method: "POST",
headers: {
"accept": "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
recipient: recipientAddress,
metadata: {
name: "Crossmint Test NFT",
image: "https://picsum.photos/400",
description: "My first NFT using Crossmint",
},
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
This code creates an NFT, uploads it to the blockchain, and delivers it to a user wallet, attached to the email address provided. Before running it, be sure to fill in values for:
* `YOUR_API_KEY` with the key obtained in the prior step.
* `TEST_EMAIL_ADDRESS` with an email address you control, for testing.
Now, run the `mintNFT.js` script.
```bash theme={null}
node mintNFT.js
```
After a few seconds, it should return a response indicating the mint has
started processing. Check the full response and save its `actionId` property, as you'll use it later.
Here is an example response returned from the API call above:
```json JSON theme={null}
{
"id": "b3f61f22-c30e-424d-a5ab-d0cbc5c41aab",
"onChain": {
"status": "pending",
"chain": "polygon-amoy",
"contractAddress": "0x921Bc21bf3Fd6568cdC2C904F75b83556062c3d0"
},
"actionId": "b3f61f22-c30e-424d-a5ab-d0cbc5c41aab"
}
```
### 4. Confirm Delivery of the NFT
The mint has started processing. However, blockchains can take a few seconds (or,
at times of extreme network congestion, even minutes) to confirm the operation.
Before showing the user a success screen, the next step is checking the
status of the mint.
To do this, grab the `actionId` received at the end of step 3 and use it
alongside your API key in one of the snippets below.
```javascript mintStatus.js theme={null}
const apiKey = "";
const env = "staging"; // or "www"
const actionId = "";
const url = `https://${env}.crossmint.com/api/2022-06-09/actions/${actionId}`;
const options = {
method: "GET",
headers: { "X-API-KEY": apiKey },
};
fetch(url, options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
```bash cURL theme={null}
# Replace "staging" with "www" for production, and fill in your api key and mint action id
ENV="staging"
API_KEY=""
MINT_ACTION_ID=""
curl --header "x-api-key: $API_KEY" \
-X GET \
https://${ENV}.crossmint.com/api/2022-06-09/actions/${MINT_ACTION_ID}
```
Here is an example response from calling the status API:
```json JSON theme={null}
{
"actionId": "b3f61f22-c30e-424d-a5ab-d0cbc5c41aab",
"action": "nfts.create",
"status": "success",
"data": {
"collection": {...},
"recipient": {...},
"token": {...}
},
"startedAt": "2025-05-27T02:41:08.000Z",
"completedAt": "2025-05-27T02:41:40.000Z",
"resource": "https://staging.crossmint.com/api/2022-06-09/collections/default-polygon-amoy/nfts/b3f61f22-c30e-424d-a5ab-d0cbc5c41aab"
}
```
Pay attention to the "status" field. Once it says "success":
**Congratulations. You have minted your first NFT** 🥷 🎉
For scalable production applications, consider using [webhooks](/minting/nfts/integrate/webhooks-and-status-apis) to
determine when your NFT has been minted, instead of periodically polling for its status via the API.
### 1. Create a Developer Account
### 2. Get an API Key
Within the "Server-side keys" section, click the "Create new key" button in the top right.
Then, check the scopes `nfts.create` and `nfts.read` under the "Minting API"
category and create your key. Save this key for the next step.
### 3. Mint an NFT
We're almost there! With our key created, we're now going to write a small
function that creates an NFT and delivers it to a user.
Create a file (e.g. `mintNFT.js`) and enter this code into it:
```javascript mintNFT.js theme={null}
const apiKey = "YOUR_API_KEY";
const chain = "solana";
const env = "staging"; // or "www"
const recipientEmail = "TEST_EMAIL_ADDRESS";
const recipientAddress = `email:${recipientEmail}:${chain}`;
const url = `https://${env}.crossmint.com/api/2022-06-09/collections/default-solana/nfts`;
const options = {
method: "POST",
headers: {
"accept": "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
recipient: recipientAddress,
metadata: {
name: "Crossmint Test NFT",
image: "https://picsum.photos/400",
description: "My first NFT using Crossmint",
},
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
This code creates an NFT, uploads it to the Solana blockchain, and delivers it to a user wallet, attached to the email address provided. Before running it, be sure to fill in values for:
* `YOUR_API_KEY` with the key obtained in the prior step.
* `TEST_EMAIL_ADDRESS` with an email address you control, for testing.
Now, run the `mintNFT.js` script.
```bash theme={null}
node mintNFT.js
```
After a few seconds, it should return a response indicating the mint has
started processing. Check the full response and save its `actionId` property, as you'll use it later.
Here is an example response returned from the API call above:
```json JSON theme={null}
{
"id": "cace8d7d-e4dd-483d-a780-faf6aa9f761a",
"onChain": {
"status": "pending",
"chain": "solana"
},
"actionId": "cace8d7d-e4dd-483d-a780-faf6aa9f761a"
}
```
### 4. Confirm Delivery of the NFT
The mint has started processing. However, blockchains can take a few seconds (or,
at times of extreme network congestion, even minutes) to confirm the operation.
Before showing the user a success screen, the next step is checking the
status of the mint.
To do this, grab the `actionId` received at the end of step 3 and use it
alongside your API key in one of the snippets below.
```javascript mintStatus.js theme={null}
const apiKey = "";
const env = "staging"; // or "www"
const actionId = "";
const url = `https://${env}.crossmint.com/api/2022-06-09/actions/${actionId}`;
const options = {
method: "GET",
headers: { "X-API-KEY": apiKey },
};
fetch(url, options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
```bash cURL theme={null}
# Replace "staging" with "www" for production, and fill in your api key and mint action id
ENV="staging"
API_KEY=""
MINT_ACTION_ID=""
curl --header "x-api-key: $API_KEY" \
-X GET \
https://${ENV}.crossmint.com/api/2022-06-09/actions/${MINT_ACTION_ID}
```
Here is an example response from calling the status API:
```json JSON theme={null}
{
"actionId": "cace8d7d-e4dd-483d-a780-faf6aa9f761a",
"action": "nfts.create",
"status": "success",
"data": {
"chain": "solana",
"txId": "5KQHjcEq7DWnD1Rzo7RW7rknY3NFeKsJ9Lxkrd1DBPPwxbv9k4JAgxELWW5rqWMd5sXrHDwYzsRMeUok3tqYvTLa",
"collection": {...},
"recipient": {...},
"token": {...}
},
"startedAt": "2025-05-27T03:04:23.000Z",
"completedAt": "2025-05-27T03:04:49.000Z",
"resource": "https://staging.crossmint.com/api/2022-06-09/actions/cace8d7d-e4dd-483d-a780-faf6aa9f761a"
}
```
Pay attention to the "status" field. Once it says "success":
**Congratulations. You have minted your first NFT** 🥷 🎉
For scalable production applications, consider using [webhooks](/minting/nfts/integrate/webhooks-and-status-apis) to
determine when your NFT has been minted, instead of periodically polling for its status via the API.
### 1. Create a Developer Account
### 2. Get an API Key
Within the "Server-side keys" section, click the "Create new key" button in the top right.
Then, check the scopes `nfts.create` and `nfts.read` under the "Minting API"
category and create your key. Save this key for the next step.
### 3. Mint a Compressed NFT
We're almost there! With our key created, we're now going to write a small
function that creates a compressed NFT and delivers it to a user.
Compressed NFTs are a new standard on the Solana blockchain, for minting NFTs with the lowest cost amongst L1 and L2 blockchains, and the highest throughput (thousands of NFTs per second).
Create a file (e.g. `mintCompressedNFT.js`) and enter this code into it:
```javascript mintCompressedNFT.js theme={null}
const apiKey = "YOUR_API_KEY";
const chain = "solana";
const env = "staging"; // or "www"
const recipientEmail = "TEST_EMAIL_ADDRESS";
const recipientAddress = `email:${recipientEmail}:${chain}`;
const url = `https://${env}.crossmint.com/api/2022-06-09/collections/default-solana/nfts`;
const options = {
method: "POST",
headers: {
"accept": "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
recipient: recipientAddress,
metadata: {
name: "Crossmint Compressed NFT",
image: "https://picsum.photos/400",
description: "My first compressed NFT using Crossmint",
},
compressed: true,
reuploadLinkedFiles: false
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
This code creates a compressed NFT, uploads it to the Solana blockchain, and delivers it to a user wallet, attached to the email address provided. Before running it, be sure to fill in values for:
* `YOUR_API_KEY` with the key obtained in the prior step.
* `TEST_EMAIL_ADDRESS` with an email address you control, for testing.
Now, run the `mintCompressedNFT.js` script.
```bash theme={null}
node mintCompressedNFT.js
```
Current Protocol Limitations:
* There's a 10-30 second delay between when a compressed NFT is minted and it shows in wallets.
After a few seconds, it should return a response indicating the mint has
started processing. Check the full response and save its `actionId` property, as you'll use it later.
Here is an example response returned from the API call above:
```json JSON theme={null}
{
"actionId": "de26f73e-b0ae-4ac6-8784-39f20527d39d",
"onChain": {
"status": "pending",
"chain": "solana"
},
"id": "de26f73e-b0ae-4ac6-8784-39f20527d39d"
}
```
### 4. Confirm Delivery of the NFT
The mint has started processing. However, blockchains can take a few seconds (or,
at times of extreme network congestion, even minutes) to confirm the operation.
Before showing the user a success screen, the next step is checking the
status of the mint.
To do this, grab the `actionId` received at the end of step 3 and use it
alongside your API key in one of the snippets below.
```bash cURL theme={null}
# Replace "staging" with "www" for production, and fill in your api key and mint action id
ENV="staging"
API_KEY=""
MINT_ACTION_ID=""
curl --header "x-api-key: $API_KEY" \
-X GET \
https://${ENV}.crossmint.com/api/2022-06-09/actions/${MINT_ACTION_ID}
```
```javascript mintStatus.js theme={null}
const apiKey = "";
const env = "staging"; // or "www"
const actionId = "";
const url = `https://${env}.crossmint.com/api/2022-06-09/actions/${actionId}`;
const options = {
method: "GET",
headers: { "X-API-KEY": apiKey },
};
fetch(url, options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
Here is an example response from calling the status API:
```json JSON theme={null}
{
"actionId": "de26f73e-b0ae-4ac6-8784-39f20527d39d",
"action": "nfts.create",
"status": "success",
"data": {
"chain": "solana",
"txId": "dLFcyYtgFrrX7VkXh8U8JUmECNykH7VASzovNzM67NPRypCzMkTnx1ffv7dB99N4YrAT2mDRqszGofKunnazbXX",
"collection": {...},
"recipient": {...},
"token": {...}
},
"startedAt": "2025-05-27T03:09:47.000Z",
"completedAt": "2025-05-27T03:09:52.000Z",
"resource": "https://staging.crossmint.com/api/2022-06-09/actions/de26f73e-b0ae-4ac6-8784-39f20527d39d"
}
```
Pay attention to the "status" field. Once it says "success":
**Congratulations. You have minted your first compressed NFT** 🥷 🎉
Other explorers, like Solscan, still have not added support.
For scalable production applications, consider using [webhooks](/minting/nfts/integrate/webhooks-and-status-apis) to
determine when your NFT has been minted, instead of periodically polling for its status via the API.
## View your NFTs
1. If the NFTs were delivered to an **email address**, the recipient can see them by:
* Logging into their wallet from Crossmint's [website](https://www.crossmint.com). For staging, they must use [https://staging.crossmint.com](https://staging.crossmint.com).
* From your website if you use [embedded wallets](/wallets/overview). See the API for [getting the NFTs](/api-reference/wallets/get-nfts-from-wallet) in a wallet.
2. If the NFTs were delivered to a **wallet address**, the user will be able to see them there directly, connecting to testnet if needed, or on the testnet blockchain explorer.
And voilá, there's your NFT! Now think of all the cool things you can build
with this, at scale :)
## Launching in Production
For production, the steps are almost identical, but some changes are required:
1. Create a developer account on the [production console](https://www.crossmint.com/console).
2. Add credits to your account from [Billing & Usage](https://www.crossmint.com/console/billing).
3. Then, create a production key on the [API Keys](https://www.crossmint.com/console/projects/apiKeys) page with the
same API scopes.
4. Modify all code snippets with `const env = "www"`, so they use the production APIs. You may also need to change the
`chain` variable to match your production blockchain.
5. Check the guide with [best practices](/minting/advanced/best-practices)
## Learn More
Learn how to create and manage NFT collections.
Check out more advanced options for minting digital assets.
Update and delete tokens after minting.
# Semi-Fungible Tokens (SFTs)
Source: https://docs.crossmint.com/minting/quickstarts/sfts
Mint and send limited sets of identical digital assets in under 5 minutes
In this quickstart you will learn how to create semi-fungible tokens (SFTs) and deliver them to a wallet or email address.
## What are SFTs?
SFTs (semi-fungible tokens) follow the ERC-1155 standard. Each token is a replica of a predefined template. Each collection (smart contract) can contain multiple templates, which can contain many tokens.
This API only supports EVM chains self-serve. Contact us if you need support for another chain.
## Integration steps
### 1. Create a Developer Account
### 2. Get an API Key
Within the "Server-side keys" section, click the "Create new key" button in the top right.
Then, check the scopes `nfts.create`, `nfts.read`, and `collections.create` under the "Minting API"
category and create your key. Save this key for the next step.
### 3. Create an SFT collection
With our key created, we're now going to create an SFT collection to hold our templates.
```javascript createCollection.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const url = `https://${env}.crossmint.com/api/2022-06-09/collections`;
const options = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
chain: "polygon-amoy",
fungibility: "semi-fungible",
metadata: {
name: "My SFT Collection",
imageUrl: "https://www.crossmint.com/assets/crossmint/logo.png",
description: "A new collection with its own dedicated smart contract",
},
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
```json JSON theme={null}
{
"id": "5263650e-6d43-4ed3-9e31-0cf593d076a4",
"metadata": {
"name": "My SFT Collection",
"description": "A new collection with its own dedicated smart contract",
"imageUrl": "https://www.crossmint.com/assets/crossmint/logo.png",
"symbol": "XMINT"
},
"fungibility": "semi-fungible",
"onChain": {
"chain": "polygon-amoy",
"type": "erc-1155"
},
"actionId": "5263650e-6d43-4ed3-9e31-0cf593d076a4"
}
```
Save the `id` from the response as your `COLLECTION_ID` for the next steps.
### 4. Create a template within that collection
Now, let's create a template within our collection:
```javascript createTemplate.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const collectionId = "YOUR_COLLECTION_ID";
const url = `https://${env}.crossmint.com/api/2022-06-09/collections/${collectionId}/templates`;
const options = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
onChain: {
tokenId: "1",
},
supply: {
limit: 10,
},
metadata: {
name: "My template",
image: "https://www.crossmint.com/assets/crossmint/logo.png",
description: "A new token template for my ERC1155 collection",
},
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
```json JSON theme={null}
{
"templateId": "58b0c1aa-e457-48dd-bb55-5a27e6a92f74",
"metadata": {
"name": "My template",
"image": "ipfs://bafkreigbqsmxzkbjgbwtj6exfdt5z3t3swgoysf7hr6vjzddqnmykj6x2u",
"description": "A new token template for my ERC1155 collection"
},
"onChain": {
"tokenId": "1"
},
"supply": {
"limit": "10",
"minted": "0"
}
}
```
Save the `templateId` from the response for the next step.
### 5. Mint an SFT from a template
Now, let's mint an SFT from our template and send it to a wallet or email address:
```javascript mintSFT.js theme={null}
const apiKey = "YOUR_API_KEY";
const env = "staging"; // or "www"
const collectionId = "YOUR_COLLECTION_ID";
const templateId = "YOUR_TEMPLATE_ID";
const recipientEmail = "TEST_EMAIL_ADDRESS";
const url = `https://${env}.crossmint.com/api/2022-06-09/collections/${collectionId}/sfts`;
const options = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
templateId: templateId,
recipient: `email:${recipientEmail}:polygon-amoy`,
amount: 1,
}),
};
fetch(url, options)
.then((res) => res.json())
.then((json) => console.log(json))
.catch((err) => console.error("error:" + err));
```
```json JSON theme={null}
{
"actionId": "a91c15e3-60f2-4a45-bf1a-cee508981667",
"action": "nfts.create",
"status": "pending",
"data":
{
"chain": "polygon-amoy",
"collection":
{
"id": "84e3d617-9c1b-4e7a-9686-522a9ea7c520",
"contractAddress": "0x9b8ab8949bd7E73E61945b88F7fe12151f98ad3C"
},
"recipient":
{
"walletAddress": "0xcFDc00Cf926A5053f9Cdf004e6DF17e6dEB2E146",
"email": "test@example.com"
},
"token":
{
"id": "a91c15e3-60f2-4a45-bf1a-cee508981667"
}
},
"startedAt": "2024-01-02T22:05:01.000Z",
"resource": "https://staging.crossmint.com/api/2022-06-09/actions/a91c15e3-60f2-4a45-bf1a-cee508981667"
}
```
### 6. Confirm Delivery of the SFT
The mint has started processing. However, blockchains can take a few seconds (or,
at times of extreme network congestion, even minutes) to confirm the operation.
Before showing the user a success screen, the next step is checking the
status of the mint.
To do this, grab the `actionId` received at the end of step 5 and use it
alongside your API key in one of the snippets below.
```javascript mintStatus.js theme={null}
const apiKey = "";
const env = "staging"; // or "www"
const actionId = "";
const url = `https://${env}.crossmint.com/api/2022-06-09/actions/${actionId}`;
const options = {
method: "GET",
headers: { "X-API-KEY": apiKey },
};
fetch(url, options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
```bash cURL theme={null}
# Replace "staging" with "www" for production, and fill in your api key and mint action id
ENV="staging"
API_KEY=""
MINT_ACTION_ID=""
curl --header "x-api-key: $API_KEY" \
-X GET \
https://${ENV}.crossmint.com/api/2022-06-09/actions/${MINT_ACTION_ID}
```
Here is an example response from calling the status API:
```json JSON theme={null}
{
"actionId": "a91c15e3-60f2-4a45-bf1a-cee508981667",
"action": "nfts.create",
"status": "success",
"data": {
"collection": {},
"recipient": {},
"token": {}
},
"startedAt": "2024-01-02T22:05:01.000Z",
"completedAt": "2024-01-02T22:06:04.000Z",
"resource": "https://staging.crossmint.com/api/2022-06-09/actions/a91c15e3-60f2-4a45-bf1a-cee508981667"
}
```
Pay attention to the "status" field. Once it says "success":
**Congratulations. You have minted your first SFT** 🥷 🎉
For scalable production applications, consider using [webhooks](/minting/nfts/integrate/webhooks-and-status-apis) to
determine when your SFT has been minted, instead of periodically polling for its status via the API.
## View your SFTs
1. If the SFTs were delivered to an **email address**, the recipient can see them by:
* Logging into their wallet from Crossmint's [website](https://www.crossmint.com). For staging, they must use [https://staging.crossmint.com](https://staging.crossmint.com).
* From your website if you use [embedded wallets](/wallets/overview). See the API for [getting the NFTs](/api-reference/wallets/get-nfts-from-wallet) in a wallet.
2. If the SFTs were delivered to a **wallet address**, the user will be able to see them there directly, connecting to testnet if needed, or on the testnet blockchain explorer.
And voilá, there's your SFT! Now think of all the cool things you can build
with this, at scale :)
## Launching in Production
For production, the steps are almost identical, but some changes are required:
1. Create a developer account on the [production console](https://www.crossmint.com/console).
2. Add credits to your account from [Billing & Usage](https://www.crossmint.com/console/billing).
3. Then, create a production key on the [API Keys](https://www.crossmint.com/console/projects/apiKeys) page with the
same API scopes.
4. Modify all code snippets with `const env = "www"`, so they use the production APIs. You may also need to change the
`chain` variable to match your production blockchain.
5. Check the guide with [best practices](/minting/advanced/best-practices)
## Learn More
Learn how to create and manage SFT collections.
Check out more advanced options for minting.
Update and delete tokens after minting.
# Create Credential Templates
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/create-credential-templates
Create Verifiable Credential templates with a single API call
A Verifiable Credential template is equivalent to an NFT collection. Similar to how a NFT collection groups NFTs together, a Verifiable Credential template references the credential type and additional credential configurations that credentials adhere to. Every Verifiable Credential must belong to a template.
When defining a template, the issuer initially specifies a **credentials** object. Within that the following needs to be provided:
* **type** (required): A credential's private data schema is referred to as "type". Types act as a protective measure, preventing the addition of unauthorized fields and, as a result, the tampering of the Verifiable Credential. You can [define your own](/minting/verifiable-credentials/integrate/create-credential-types).
* **encryption** (required): The method chosen to protect the credential's private data. If set to `none` the credential's private data will be stored in plain text. To encrypt and protect the credential's private data, read about the supported [encryption modalities](/minting/verifiable-credentials/integrate/encrypt-credentials).
* **storage** (required): The location where credentials defined by this template will be stored. Such storage can be on Crossmint, another company's database, or decentralized storage. Read about the supported [storage modalities](/minting/verifiable-credentials/integrate/store-credentials).
The template issuer must then specify the template's public metadata:
* **metadata** (required): A template's public name, description and imageUrl. Name is required, while the other attributes are optional.
Finally, the issuer provides information on:
* **chain** (required): The blockchain where the credential's contract and associated NFTs will be registered. Verifiable Credentials supports all EVM chains. Interested in launching your verifiable credentials on a specific blockchain? [Get in touch](https://www.crossmint.com/contact/sales).
* **delegatedSignature** (optional): A non-custodial wallet of choice to be used for signing credential issuance. By default, the issuer's Crossmint-managed custodial wallet will be used. Read more about delegated signatures [here](/minting/verifiable-credentials/integrate/delegated-signatures).
## 1. Create a Credential Template
To issue a Verifiable Credential you have to first create the template that this credential will adhere to. You can do so via a single API call (requires the API key scope `templates.create`):
```javascript createTemplate.js theme={null}
const myApiKey = ""; // Replace with key from step 2
const templateParams = {
credentials: {
type: "crossmint:5fe6040e-07a1-48bb-97a3-b588a7e927d2:CourseCompletionCertificate",
encryption: "none",
storage: "crossmint",
},
metadata: {
name: "Satoshi University Credentials",
description: "Credentials accredited by Satoshi University",
imageUrl: "https://picsum.photos/400",
},
chain: "polygon-amoy",
};
const options = {
method: "POST",
headers: {
"X-API-KEY": myApiKey,
"Content-Type": "application/json",
},
body: JSON.stringify(templateParams),
};
fetch("https://staging.crossmint.com/api/v1-alpha1/credentials/templates/", options)
.then((response) => response.json())
.then((response) => console.log(JSON.stringify(response)))
.catch((err) => console.error(err));
```
The template's metadata will be used by default for the NFT metadata, unless explicitly set when issuing a new credential.
## 2. Check the status of your template
It takes a few seconds (up to a minute, depending on the blockchain and how congested it is) to deploy a template.
To check the template's status, you will need the `nfts.create` API scope and run the following code.
```typescript checkTemplateStatus.js theme={null}
const templateId = "";
const options = { method: "GET", headers: { "X-API-KEY": "" } };
fetch(`https://staging.crossmint.com/api/2022-06-09/actions/${templateId}`, options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
Test any API in seconds directly from the docs.
Contact our sales team for support.
# Create Credential Types
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/create-credential-types
Specify the schema that the Verifiable Credential adheres to
In the realm of Verifiable Credentials, each credential must be associated with a schema. The credential's schema is referred to as "type".
Credential types outline the attributes and their respective data types that the credential contains. The purpose of types is to ensure the credential's authenticity. Types allow you to specify the attributes you are interested in certifying and act as a protective measure, preventing the addition of unauthorized attributes and, as a result, the tampering of the Verifiable Credential.
It's important to note that all credentials within a single template will share the same type. Trying to issue or verify a credential with different field names or types will result in an error.
By adhering to this structure, we maintain the integrity and verifiability of the credentials, ensuring they serve their purpose effectively.
## Example Types
Credentials play a crucial role in verifying and validating various aspects of identity, achievement, and authorization. Below are some examples of credential types you can create and manage through Crossmint:
### Proof of Employment
* Purpose: Issued to individuals that want to prove their employment.
* Name: `ProofOfEmployment`
```typescript theme={null}
ProofOfEmployment {
position: string;
department: string;
startDate: string;
}
```
The payload used to create this type is the following:
```json theme={null}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Proof of Employment",
"description": "Schema for verifying employment details",
"type": "object",
"properties": {
"credentialSubject": {
"type": "object",
"properties": {
"position": {
"type": "string"
},
"department": {
"type": "string"
},
"startDate": {
"type": "string"
}
},
"required": ["position", "department", "startDate"],
"additionalProperties": false
}
}
}
```
To reference this example type in your credential template definition, use `crossmint:bfb292e7-2700-4924-9213-478f3d71f2d8:ProofOfEmployment`.
### Course Completion Certificates
* Purpose: Issued to students upon the successful completion of a course. The certificate may indicate that the student has mastered a specific skill or knowledge.
* Type: `CourseCompletionCertificate`
```typescript theme={null}
CourseCompletionCertificate {
course: string;
grade: string;
}
```
If you want to use this example type in your credential template definition, you can reference it as `crossmint:bfb292e7-2700-4924-9213-478f3d71f2d8:CourseCompletionCertificate`.
### Membership Credentials
* Purpose: Provided to individuals who are members of a specific organization or club. The certificate may grant access to exclusive resources and events.
* Name: `Membership`
```typescript theme={null}
Membership {
organizationName: string;
membershipType: string;
issueDate: string;
benefits: string[];
status: string;
}
```
If you want to use this example type in your credential template definition, you can reference it as `crossmint:bfb292e7-2700-4924-9213-478f3d71f2d8:Membership`.
### Event Participation Certificates
* Purpose: Issued to individuals who attend or participate in specific events.
* Name: `EventParticipation`
```typescript theme={null}
EventParticipation {
eventName: string;
eventDate: string;
location: string;
description: string;
}
```
If you want to use this example type in your credential template definition, you can reference it as `crossmint:bfb292e7-2700-4924-9213-478f3d71f2d8:EventParticipation`.
## Types Creation Quickstart
This example creates a custom type, `CourseCompletionCertificate`, for a credential that represents a course completion. The credential will contain two fields: `course` and `passed`.
```javascript createType.js theme={null}
const typeName = "CourseCompletionCertificate";
const schema = {
$schema: "https://json-schema.org/draft/2020-12/schema",
title: "Course completion",
description: "Describes the course completed and the assigned grade",
type: "object",
properties: {
credentialSubject: {
type: "object",
properties: {
course: {
type: "string",
},
grade: {
type: "string",
},
id: {
// Optional, will be added automatically
type: "string",
},
},
required: ["course", "grade"],
additionalProperties: false,
},
},
};
const options = {
method: "PUT",
headers: {
"X-API-KEY": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify(schema),
};
fetch(`https://staging.crossmint.com/api/v1-alpha1/credentials/types/${typeName}`, options)
.then((response) => response.json())
.then((response) => console.log(JSON.stringify(response)))
.catch((err) => console.error(err));
```
Copy the `createType.js` file in the tab above, add your API key to the `X-API-KEY` header (or [create one](/minting/quickstarts/credentials#2-get-an-api-key) if you haven't already) and run via node.
```shell theme={null}
node createType.js
```
The response, shown below, includes the credential type name.
```json Response theme={null}
{
"id": "crossmint:bfb292e7-2700-4924-9213-478f3d71f2d8:CourseCompletionCertificate", // Internal type id
"typeSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Course completion", // Veryfiable credential `type` name.
"description": "Describes the course completed and the assigned grade",
"type": "object",
"properties": {
"credentialSubject": {
"type": "object",
"properties": {
"course": {
"type": "string"
},
"grade": {
"type": "string"
},
"id": {
"type": "string"
}
},
"required": ["course", "grade"],
"additionalProperties": false
}
},
"$id": "https://staging.crossmint.com/api/v1-alpha1/credentials/types/crossmint:bfb292e7-2700-4924-9213-478f3d71f2d8:CourseCompletionCertificate" // External schema URI
}
}
```
The type id defined will be used when you create your [credential template](/minting/verifiable-credentials/integrate/create-credential-templates).
You can also utilize the `POST /credentials/types/` endpoint to generate a random uuid for the new credential type.
For internal use within Crossmint, reference the type using `id`. The `$id` will be included as the credentials
schema by Crossmint but should not be used to reference the type in Crossmint APIs.
Example of a credentials using the type `"crossmint:bfb292e7-2700-4924-9213-478f3d71f2d8:CourseCompletionCertificate"` just created.
```json Example Credential theme={null}
{
"id": "urn:uuid:820b09e8-bd6d-4680-b2b1-b564169fde8a",
"credentialSubject": {
"course": "Math",
"grade": "A",
"id": "did:polygon-amoy:0x1B887669437644aA348c518844660ef8d63bd643"
},
"nft": {
"tokenId": "7",
"chain": "polygon-amoy",
"contractAddress": "0xb14Bf8EEa68026a2746350059389Bd119CFB8E29"
},
"issuer": {
"id": "did:polygon-amoy:0xd9d8BA9D5956f78E02F4506940f42ac2dAB9DABd"
},
"type": [
"VerifiableCredential",
"Course completion" // Schema.title
],
"validFrom": "2024-11-19T18:41:30.503Z",
"@context": [
"https://www.w3.org/ns/credentials/v2"
],
"credentialSchema": {
"id": "https://staging.crossmint.com/api/v1-alpha1/credentials/types/crossmint:bfb292e7-2700-4924-9213-478f3d71f2d8:CourseCompletionCertificate", // Schema.$id
"type": "JsonSchema"
},
"proof": { ... }
}
```
## Types Creation Deep Dive
In the realm of Verifiable Credentials, types are defined using the JSON schema. For more details, refer to the [W3C VC JSON Schema](https://www.w3.org/TR/vc-json-schema/).
When calling the create type API an invalid JSON schema will result in an error.
### Type schema structure
Below is an example of a JSON schema structure for defining a credential type:
```json Example Schema Payload theme={null}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Example nested schema",
"description": "A schema capturing a human name and a nested `additionalInfo` object",
"type": "object",
"properties": {
"credentialSubject": {
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"id": {
// This will be added automatically, is not necessary
"type": "string"
},
"additionalInfo": {
"type": "object",
"properties": {
"age": {
"type": "integer"
},
"isActive": {
"type": "boolean"
},
"tags": {
// Optional, therefore is not added to `required`
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["age", "isActive"],
"additionalProperties": false // Needs to be repeated for each nested object
}
},
"required": ["firstName", "additionalInfo"],
"additionalProperties": false
}
}
}
```
#### Explanation of Each Part:
* **\$schema**: This should always be set to "[https://json-schema.org/draft/2020-12/schema](https://json-schema.org/draft/2020-12/schema)".
* **\$id**: This field should NOT be defined, will be generated by Crossmint and returned in the response.
* **title**: Represents the name of the type.
* **description**: A brief description of the schema.
* **type**: Must be set to "object".
* **properties**: Must include `credentialSubject` as an object.
#### Important Notes:
* The credential subject is the section that needs to be defined. Any field or nested field can be added, as long as it is valid JSON schema.
* The `credentialSubject.properties.id` will be automatically added with the type "string", this is used to specify the wallet and therefore the identity of the subject.
* `additionalProperties` should always be set to `false` to prevent unauthorized fields from being added.
* All specified fields should be marked as `required` unless they are optional. This ensures that missing fields will trigger an error during schema validation.
* `additionalProperties` and `required` apply only at the level they are specified. For nested objects, these need to be repeated.
### Type import
If you already have a JSON schema type, you can easily import it by calling the same endpoint with the following structure:
```javascript importType.js theme={null}
const payload = {
$id: "https://raw.githubusercontent.com/decentralized-identity/credential-schemas/main/schemas/ExampleEntity/ExampleNameSchema.json",
};
const options = {
method: "POST",
headers: {
"X-API-KEY": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify(schema),
};
fetch(`https://staging.crossmint.com/api/v1-alpha1/credentials/types`, options)
.then((response) => response.json())
.then((response) => console.log(JSON.stringify(response)))
.catch((err) => console.error(err));
```
```json Response theme={null}
{
"id": "crossmint:e62564a7-06eb-4f65-b389-eb3b7a4f6f98:10ede8a6",
"typeSchema": {
"$id": "https://raw.githubusercontent.com/decentralized-identity/credential-schemas/main/schemas/ExampleEntity/ExampleNameSchema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Example Name schema",
"description": "A schema capturing a human name",
"type": "object",
"properties": {
"credentialSubject": {
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"id": {
"type": "string"
}
},
"required": ["firstName"],
"additionalProperties": false
}
}
}
}
```
#### Important Notes for Import:
* All notes for type creation apply here as well.
* `credentialSubject.properties.id` will not be added for imported schemas. If the original schema does not include this specification, the resulting credentials will not include the user wallet in the `subject.id`. To know the subject of a credential, checking the NFT owner will be necessary.
To follow along and test the API calls use a tool like Postman or our [API
playground](/api-reference/verifiable-credentials/types/create-named-type).
Test any API in seconds directly from the docs.
Contact our sales team for support.
# Decrypt Credentials
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/decrypt-credentials
Decrypt credentials you care about
As a credential issuer you can use the [retrieval endpoints](/minting/verifiable-credentials/integrate/retrieve-credentials) and Crossmint will autodecrypt the credential for you.
As a verifier, however, working with [encryption modalities](/minting/verifiable-credentials/integrate/encrypt-credentials) to decrypt credentials requires deep understanding of their APIs, managing cryptographic operations securely, and handling user signature interactions.
The Verifiable Credentials SDK encapsulates all this functionality for verifiers, ensuring secure and seamless decryption.
* Upon a verifier's request, the SDK prompts the credential subject to sign a message proving they are the credential subject, and approving the credential's decryption.
* Once the subject approves, Crossmint decrypts the credential for the verifier to verify.
If the credentials are encrypted using the Lit protocol, in order to decrypt them, the SDK provides a `Lit.decrypt(encryptedCredential)` function.
If the credentials are encrypted using Crossmint, the SDK provides a `CrossmintMetamaskDecrypt().decrypt(encryptedCredential)` function that decrypts the credential, assuming the user has a Metamask wallet. For the support of additional wallets during decryption, review our SDK reference.
A Crossmint API key (`credentials.decrypt`) is needed when:
* Crossmint has encrypted the data (`crossmint-recoverable` encryption modality)
* Data is encrypted via Lit but the verifier wants to delegate Lit network billing to Crossmint
Decryption will not work server-side when using Lit.
In both cases, the user will be prompted to sign a message with their wallet to authenticate the decryption, then the credential will be decrypted.
```typescript decrypt.ts theme={null}
import * as sdk from '@crossmint/client-sdk-verifiable-credentials';
sdk.CrossmintAPI.init('YOUR_API_KEY');
const encryptedCredential = ;
if (collection.metadata.credentialMetadata.encryption.type==VerifiableCredentialEncryptionType.DECENTRALIZED_LIT){
const decryptedData = await new sdk.Lit("staging").decrypt(encryptedCredential);
}else if (collection.metadata.credentialMetadata.encryption.type==VerifiableCredentialEncryptionType.CROSSMINT_RECOVERABLE){
const decryptedData = await new sdk.CrossmintMetamaskDecrypt().decrypt(encryptedCredential);
} else {
throw new Error("Not supported")
}
console.log("Decrypted credential:", decryptedData);
```
```json Response theme={null}
{
"unencryptedCredential": {
"id": "urn:uuid:4c9c41af-8410-4265-9fe4-3674d74e80d9",
"credentialSubject": {
"course": "Blockchain 101",
"grade": "A",
"id": "did:polygon-amoy:0xbFB0d0F9d49d80062103199c5aD309CE7a808039"
},
"validUntil": "2034-02-02",
"nft": {
"tokenId": "1",
"chain": "polygon-amoy",
"contractAddress": "0x288AAbb7D72A041Ea10719d2d676B65D2E9689F5"
},
"issuer": { "id": "did:polygon-amoy:0xB658f974Fe3744A6F9344810BC88e021E31B4d3e" },
"type": [
"VerifiableCredential",
"crossmint:5fe6040e-07a1-48bb-97a3-b588a7e927d2:courseCompletionQuickstart"
],
"validFrom": "2024-09-14T13:59:34.918Z",
"@context": [
"https://www.w3.org/2018/credentials/v1"
],
"proof": {
"verificationMethod": "did:polygon-amoy:0xB658f974Fe3744A6F9344810BC88e021E31B4d3e#evmAddress",
"created": "2024-09-14T13:59:34.918Z",
"proofPurpose": "assertionMethod",
"type": "EthereumEip712Signature2021",
"proofValue": "0x80587c5e9a801f4eed5bca95c48083277fc2ffe2fc1254f230f1b8c11cab59f63afaaa29bf29007129068243feb0069c46a8e583a64e64ff2517a2e3babd18101b",
"eip712": {
"domain": {
"name": "Crossmint",
"version": "0.1",
"chainId": 4,
"verifyingContract": "0xD8393a735e8b7B6E199db9A537cf27C61Aa74954"
},
"types": {
"VerifiableCredential": [
{ "name": "@context", "type": "string[]" },
{ "name": "type", "type": "string[]" },
{ "name": "id", "type": "string" },
{ "name": "issuer", "type": "Issuer" },
{ "name": "credentialSubject", "type": "CredentialSubject" },
{ "name": "validFrom", "type": "string" },
{ "name": "validUntil", "type": "string" },
{ "name": "nft", "type": "Nft" }
],
"CredentialSubject": [
{ "name": "id", "type": "string" },
{ "name": "course", "type": "string" },
{ "name": "grade", "type": "string" }
],
"Issuer": [
{ "name": "id", "type": "string" }
],
"Nft": [
{ "name": "tokenId", "type": "string" },
{ "name": "contractAddress", "type": "string" },
{ "name": "chain", "type": "string" }
]
},
"primaryType": "VerifiableCredential"
}
}
}
}
```
The decrypted credential content can now be reviewed and verified by the verifier.
# Delegated Signature
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/delegated-signatures
Sign credentials with your wallet of choice
Credential issuers, by default, sign credentials using their custodial wallet, managed by Crossmint. This means that Crossmint handles the management of the wallet used for signing, providing a default and streamlined solution for issuers.
If an issuer wants to sign credentials using a separate wallet they own, they can do so by providing a signing endpoint. This endpoint is essentially a service or URL that the issuer can set up, which Crossmint will use to access the issuer's chosen wallet for signing credentials.
Delegated signature and encryption are not concurrently supported for the moment, so it is advised to also use
[delegated storage](/minting/verifiable-credentials/integrate/store-credentials#delegated-storage) for sensitive
data.
## Template Creation
Upon template creation, customers can specify parameters that configure the `delegatedSignature` specification:
* `issuerDid`: The desired issuer DID represents the entity that will be issuing the credentials.
* `endpoint`: The signing endpoint is the URL or endpoint where the credential signing process takes place
* `token`: The Auth token for the endpoint is a secure token used to authenticate requests to the signing endpoint. This token ensures that only authorized entities can access the signing endpoint.
```json theme={null}
{
...standard_template_params,
delegatedSignature: {
issuerDid: "did:polygon:<0xADDRESS>",
endpoint: "string",
token: "string"
}
}
```
The credential issuance process remains unchanged, except for one crucial modification. Instead of directly calling `wallet.signTypedData(payload)` using Crossmint's wallet, Crossmint sends the payload to a designated endpoint. This endpoint is responsible for wrapping the `signTypedData` call and performing the necessary security checks to ensure only authorized payloads are processed, preventing arbitrary or malicious data from being signed.
## Request Payload
Crossmint will POST to the given endpoint with the following payload:
```json theme={null}
{
"issuer": "did:polygon:<0xADDRESS>",
"domain": "ethers.TypedDataDomain",
"types": "Record>",
"message": "",
"token": ""
}
```
## Response Format
The endpoint should return a response with the following format:
```json theme={null}
{
"credential": "",
"issuer": "",
"signature": "string"
}
```
## DID
The issuer's identity is represented by a [DID](https://www.w3.org/TR/did-core/). The DID is a string that uniquely identifies the issuer.
The easiest way to provide a DID is to send the issuer's wallet address as DID, setting issuerDid to `"did:{chain}:{address}"`.
### Web DID
Crossmint also supports [Web DID](https://w3c-ccg.github.io/did-method-web/). This allows issuer's to be identified by a domain name, setting issuerDid to `"did:web:issuer.com"`. webDID leverages existing web domain reputation to establish trust around decentralized identities.
The issuer must have a [DID Document](https://www.w3.org/TR/did-core/#did-documents) hosted at `https://issuer.com/.well-known/did.json` for the webDID mapping to work properly.
The DID document, among other useful information, needs to contain the "service" attribute, stating the issuer's wallet address:
The issuer's wallet is the wallet that will be used to sign the credential and verifiers can recognize this wallet by its related webDID.
```json theme={null}
{
"id": "did:web:myissuer.com",
"service": [
{
"id": "did:web:myissuer.com#wallet",
"type": "wallet",
"serviceEndpoint": "chain:<0xADDRESS>"
}
]
// ... other properties like verificationMethod, authentication, etc.
}
```
# Credential Encryption
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/encrypt-credentials
Encrypt private credential data
Verifiable Credentials can be used to store sensitive information. In such cases, it's crucial to use encrypted credentials to ensure data privacy and security.
The encryption process is designed so that *only* the credential subject and the credential issuer can decrypt the credential. This ensures that sensitive information is only accessible to the relevant parties.
Once the credential is decrypted, it can be verified by anyone. This allows for the credential's authenticity to be confirmed while still maintaining the privacy of its content.
The process of encryption and decryption can be automated using the Crossmint API and SDK. This provides a seamless and secure way to handle sensitive information within Verifiable Credentials.
Encryption will make credential issuance and retrieval slower due to the encryption and decryption process.
## Create an encrypted credential template
When creating a [template](/minting/verifiable-credentials/integrate/create-credential-templates), you can specify an `encryption` parameter to determine the encryption modality you will use. The possible values for this parameter are:
* `crossmint-recoverable`: This option is ideal when users prioritize the convenience and security of having a fallback recovery mechanism. Crossmint will take care of encrypting the credential. The user will be required to prove ownership of the subject wallet to decrypt the credential. As a fallback mechanism, the key is also shared with the credential issuer.
* `decentralized-lit`: This option is better suited for users who prioritize full decentralization and privacy. The credential is encrypted and decrypted using the [Lit protocol](https://www.litprotocol.com/). The user will be required to prove ownership of the subject wallet to decrypt the credential.
* `none`: The certificate is not encrypted.
For example, the `encryption` field part of the template creation request below is set to `crossmint-recoverable`.
```json theme={null}
{
"metadata": {
"name": "Template Name",
"description": "Encrypted credentials template"
},
"chain": "polygon",
"credentials": {
"type": "MyCustomType",
"encryption": "crossmint-recoverable"
}
}
```
During credential creation, the Crossmint API encrypts the credential and sets an access rule that only the credential subject and the credential issuer can decrypt the credential.
Credentials issued from this template will be encrypted for you. Behind the scenes:
* The Crossmint API encrypts the certificate's private data and sets an access rule that only the credential subject and issuer can decrypt the credential.
* Crossmint triggers a webhook to the issuer after a successful credential creation.
* Issuer receives via webhook the credential content and the encryption key.
* As a fallback a copy of the encryption key is sent to the issuer.
## Encrypted credential object
An encrypted credential consists of a 'credentialId' and a base64 encoded encrypted payload.
```json theme={null}
{
"credentialId": "urn:uuid:",
"payload": "base64_encoded_cipher_text"
}
```
## Retrieve an encrypted credential
You can leverage the standard retrieval Crossmint API endpoint to retrieve an encrypted credential:
`GET https://staging.crossmint.com/api/v1-alpha1/credentials/{credentialId}`
Hitting the `GET credentials/{credentialId}` endpoint as the issuer will automatically decrypt the credential for
you and return both the clear and ciphertext.
In case of successful autodecryption, the response object will be:
```json theme={null}
{
"encryptedCredential": {
"credentialId": "urn:uuid:",
"payload": "base64_encoded_cipher_text"
},
"unencryptedCredential": "",
"decryptionError": undefined
}
```
For information on how to decrypt a credential, see [here](/minting/verifiable-credentials/integrate/decrypt-credentials).
# Issue Credentials
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/issue-credentials
Specify the credential's values and a recipient to send it to
With both a credential type and template defined, we can proceed to issue a credential.
## 1. Issue a Credential
First and foremost, the issuer must specify a valid recipient for the credential:
* **recipient** (required): Issuer must specify a credential recipient. This can be done via `email:${userEmail}:${chain}` or `{wallet address}`.
In addition, the issuer must specify a **credential** object containing:
* **subject** (required): Object matching a defined credential type's schema, with values relevant to the recipient. The credential's contents are identified by the "subject" key within the credential, following [W3C naming standards](https://www.w3.org/TR/vc-data-model-2.0/#credential-subject).
The credential subject (credential.subject) must respect the schema of the chosen Verifiable Credential type. You
cannot add additional fields, nor exclude any that were previously set.
* **expiresAt** (required): Specify date, in form such as `2034-02-02` (ISO) or `none` to imply infinite expiration.
If you don't include an expiration date, you must revoke the credential via burning to invalidate it.
Finally, the issuer can provide optional public metadata related to the specific credential, as such:
* **metadata** (optional): NFT metadata is inherited from the VC template metadata. Metadata attributes can be set by the customer to override metadata definition inherited by baseURI.
To issue your first credential, copy the `issueCredential.js` file from below, add your API key, and templateId (that was returned to you in the previous step), and run the file from your terminal.
```javascript issueCredential.js theme={null}
const userEmail = "user@email.com"; // Replace with recipient email
const templateId = "YOUR_TEMPLATE_ID"; // Replace with ID from previous step
const credentialParams = {
recipient: `email:${userEmail}:polygon-amoy`,
credential: {
// The CourseCompletionCertificate credential type requires course and grade declarations
subject: {
course: "Blockchain 101",
grade: "A",
},
expiresAt: "2034-02-02",
},
};
const options = {
method: "POST",
headers: {
"X-API-KEY": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify(credentialParams),
};
fetch(`https://staging.crossmint.com/api/v1-alpha1/credentials/templates/${templateId}/vcs`, options)
.then((response) => response.json())
.then((response) => console.log(JSON.stringify(response)))
.catch((err) => console.error(err));
```
```json Response theme={null}
{
"id": "d7eb777b-e9b4-4f34-ab5f-ce199111166a",
"onChain": {
"status": "pending",
"chain": "polygon-amoy",
"contractAddress": "0xdC444A3F4768185497Dae6250E2F348b99bE89F3"
},
"credentialId": "urn:uuid:64f9877d-a19a-4205-8d61-f8c2abed5766",
"actionId": "d7eb777b-e9b4-4f34-ab5f-ce199111166a"
}
```
```shell theme={null}
node issueCredential.js
```
## 2. Check the status of your credential
You can [set up a webhook](/minting/verifiable-credentials/integrate/webhooks) to know when the Verifiable Credential NFT minting is completed, or call the
[action status API](/api-reference/common/get-action-status) with the returned `actionId`.
To check the template's status you will need the `nfts.create` API scope and run the following code.
```typescript checkTemplateStatus.js theme={null}
const actionId = ""; // Returned from previous step
const options = { method: "GET", headers: { "X-API-KEY": "" } };
fetch(`https://staging.crossmint.com/api/2022-06-09/actions/${actionId}`, options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
Test any API in seconds directly from the docs.
Contact our sales team for support.
# Retrieve Credentials
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/retrieve-credentials
Get access to credentials you care about
Retrieving and effectively presenting credentials is essential for issuers, verifiers, and credential subjects. However, doing so requires interfacing with the blockchain, 3rd party services to fetch NFTs, all while at the same time struggling with unpredictable network issues, and data inconsistency.
Crossmint’s Verifiable Credentials API and SDK simplify credential management, allowing you to seamlessly retrieve and use verifiable credentials where they matter most.
## Verifiable Credential API
Crossmint's Verifiable Credentials API is primarily meant to be used by issuers. It's designed for robust backend operations, enabling seamless, automated credential retrieval without requiring user interaction.
The API integrates with your existing server-side infrastructure, ensuring efficient and scalable credential management. With the API, you can implement complex business logic, providing unparalleled control and customization.
You can retrieve a Verifiable Credential via the Crossmint API using different identifiers associated with the credential itself, or the NFT associated with the credential.
* [Get Verifiable Credential by ID](/api-reference/verifiable-credentials/credentials/retrieve-credential-by-id) uses the format: `urn:uuid:`
* [Get Verifiable Credential by NFT Locator](/api-reference/verifiable-credentials/credentials/retrieve-credential-by-nft-locator) uses the format: `::`
* [Get Verifiable Credential by NFT ID](/api-reference/verifiable-credentials/credentials/retrieve-credential-by-nft) uses crossmint's internal NFT ID
There is no access control on credential retrieval. Credential data is public and can be retrieved by anyone. Choose
to [encrypt your credentials](/minting/verifiable-credentials/integrate/encrypt-credentials) if you need to protect
your data.
Credential retrieval via the crossmint API will not work for [delegated
storage](/minting/verifiable-credentials/integrate/store-credentials).
To retrieve the credential, copy the `retrieveCredential.js` file from below, use your API key, the `credentialId`, and run the file from your terminal.
```javascript retrieveCredential.js theme={null}
const options = {
method: "GET",
headers: {
"X-API-KEY": "YOUR_API_KEY",
},
};
fetch(`https://staging.crossmint.com/api/v1-alpha1/credentials/${credentialId}`, options)
.then((response) => response.json())
.then((response) => console.log(JSON.stringify(response)))
.catch((err) => console.error(err));
```
```json Response theme={null}
{
"unencryptedCredential": {
"id": "urn:uuid:4c9c41af-8410-4265-9fe4-3674d74e80d9",
"credentialSubject": {
"course": "Blockchain 101",
"grade": "A",
"id": "did:polygon-amoy:0xbFB0d0F9d49d80062103199c5aD309CE7a808039"
},
"validUntil": "2034-02-02",
"nft": {
"tokenId": "1",
"chain": "polygon-amoy",
"contractAddress": "0x288AAbb7D72A041Ea10719d2d676B65D2E9689F5"
},
"issuer": { "id": "did:polygon-amoy:0xB658f974Fe3744A6F9344810BC88e021E31B4d3e" },
"type": ["VerifiableCredential", "crossmint:5fe6040e-07a1-48bb-97a3-b588a7e927d2:courseCompletionQuickstart"],
"validFrom": "2024-09-14T13:59:34.918Z",
"@context": ["https://www.w3.org/2018/credentials/v1"],
"proof": {
"verificationMethod": "did:polygon-amoy:0xB658f974Fe3744A6F9344810BC88e021E31B4d3e#evmAddress",
"created": "2024-09-14T13:59:34.918Z",
"proofPurpose": "assertionMethod",
"type": "EthereumEip712Signature2021",
"proofValue": "0x80587c5e9a801f4eed5bca95c48083277fc2ffe2fc1254f230f1b8c11cab59f63afaaa29bf29007129068243feb0069c46a8e583a64e64ff2517a2e3babd18101b",
"eip712": {
"domain": {
"name": "Crossmint",
"version": "0.1",
"chainId": 4,
"verifyingContract": "0xD8393a735e8b7B6E199db9A537cf27C61Aa74954"
},
"types": {
"VerifiableCredential": [
{ "name": "@context", "type": "string[]" },
{ "name": "type", "type": "string[]" },
{ "name": "id", "type": "string" },
{ "name": "issuer", "type": "Issuer" },
{ "name": "credentialSubject", "type": "CredentialSubject" },
{ "name": "validFrom", "type": "string" },
{ "name": "validUntil", "type": "string" },
{ "name": "nft", "type": "Nft" }
],
"CredentialSubject": [
{ "name": "id", "type": "string" },
{ "name": "course", "type": "string" },
{ "name": "grade", "type": "string" }
],
"Issuer": [{ "name": "id", "type": "string" }],
"Nft": [
{ "name": "tokenId", "type": "string" },
{ "name": "contractAddress", "type": "string" },
{ "name": "chain", "type": "string" }
]
},
"primaryType": "VerifiableCredential"
}
}
}
}
```
```shell theme={null}
node retrieveCredential.js
```
## Verifiable Credentials SDK
Crossmint's Verifiable Credentials SDK is primarily meant to be used by verifiers. Using the Verifiable Credentials client-side SDK is advantageous in scenarios where direct interaction with the user's wallet is preferred, directly integrating their credentials with your browser-based application.
* **Wallet Integration**: The Verifiable Credentials SDK is designed to work smoothly with any wallet, providing a streamlined experience for both users and verifiers. This reduces friction and improves the user experience.
* **Efficient Retrieval**: Once the wallet is connected, the SDK can directly interact with it to retrieve credentials. Instead of dealing with multiple API calls, the SDK surfaces credential retrieval capabilities that matter most to verifiers, such as fetching credentials based on specific filter conditions, i.e. type and issuer.
* **User Approvals**: The SDK takes care of prompting the user, when applicable, to approve transactions, such as decryption requests, enhancing security and the user's confidence in the application.
Using the SDK requires a Crossmint developer account and a client-side API key scoped to `credentials.read` and `credentials.decrypt`. Go to the `Developers -> API Keys` tab, click on `New API Key` in the `Client-side keys` section, and select the API key scopes.
You can download the SDK [here](https://www.npmjs.com/package/@crossmint/client-sdk-verifiable-credentials).
The SDKs offer functions to easily retrieve and filter NFTs associated with credentials. For example, the `getCredentialNFTs` function fetches all VC NFTs in a wallet and lets you filter by credential type or issuer.
Below, we show how easy it is to retrieve academic credentials from a user's wallet.
```typescript index.ts theme={null}
import * as sdk from '@crossmint/client-sdk-verifiable-credentials';
sdk.CrossmintAPI.init('YOUR_API_KEY');
const wallet = "USER_WALLET";
const filters = {
// typeId represents an academic credential schema
type:
};
const credentials = await sdk.getCredentialNFTs(
"polygon",
wallet,
filters
);
console.log('Academic credentials retrieved successfully:', credentials);
```
```json Response theme={null}
[
{
"contractAddress": "0x5E1ab2aC27f1a0AD28f1b0DDb0E00baF2677725e",
"chain": "polygon-amoy",
"nfts": [
{
"chain": "polygon-amoy",
"contractAddress": "0x5E1ab2aC27f1a0AD28f1b0DDb0E00baF2677725e",
"tokenId": "1"
}
],
"metadata": {
"name": "Crossmint Tickets",
"description": "Access Crossmint Events",
"image": "ipfs://Qme551yKU6a5Cptsk8hxcMtWwTrKkHArZtTTwFSvu19eFD",
"credentialMetadata": {
"type": [
"VerifiableCredential",
"userName"
],
"encryption": {
"type": "crossmint-recoverable"
},
"credentialsEndpoint": "ipfs://bafybeidmfu6xmdpxddads6nsyxfg6hd5ylj2drrywhzvfi2bbq45owklsa",
"issuerDid": "did:polygon:0xd9d8BA9D5956f78E02F4506940f42ac2dAB9DABd"
}
}
},
{
"contractAddress": "0x38989746BF435d5f6Dc7C8DdE6038b5a17E62cE2",
"chain": "polygon-amoy",
"nfts": [
{
"chain": "polygon-amoy",
"contractAddress": "0x38989746BF435d5f6Dc7C8DdE6038b5a17E62cE2",
"tokenId": "1"
}
],
"metadata": {
"name": "Crossmint Tickets",
"description": "Access Crossmint Events",
"image": "ipfs://Qme551yKU6a5Cptsk8hxcMtWwTrKkHArZtTTwFSvu19eFD",
"credentialMetadata": {
"type": [
"VerifiableCredential",
"userName"
],
"encryption": {
"type": "crossmint-recoverable"
},
"credentialsEndpoint": "a",
"issuerDid": "did:polygon:0xd9d8BA9D5956f78E02F4506940f42ac2dAB9DABd"
}
}
}
]
```
Now that the credential is retrieved, it's important to decrypt it and verify it.
In case the endpoint is called by the issuer the credential will be automatically decrypted.
Test any API in seconds directly from the docs.
Contact our sales team for support.
# Revoke Credentials
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/revoke-credentials
Invalidate a credential, rendering it no longer trustworthy or verifiable
To revoke a credential, you can directly use the [revoke credential API](/api-reference/verifiable-credentials/credentials/revoke-credential). Burning the NFT associated with the credential via the [burn-nft](/api-reference/minting/nfts/burn-nft) API accomplishes the same result.
Issuers can revoke a Verifiable Credential without the subject's intermediation.
To revoke the credential, copy the `revokeCredential.js` file from below, use your API key, the credentialId, and run the file from your terminal.
```javascript revokeCredential.js theme={null}
const options = {
method: "DELETE",
headers: {
"X-API-KEY": "YOUR_API_KEY",
},
};
fetch(`https://staging.crossmint.com/api/v1-alpha1/credentials/${credentialId}`, options)
.then((response) => response.json())
.then((response) => console.log(JSON.stringify(response)))
.catch((err) => console.error(err));
```
```shell theme={null}
node revokeCredential.js
```
Test any API in seconds directly from the docs.
Contact our sales team for support.
# Credential Storage
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/store-credentials
Choose how to store credential data
When creating a [template](/minting/verifiable-credentials/integrate/create-credential-templates), you can specify a `storage` parameter to determine where the credential payload will be stored. The possible values for this parameter are:
* `crossmint`
* `crossmint-private`
* `delegated`
* `decentralized-ipfs`
## Crossmint
With this option, credentials are stored on the Crossmint database. This is the most flexible solution and is compatible with all other features.
There is not access control on credential retrieval, credential data is public and can be retrieved by anyone, use
[encrypted credentials](/minting/verifiable-credentials/integrate/encrypt-credentials) if you need to protect the
data.
## Crossmint Private
This option is designed for integrated systems in which the issuer is also the verifier. Credentials are stored by Crossmint, but only the issuer can retrieve the credential payload. Because the credential issuer is also the credential verifier, there is no need to encrypt the credentials.
## Decentralized Storage
This option is the most open and transparent solution as it will store the credential to a decentralized storage (IPFS). All retrieval API endpoints will still be available and will proxy the request to ipfs.
[Encrypting the payload](/minting/verifiable-credentials/integrate/encrypt-credentials) is suggested to avoid private credential data being exposed to be public.
Credential issuance will be slower than other storage options.
## Delegated Storage
This option is designed for the enterprise, giving the issuer full control over the credentials. With delegated storage, no reference to the data is stored on Crossmint databases. The credential is returned to the issuer via the [webhook](/minting/verifiable-credentials/integrate/webhooks) and the issuer is responsible for storing it.
All Crossmint retrieval endpoints will consequently be disabled. A `delegatedStorageEndpoint` parameter must be added when creating the credential template that specifies the endpoint where the credential can be retrieved (can be set to a dummy value like "unknown" if not desired).
This endpoint will be saved in the contract metadata and can be used by users to retrieve the credential associated with their NFT. Issuers can protect content privacy by encrypting the payload.
For delegated storage, if the credential is not catched by the webhook, it will be lost. Read more about webhooks
[here](/minting/verifiable-credentials/integrate/webhooks).
# Verify Credentials
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/verify-credentials
Ensure that the credential is valid
Verification is a crucial step in the process of handling Verifiable Credentials. To ensure a credential is valid, the following checks are performed:
* Credential Structure: The credential should not be malformed. It must adhere to the correct structure as defined by the [credential type](/minting/verifiable-credentials/integrate/create-credential-types) .
* Signature Validation: The credential’s signature must be valid and match the identity of the issuer. This ensures that the credential was indeed issued by the claimed issuer and hasn’t been tampered with.
* Attribute Check: No additional attributes should have been added to the credential beyond what’s defined in its schema. This prevents unauthorized modifications to the credential.
* Expiration Check: The credential should not have expired. Credentials often have a validity period after which they are no longer considered valid.
* Revocation Check: The credential should not have been revoked by the issuer. Even if a credential is valid and hasn’t expired, it may have been revoked by the issuer for various reasons.
By performing these checks, we can ensure the authenticity and validity of a Verifiable Credential. Verifying a credential can happen either via the Crossmint API or SDK.
## API
To verify a credential on the server-side use the Crossmint API and the following endpoint, passing the [retrieved](/minting/verifiable-credentials/integrate/retrieve-credentials) unencrypted credential JSON.
```javascript verifyCredential.js theme={null}
const options = {
method: "POST",
headers: {
"X-API-KEY": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: `{"credential": ${JSON.stringify(credential.unencryptedCredential)}}`,
};
fetch("https://staging.crossmint.com/api/v1-alpha1/credentials/verification/verify", options)
.then((response) => response.json())
.then((response) => console.log(JSON.stringify(response)))
.catch((err) => console.error(err));
```
```json Response theme={null}
{
"isValid": true
}
```
Verifying a credential via the API will require a Crossmint developer account.
## SDK
With the Verifiable Credentials client-side SDK, the verification process is performed locally, eliminating the need for interaction with Crossmint. Consequently, there is no need for a Crossmint developer account to verify the credential.
Install the `@crossmint/client-sdk-verifiable-credentials` [npm package](https://www.npmjs.com/package/@crossmint/client-sdk-verifiable-credentials) and call the `verifyCredential` function passing a `VerifiableCredential` object, as shown below.
```typescript decrypt.ts theme={null}
import * as sdk from '@crossmint/client-sdk-verifiable-credentials';
sdk.CrossmintAPI.init('YOUR_API_KEY');
const decryptedCredential = ;
const verificationResult = await sdk.verifyCredential(decryptedCredential);
console.log('Verification result:', verificationResult);
```
```json Response theme={null}
{
"isValid": true
}
```
Verifying the status of the NFT related to the credential requires connection to the internet in order to access
blockchain state.
For details on the SDK functionality, review our [SDK reference documentation](/sdk-reference/credentials/introduction).
## Independent Verification
Since Crossmint’s Verifiable Credentials are based on the [W3C standard](https://www.w3.org/TR/vc-data-model-2.0/), you can use any library that supports this standard to verify a credential.
The W3C Verifiable Credentials standard ensures interoperability across different platforms and systems, enhancing the reliability and security of the credential verification process.
Crossmint verification is stricter and performs additional checks that are not part of the W3C standard. For example, that no additional fields have been added to the credential.
Test any API in seconds directly from the docs.
Contact our sales team for support.
# Webhook
Source: https://docs.crossmint.com/minting/verifiable-credentials/integrate/webhooks
Receive updates about your credentials
Webhooks are a critical component in the management and distribution of verifiable credentials, providing real-time notifications and seamless integration for issuers. Here’s why webhooks enhance the credential issuance process:
* **Automatic Credential Retrieval**: Upon issuance, the credential is sent directly to the issuer through the webhook payload. This eliminates the need for issuers to manually call `getCredential()`. This functionality is especially crucial when the issuer opts for delegated storage; without catching the webhook, the credential could be lost, as there would be no subsequent retrieval.
* **Notification of Credential Issuance Failure**: Webhooks provide immediate alerts if the credential issuance process fails. This enables issuers to quickly identify and address issues, ensuring a reliable and efficient credential issuance process.
* **Secure Storage of Encryption Keys**: In scenarios involving encryption via the "crossmint-recoverable" encryption type, the webhook stores the encryption key for the recipient subject. This ensures that, in case of any fallback, the key is readily available, maintaining the security and accessibility of the encrypted credentials.
* **Immediate Access to Credential Pass Links**: Issuers can instantly receive a link to the wallet credential pass via the webhook. This link can then be forwarded to the subject, ensuring a swift and efficient delivery of credentials.
If you choose not to use a webhook, you can query for the credential's status via the [status API](https://docs.crossmint.com/api-reference/minting/nfts/mint-status).
## Setting Up a Webhook
First, you need to [setup a webhook](https://docs.crossmint.com/docs/mintapi-webhooks) in the developer console. Once that is done either programmatically or via the Dev Console, the webhook payload received, looks as follows:
```json theme={null}
data:{
credential: credentialObject or encryptedCredentialObjcet
pass: wallet pass object
secret: encryption key
}
```
The events you can monitor are:
* `credential.creation.succeeded`
Will be emitted when the credential creation process is complete.
The issued credential, credential pass, and encryption key will be contained in the `data.credential` field, `data.pass` field, and `data.secret` field, respectively.
In case of encrypted credential templates, the issued credential returned will be encrypted.
* `credential.creation.failed`
Will be emitted when the credential creation process fails. The error will be contained in the `data.error` field.
# Create Order
Source: https://docs.crossmint.com/offramp/api-reference/create-order
api-reference/_open-api/OfframpAPIs.json POST /2022-06-09/orders
Creates an offramp order to cash out stablecoins to fiat. The order returns on-chain deposit instructions that the payer wallet broadcasts to fund the fiat payout.
# Get Order
Source: https://docs.crossmint.com/offramp/api-reference/get-order
api-reference/_open-api/OfframpAPIs.json GET /2022-06-09/orders/{orderId}
Retrieves an offramp order by its ID to track its progress from the on-chain deposit through to the fiat payout.
# List Orders
Source: https://docs.crossmint.com/offramp/api-reference/list-orders
api-reference/_open-api/OfframpAPIs.json GET /2022-06-09/orders
Retrieve a paginated list of offramp orders for your project to track and reconcile payouts.
# Create Payment Method
Source: https://docs.crossmint.com/offramp/api-reference/payment-methods/create-payment-method
api-reference/_open-api/OfframpAPIs.json POST /unstable/payment-methods
Save a bank account payout method so offramp orders can settle fiat to it.
Saves a bank account payout method for a user so offramp orders can settle to it.
Account numbers and IBANs are tokenized by the PCI-compliant vault, so create requests must be submitted through `vault.crossmint.com`.
## Returns
Returns a [PaymentMethod](/offramp/api-reference/payment-methods/payment-method-object) object.
# Delete a Payment Method
Source: https://docs.crossmint.com/offramp/api-reference/payment-methods/delete-payment-method
api-reference/_open-api/OfframpAPIs.json DELETE /unstable/payment-methods/{paymentMethodId}
Soft-delete a payout method. The record is retained for compliance but can no longer be used.
Deletes a payout method. The record is retained internally but can no longer be used for payouts.
## Returns
Returns the [PaymentMethod](/offramp/api-reference/payment-methods/payment-method-object) object.
# Retrieve a Payment Method
Source: https://docs.crossmint.com/offramp/api-reference/payment-methods/get-payment-method
api-reference/_open-api/OfframpAPIs.json GET /unstable/payment-methods/{paymentMethodId}
Fetch a single bank account payout method by its ID.
Retrieves a bank account payout method by its ID.
## Returns
Returns a [PaymentMethod](/offramp/api-reference/payment-methods/payment-method-object) object.
# List Payment Methods by User
Source: https://docs.crossmint.com/offramp/api-reference/payment-methods/list-payment-methods
api-reference/_open-api/OfframpAPIs.json GET /unstable/payment-methods
Retrieve the bank account payout methods saved for a given user.
Returns a paginated list of the bank account payout methods saved for a given user.
## Returns
An object with a `data` array of [PaymentMethod](/offramp/api-reference/payment-methods/payment-method-object) objects. Use the `cursor` parameter to paginate; when `nextCursor` is present, more pages are available.
# The Payment Method Object
Source: https://docs.crossmint.com/offramp/api-reference/payment-methods/payment-method-object
Attributes and structure of a saved bank account payout method
PaymentMethod objects represent the bank account payout methods that offramp orders settle fiat to. Save one with the [Create Payment Method](/offramp/api-reference/payment-methods/create-payment-method) endpoint, then reference its `paymentMethodId` as the recipient when creating an offramp order.
## Type Values
`type` names the **account identifier** (the capture schema Crossmint collects and validates) — never the payment rail, scheme, or clearing house. The same saved account can be paid over multiple rails; rail selection happens at payout execution time.
| Value | Description |
| - | - |
| `bank-account-us` | US bank account (routing number + account number). |
| `bank-account-sepa-iban` | Bank account identified by an IBAN within the supported SEPA scope. |
| `bank-account-co` | Colombian bank account (institution ID + account number + national document). |
| `bre-b` | Colombian Bre-B key: an `@handle`, a mobile number, an e-mail, an identity document, or a commerce code. |
| `bank-account-id` | Indonesian bank account (institution ID + account number), paid out over BI-FAST. |
| `bank-account-mx-clabe` | Mexican bank account, identified by an 18-digit CLABE (Clave Bancaria Estandarizada). |
| `bank-account-ng` | Nigerian bank account (institution ID + 10-digit NUBAN, the Nigeria Uniform Bank Account Number), paid out over NIP. |
| `bank-account-ph` | Philippine bank account (institution ID + account number), paid out over InstaPay or PESONet. |
| `bank-account-th` | Thai bank account (institution ID + account number), paid out over PromptPay. |
| `bank-account-vn` | Vietnamese bank account (institution ID + account number), paid out over Napas 247 or CITAD. |
## Attributes
Unique identifier for the payout method.
Whether this is the user's default payout method. Returned for `bank-account-sepa-iban` only.
Server-derived label, returned for `bank-account-sepa-iban` only (e.g. `"SEPA Account ••6789"`). Absent on every other payout method; read `label` there.
Payout method type. One of `bank-account-us`, `bank-account-sepa-iban`, `bank-account-co`, `bre-b`, `bank-account-id`, `bank-account-mx-clabe`, `bank-account-ng`, `bank-account-ph`, `bank-account-th`, or `bank-account-vn`.
Bank accounts and Bre-B keys only. Readiness of the destination, not user approval, ownership, or a guaranteed payout. One of:
* `pending`: initial readiness checks are running.
* `active`: required readiness checks passed. Orders still run their own eligibility checks.
* `rejected`: the destination cannot receive a supported payout. The initial checks ended without a usable record, a provider refused the account, or the bank returned a payout to it. Retrying requires a new payment method.
* `deleted`: removed by the client. Never reactivates.
`pending` moves to `active`, `rejected`, or `deleted`; `active` moves to `rejected` or `deleted`; `rejected` moves only to `deleted`. Creation starts the readiness checks once; reads, label edits, and orders never restart them. Treat an unrecognized status as not ready for orders. See [Manage Payment Methods](/offramp/guides/collect-bank-accounts#statuses).
Always `null` except on a `rejected` method, where it may be `destination-not-found`, `destination-closed`, `destination-cannot-receive`, or `no-rail-available`. `null` when no safe destination condition was established. Never carries provider errors or compliance detail. Codes are extensible, so tolerate unknown values.
Bank account details. Absent when `type` is `bre-b`, which returns `breB` instead.
The person or company that owns the account. Always the value the client sent, never the bank's record of the holder. Derived from `billing` when the record predates this field.
Full legal name.
E-mail address.
Phone number (E.164 format).
Holder address, without a country: the account's own `country` is the bank's jurisdiction. Present only on types that collect one. Never on `bank-account-mx-clabe`.
Name of the bank. `null` if not resolvable. Absent for `bank-account-co`, `bank-account-id`, `bank-account-mx-clabe`, `bank-account-ng`, `bank-account-ph`, `bank-account-th`, and `bank-account-vn`.
Last four digits of the account number, CLABE, or IBAN.
ISO 4217 currency code (e.g. `usd`, `mxn`, `eur`, `cop`).
ISO 3166-1 alpha-2 country code.
Account holder type. `individual` or `business`. Required on creation. May be absent on records created before the field was enforced.
Bank branch address.
Nine-digit ABA routing number.
`checking` or `savings`.
BIC/SWIFT code (8 or 11 characters).
Identifier of the bank that holds the account, for example `co-bancolombia`. See [Bank Institution IDs](/offramp/guides/bank-institution-ids).
`checking`, `savings`, or `electronic-deposit`.
The holder's national document. The full number is never returned.
One of `cc`, `nit`, `ce`, `pa`, `ppt`, `ti`, `rc`, `te`, or `die`.
Last four digits of the document number.
Identifier of the bank that holds the account, for example `id-bca`. See [Bank Institution IDs](/offramp/guides/bank-institution-ids).
`checking` or `savings`. Present when sent on creation.
Bre-B key details. Present when `type` is `bre-b`, in place of `bankAccount`. The key itself is never returned.
The person or company that owns the key. Always the value the client sent.
Full legal name.
E-mail address.
Phone number (E.164 format).
Masked form of the key. An e-mail key is masked in place. Every other key shows its last four characters.
Always `cop`. Bre-B only settles in Colombian pesos.
Always `CO`.
Key holder type. `individual` or `business`.
Category of the key, derived from its format. One of `alphanumeric`, `phone`, `email`, `identification`, or `commerce`. A client-supplied value is refused.
Display name of the institution that holds the key. Absent until a name source answers.
Read-only. The payout rails a currently-available provider advertises for this key. Absent when no provider serves the key.
Rail values this key supports. Bre-B carries `supported` alone, without the `status` and `pending` fields an ordinary bank account returns.
The client's own name for this payout method, when one was sent on creation. At most 255 characters.
Read-only. ISO 8601 timestamp of the most recent successful offramp payout funded by this account. Absent until the first payout.
```json US Bank Account theme={null}
{
"paymentMethodId": "f2ecef7e-91af-4fa7-93f9-12db856c227f",
"type": "bank-account-us",
"bankAccount": {
"bankName": "Chase",
"accountSuffix": "7899",
"currency": "usd",
"country": "US",
"entityType": "individual",
"bankAddress": {
"line1": "123 Main St",
"city": "New York",
"stateOrRegion": "NY",
"postalCode": "10001",
"country": "US"
},
"routingNumber": "071000013",
"accountType": "checking"
}
}
```
```json SEPA theme={null}
{
"paymentMethodId": "3323460b-d9d2-4ec6-b380-8a3e01e3f226",
"default": false,
"displayName": "SEPA Account ••6789",
"type": "bank-account-sepa-iban",
"bankAccount": {
"bankName": null,
"accountSuffix": "6789",
"currency": "eur",
"country": "DE",
"entityType": "individual",
"bic": "COBADEFFXXX"
}
}
```
```json CO Bank Account theme={null}
{
"paymentMethodId": "1cad2281-bd3d-4219-8787-7dfea94c60b1",
"type": "bank-account-co",
"bankAccount": {
"accountSuffix": "7320",
"institutionId": "co-bancolombia",
"accountType": "checking",
"currency": "cop",
"country": "CO",
"entityType": "individual",
"accountHolder": { "name": "Camila Restrepo", "document": { "type": "cc", "suffix": "4050" } },
"rails": { "status": "pending", "supported": [], "pending": ["ach-credit"] }
}
}
```
```json CO Bre-B key theme={null}
{
"paymentMethodId": "9f1a6b2c-3d4e-4f50-9a61-2b3c4d5e6f70",
"type": "bre-b",
"status": "active",
"statusReason": null,
"breB": {
"accountHolder": {
"name": "Carlos Gómez",
"phoneNumber": "+573001234567"
},
"accountSuffix": "CDKS",
"currency": "cop",
"country": "CO",
"entityType": "individual",
"keyType": "alphanumeric"
}
}
```
```json ID Bank Account theme={null}
{
"paymentMethodId": "7a3e5c91-2b4d-4f86-a1c0-9d8e7f6a5b42",
"type": "bank-account-id",
"bankAccount": {
"accountSuffix": "7890",
"institutionId": "id-bca",
"currency": "idr",
"country": "ID",
"entityType": "individual",
"accountHolder": { "name": "Budi Santoso" },
"rails": { "status": "complete", "supported": ["bi-fast"], "pending": [] }
}
}
```
```json MX Bank Account theme={null}
{
"paymentMethodId": "27bacbe1-d0ff-4bc3-b37a-ca91cc64fa39",
"type": "bank-account-mx-clabe",
"bankAccount": {
"accountHolder": {
"name": "Luis Gomez Rios",
"phoneNumber": "+525512345678"
},
"accountSuffix": "7899",
"currency": "mxn",
"country": "MX",
"entityType": "individual"
}
}
```
```json NG Bank Account theme={null}
{
"paymentMethodId": "2e9b4d17-6a3c-4f85-b1d0-8c7e5a2f9b63",
"type": "bank-account-ng",
"bankAccount": {
"accountSuffix": "7897",
"institutionId": "ng-gt-bank",
"currency": "ngn",
"country": "NG",
"entityType": "individual",
"accountHolder": { "name": "Chinedu Okafor" },
"rails": { "status": "complete", "supported": ["nip"], "pending": [] }
}
}
```
```json PH Bank Account theme={null}
{
"paymentMethodId": "5e2b7c41-9d0a-4f3b-8c6d-2a1b3c4d5e6f",
"type": "bank-account-ph",
"bankAccount": {
"accountSuffix": "7890",
"institutionId": "ph-bdo",
"currency": "php",
"country": "PH",
"entityType": "individual",
"accountHolder": { "name": "Maria Santos" },
"rails": { "status": "complete", "supported": ["instapay", "pesonet"], "pending": [] }
}
}
```
```json TH Bank Account theme={null}
{
"paymentMethodId": "8a4c7e39-2f1b-4d96-b3e5-7c0d2a9f6b41",
"type": "bank-account-th",
"bankAccount": {
"accountSuffix": "7890",
"institutionId": "th-kasikorn-bank",
"currency": "thb",
"country": "TH",
"entityType": "individual",
"accountHolder": { "name": "Somchai Jaidee" },
"rails": { "status": "complete", "supported": ["promptpay"], "pending": [] }
}
}
```
```json VN Bank Account theme={null}
{
"paymentMethodId": "3f7d1b86-9e2a-4c50-8d34-1a6e9b7c5f02",
"type": "bank-account-vn",
"bankAccount": {
"accountSuffix": "4567",
"institutionId": "vn-acb",
"currency": "vnd",
"country": "VN",
"entityType": "individual",
"accountHolder": { "name": "Nguyễn Văn An" },
"rails": { "status": "complete", "supported": ["napas-247", "citad"], "pending": [] }
}
}
```
# Update a Payment Method
Source: https://docs.crossmint.com/offramp/api-reference/payment-methods/update-payment-method
api-reference/_open-api/OfframpAPIs.json PATCH /unstable/payment-methods/{paymentMethodId}
Update the default status of an existing payout method.
Updates a payout method's default status. Only `default` can be changed.
## Returns
Returns the updated [PaymentMethod](/offramp/api-reference/payment-methods/payment-method-object) object.
# Submit Deposit
Source: https://docs.crossmint.com/offramp/api-reference/process-payment
api-reference/_open-api/OfframpAPIs.json POST /2022-06-09/orders/{orderId}/payment
Associates the broadcast on-chain stablecoin deposit with an offramp order by its transaction ID. This step is optional because Crossmint matches the deposit automatically by the order memo.
# How It Works
Source: https://docs.crossmint.com/offramp/concepts/how-it-works
How your users convert USDC into fiat and withdraw to their bank account
Crossmint Offramp lets your users convert USDC into fiat and withdraw it to their own bank account without leaving your app. You integrate once, and Crossmint handles compliance, conversion, and the bank payout. This page explains how the flow works end to end before you build it.
## The Offramp Flow
Each user completes identity verification once. Verification runs asynchronously, so you trigger it and then poll until the user is verified. If your users are already KYC'd, you can bring them over by passing their identity data, so they skip the in-flow verification. See [KYC & Compliance](/offramp/concepts/kyc-and-compliance).
The user's bank details are saved as a reusable payment method. Sensitive fields are tokenized at capture, so raw account numbers never touch your servers or Crossmint's.
Your backend creates an order specifying the amount of USDC to cash out and the bank account to pay. The order comes back with a prepared deposit transaction that already carries a memo tying the deposit to this order.
Your app broadcasts the prepared transaction from the user's wallet. Crossmint matches the on-chain deposit to the order by its memo, so no extra confirmation call is needed.
Crossmint converts the USDC and pays out fiat to the user's bank account over local rails.
The order completes and the funds settle in the user's bank. You can track status by polling the order or via webhooks.
## Key Concepts
### Exact-In Amounts
You specify the **amount of USDC the user sends** (`exact-in`). The user sends exactly that amount of crypto, and the equivalent fiat, minus any applicable fees, is deposited into their bank account.
### First-Party Payouts
The destination bank account must belong to the same user who owns the wallet and completed verification. The wallet the user pays from is linked to that verified identity, and the order resolves KYC through it. Offramp pays users out to **their own** bank accounts, never to third parties.
### Settlement Timing
Once the USDC deposit is confirmed, fiat arrival depends on the payout rail, ranging from minutes to a few business days. Crossmint uses the account's default rail unless you pin one with `recipient.rail` when creating the order. See [Payment Schemes](/offramp/payment-methods) for the current rail catalog and settlement times.
In the staging environment, payouts are **simulated**, so you can run the full flow end to end without funds moving. Funds only move in production.
## What Crossmint Handles vs. What You Build
| Crossmint handles | You build |
| :- | :- |
| Identity screening and compliance | The in-app cash-out experience |
| Converting USDC to fiat | Collecting the user's bank account |
| Paying out over local bank rails | Creating orders and tracking status |
| Regulatory and licensing coverage | (Optional) passing KYC data you already hold |
## Next Steps
Build your first offramp end to end
How verification works for offramp
# KYC & Compliance
Source: https://docs.crossmint.com/offramp/concepts/kyc-and-compliance
How identity verification and compliance work for Crossmint Offramp
Every end user must pass KYC before they can withdraw funds through the Offramp. You can collect this verification with a Crossmint-hosted flow, or pass data you have already verified yourself. This page covers the onboarding models, the KYC tiers, and where to find the exact data requirements.
## Choose Your Onboarding Model
| Model | Best for | How it works |
| :- | :- | :- |
| **Import KYC data** | Platforms that already verify users | You pass user data to Crossmint via API (Sumsub token-sharing coming soon) |
| **Crossmint-hosted** | Teams that want the fastest integration | The user completes KYC inside secure embedded components |
## How Verification Works
Verification runs asynchronously. You register the user, trigger verification, then poll until their `offramp` eligibility reads `verified`. Only verified users can save a payment method or create an order. The [Identity quickstart](/identity/quickstart) walks through the full sequence (accept the privacy policy, register the user, trigger verification, upload any required documents, and poll status), and its response reports eligibility per product, including `offramp` (full KYC) and `offramp-light` (progressive KYC). These endpoints use a `userId:` locator, a unique identifier you choose for the user.
## KYC Tiers
Crossmint applies KYC tiers based on cumulative transaction volume, transaction frequency, and risk profile — the same model used for [Onramp](/onramp/introduction/user-onboarding):
| Tier | Applies When | Data Required |
| :- | :- | :- |
| **Progressive (light) KYC** | Up to \$1,000 in total volume in the last 12 months | Name, date of birth, nationality, country, address, email, and phone number (phone required for US residents). An ID number or SSN is required for US (SSN) and EU/EEA residents; optional elsewhere. No document uploads. |
| **Full KYC** | Over \$1,000 in total volume in the last 12 months, or a high number of transactions | Progressive KYC data plus `dueDiligence`, `verificationHistory`, identity document, and selfie. See the [data requirements](/identity/data-requirements) page. |
Light KYC is available to non-US residents; US residents always complete Full KYC. Identity verification eligibility reports both tiers per user: `offramp-light` (progressive) and `offramp` (full).Independently of the tier, Crossmint's compliance team may request a specific document from a user at any time — proof of address, source of wealth, or an identity document. See [additional document requests](/identity/quickstart#additional-document-requests).
## Guides
# Manage Payment Methods
Source: https://docs.crossmint.com/offramp/guides/collect-bank-accounts
Save and manage your users' payout destinations as reusable payment methods
Before creating an offramp order, [save the destination bank account as a payment method](/offramp/api-reference/payment-methods/create-payment-method). Saved payment methods can be reused across Crossmint products, so users only need to enter their bank details once.
## Save a Payment Method
The request body depends on the payment method `type`. See [The Payment Method Object](/offramp/api-reference/payment-methods/payment-method-object) for the full list of types and fields.
The examples below use the staging URL, `https://vault.staging.crossmint.com`. In production, use `https://vault.crossmint.com`.
```bash cURL theme={null}
curl -X POST 'https://vault.staging.crossmint.com/api/unstable/payment-methods' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"type": "bank-account-us",
"userLocator": "",
"label": "Wells Fargo Checking",
"bankAccount": {
"accountNumber": "",
"routingNumber": "091300010",
"accountType": "checking",
"entityType": "individual",
"accountHolder": {
"name": "Emily Carter"
}
}
}'
```
```bash cURL theme={null}
curl -X POST 'https://vault.staging.crossmint.com/api/unstable/payment-methods' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"type": "bank-account-sepa-iban",
"userLocator": "",
"label": "My Wise EUR",
"bankAccount": {
"iban": "BE05100052384175",
"bic": "TRWIBEB1XXX",
"entityType": "individual",
"accountHolder": {
"name": "Lukas Meier"
}
}
}'
```
```bash cURL theme={null}
curl -X POST 'https://vault.staging.crossmint.com/api/unstable/payment-methods' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"type": "bank-account-co",
"userLocator": "",
"label": "Mi Bancolombia",
"bankAccount": {
"accountNumber": "2048817320",
"institutionId": "co-bancolombia",
"accountType": "checking",
"entityType": "individual",
"accountHolder": {
"name": "Camila Restrepo",
"document": { "type": "cc", "number": "1020304050" }
}
}
}'
```
```bash cURL theme={null}
curl -X POST 'https://vault.staging.crossmint.com/api/unstable/payment-methods' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"type": "bre-b",
"userLocator": "",
"label": "Nequi Ahorros",
"breB": {
"key": "@MQ7RK2PLTA",
"entityType": "individual",
"accountHolder": {
"name": "Santiago Vargas"
}
}
}'
```
```bash cURL theme={null}
curl -X POST 'https://vault.staging.crossmint.com/api/unstable/payment-methods' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"type": "bank-account-id",
"userLocator": "",
"label": "My BCA account",
"bankAccount": {
"accountNumber": "1234567890",
"institutionId": "id-bca",
"entityType": "individual",
"accountHolder": {
"name": "Budi Santoso"
}
}
}'
```
```bash cURL theme={null}
curl -X POST 'https://vault.staging.crossmint.com/api/unstable/payment-methods' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"type": "bank-account-mx-clabe",
"userLocator": "",
"label": "Mi cuenta Nu",
"bankAccount": {
"clabe": "646180527394108255",
"entityType": "individual",
"accountHolder": {
"name": "Andrea Fuentes Lopez"
}
}
}'
```
The response returns the saved payment method, including its `paymentMethodId` and a masked `accountSuffix`. Account numbers, IBANs, CLABEs, and Bre-B keys never appear in full. Pass the `paymentMethodId` as the `recipient` when you [create an order](/offramp/quickstarts/rest-api). The examples show a payment method after its checks complete. A create response has `status: pending`.
```json theme={null}
{
"type": "bank-account-us",
"paymentMethodId": "57d99e28-8fad-4158-b825-75a693e44b10",
"status": "active",
"statusReason": null,
"label": "Wells Fargo Checking",
"bankAccount": {
"accountSuffix": "",
"routingNumber": "091300010",
"accountType": "checking",
"currency": "usd",
"country": "US",
"entityType": "individual",
"accountHolder": {
"name": "Emily Carter"
},
"rails": {
"supported": [
"ach-credit",
"same-day-ach-credit"
],
"pending": [
"rtp-credit"
]
}
},
"createdAt": "2026-08-20T19:10:00.000Z",
"updatedAt": "2026-08-20T19:10:00.000Z"
}
```
The full account number is never returned. The response carries `bankAccount.accountSuffix`, the last four digits, while `routingNumber` comes back in full because it is not secret. A create that passes the structural checks, such as the ABA routing check digit, returns `status: "active"` straight away. This does not establish that the account exists or who owns it; those checks run when you create an order. The available rails depend on the addresses you supply: `fedwire` appears in `rails.supported` only when both `accountHolder.address` and `bankAddress` are provided. `rtp-credit` starts in `rails.pending` because not every US bank participates in the RTP network, and it moves to `rails.supported` once the receiving bank's participation is confirmed. `institutionName` is never populated for US accounts.
```json theme={null}
{
"type": "bank-account-sepa-iban",
"paymentMethodId": "19ee530a-80b3-401c-9a53-d6886c22fdd4",
"status": "active",
"reason": null,
"default": false,
"displayName": "Wise ••4175",
"display": {
"label": "Wise ••4175"
},
"lastPayoutAt": "2026-09-15T22:00:51.733Z",
"bankAccount": {
"accountHolder": {
"name": "Lukas Meier"
},
"bankName": "Wise",
"accountSuffix": "4175",
"currency": "eur",
"country": "BE",
"entityType": "individual",
"bic": "TRWIBEB1XXX",
"rails": {
"status": "complete",
"supported": [
"sepa"
],
"pending": []
}
},
"createdAt": "2026-09-15T19:04:06.788Z",
"updatedAt": "2026-09-15T22:00:55.633Z"
}
```
The full IBAN is never returned. The response carries `bankAccount.accountSuffix`, the last four characters. SEPA validation is structural only (the IBAN mod-97 check), so a create that passes it returns `status: "active"` straight away. This does not establish that the account exists or who owns it. `currency` and `country` are read-only and derived from the IBAN. `bankName` is display-only and provider-sourced, so it stays absent when unknown.
```json theme={null}
{
"type": "bank-account-co",
"paymentMethodId": "6b489a5d-3867-4ec9-bb76-072a89e1319d",
"status": "pending",
"statusReason": null,
"label": "Mi Bancolombia",
"bankAccount": {
"accountSuffix": "7320",
"institutionId": "co-bancolombia",
"accountType": "checking",
"currency": "cop",
"country": "CO",
"entityType": "individual",
"accountHolder": {
"name": "Camila Restrepo",
"document": { "type": "cc", "suffix": "4050" }
},
"rails": { "status": "pending", "supported": [], "pending": ["ach-credit"] }
},
"createdAt": "2026-08-21T22:10:00.000Z",
"updatedAt": "2026-08-21T22:12:11.000Z"
}
```
The full account and document numbers are never returned. The response carries `bankAccount.accountSuffix` and `accountHolder.document.suffix`, the last four digits of each. Crossmint rejects an `institutionId` that is not on the [Bank Institution IDs](/offramp/guides/bank-institution-ids) page with `422`. `accountType` is `checking`, `savings`, or `electronic-deposit`, and `accountHolder.document.type` is one of `cc`, `nit`, `ce`, `pa`, `ppt`, `ti`, `rc`, `te`, or `die`. `accountHolder.name` is always echoed back exactly as sent, and ownership is evaluated when you create an order, not on the payment method.
```json theme={null}
{
"type": "bre-b",
"paymentMethodId": "7fdf72b9-f627-4b89-b7d7-5795893aaf12",
"status": "active",
"statusReason": null,
"label": "Nequi Ahorros",
"breB": {
"accountHolder": {
"name": "Santiago Vargas"
},
"accountSuffix": "PLTA",
"currency": "cop",
"country": "CO",
"entityType": "individual",
"keyType": "alphanumeric",
"rails": {
"supported": [
"bre-b"
]
}
},
"createdAt": "2026-09-17T14:27:20.725Z",
"updatedAt": "2026-09-17T14:27:21.410Z"
}
```
The key itself is never returned. The response carries `breB.accountSuffix` instead: the last four characters of the key's distinguishing part, which for `alphanumeric` keys is everything after the leading `@` and for `email` keys is the local part before the `@`. `keyType` is derived from the key format. `institutionName` is optional display metadata from a reliable bank directory and is omitted when unknown.
```json theme={null}
{
"type": "bank-account-id",
"paymentMethodId": "7a3e5c91-2b4d-4f86-a1c0-9d8e7f6a5b42",
"status": "active",
"statusReason": null,
"label": "My BCA account",
"bankAccount": {
"accountSuffix": "7890",
"institutionId": "id-bca",
"currency": "idr",
"country": "ID",
"entityType": "individual",
"accountHolder": {
"name": "Budi Santoso"
},
"rails": { "status": "complete", "supported": ["bi-fast"], "pending": [] }
},
"createdAt": "2026-09-23T09:00:00.000Z",
"updatedAt": "2026-09-23T09:00:12.000Z"
}
```
The five types take the same body. Change `type`, and take the `institutionId` from the country's section of the [Bank Institution IDs](/offramp/guides/bank-institution-ids) page. Crossmint rejects an account number that does not match its type with `422`:
| `type` | `accountNumber` | `currency` | Rails |
| - | - | - | - |
| `bank-account-id` | 7 to 18 digits | `idr` | `bi-fast` |
| `bank-account-ng` | 10 digits (a NUBAN, the Nigeria Uniform Bank Account Number); the last is the check digit for the bank | `ngn` | `nip` |
| `bank-account-ph` | 6 to 18 digits | `php` | `instapay`, `pesonet` |
| `bank-account-th` | 7 to 12 digits | `thb` | `promptpay` |
| `bank-account-vn` | 1 to 20 letters or digits | `vnd` | `napas-247`, `citad` |
The request carries no `currency` or `country`: the type sets both. `accountType` is optional, `checking` or `savings`. `accountHolder.address` is optional; when you send it, `line1` and `city` are required, `line1` is at most 100 characters, and a PO box or private mailbox is rejected. The full account number is never returned. `accountHolder.name` is always echoed back exactly as sent, and ownership is evaluated when you create an order, not on the payment method.
```json theme={null}
{
"type": "bank-account-mx-clabe",
"paymentMethodId": "ba4280b4-8fe3-41e5-9c38-f72d453c950f",
"status": "active",
"statusReason": null,
"label": "Mi cuenta Nu",
"bankAccount": {
"accountHolder": {
"name": "Andrea Fuentes Lopez"
},
"accountSuffix": "8255",
"currency": "mxn",
"country": "MX",
"entityType": "individual",
"rails": {
"status": "complete",
"supported": [
"spei"
],
"pending": []
}
},
"createdAt": "2026-09-17T14:16:38.552Z",
"updatedAt": "2026-09-17T14:16:41.026Z"
}
```
The full CLABE is never returned. The response carries `bankAccount.accountSuffix`, the last four digits, and no bank field, because the CLABE identifies the bank. A malformed CLABE check digit is rejected synchronously with `422` and nothing is stored. Crossmint also rejects a request that sends `bankCode`, `bankName`, or `institutionId` with `422`. `accountHolder.name` is always echoed back exactly as sent, and ownership is evaluated when you create an order, not on the payment method.
Saving a payment method does not require the user to be verified yet, but creating an order does. Before you offramp to it, verify the user and link the wallet they will pay from. See the [Quickstart](/offramp/quickstarts/rest-api).
For a complete list of fields and supported payment method types, see [The Payment Method Object](/offramp/api-reference/payment-methods/payment-method-object) in the API reference.
## Statuses
Every payment method type shares one lifecycle. `status` describes whether the payment method has passed the readiness checks required for its type and can proceed to order-level eligibility checks. It does not describe user approval, ownership, or guaranteed payout success.
| Status | Meaning |
| - | - |
| `pending` | Initial readiness checks are running. |
| `active` | Required readiness checks passed. Orders still run their own eligibility checks. |
| `rejected` | The destination cannot receive a supported payout. The initial checks ended without a usable record, a provider refused the account, or the bank returned a payout to it. To retry, create a new payment method. |
| `deleted` | Removed by you. Never reactivates. |
### Status Changes
Creation always returns `pending`. Poll the payment method to get its latest status. A US or SEPA account usually becomes `active` within seconds. A Mexican CLABE or a Bre-B key can take several minutes, because a check with the bank runs first.
The allowed transitions are:
* `pending` → `active`, `rejected`, or `deleted`
* `active` → `rejected` when a provider refuses the account or the bank returns a payout to it, or `deleted`
* `rejected` → `deleted`
An order refuses a payment method that is not `active` with the error code `payments:payment-method.not-active`. If a provider refuses the account, an `active` payment method changes to `rejected`. When this happens while you create or update the order, that request fails with the error code `payments:payment-method.rejected-by-provider`. If the payment method is deleted after the order is created and before the payout starts, the order fails. A refund of the deposit starts, except when a Colombian payout was set up before the check. An order that waits for deposit instructions also fails when its payment method is no longer `active`.
When the bank returns a payout because the account cannot receive it, every payment method for that account in your project changes to `rejected`, for all of your users. A deleted one stays `deleted`. `statusReason` gives the reason. A new payment method for that account in the same project then fails with the error code `payments:payment-method.destination-blocked`, and the error response carries the same `statusReason`:
```json theme={null}
{
"error": true,
"message": "This bank account cannot receive payouts. Use a different bank account.",
"code": "payments:payment-method.destination-blocked",
"statusReason": "destination-closed"
}
```
Ask the user for a different account.
A deleted method never reactivates.
Readiness checks start when the method is created. Fetching it, editing its label, or using it in an order does not restart those checks. A failed transfer changes the payment-method status only when the bank returns the payout because the account cannot receive it.
### Status Reasons
`statusReason` is `null` unless the payment method is `rejected`. A rejected method may include one of the following reason codes:
| Code | Meaning |
| - | - |
| `destination-not-found` | The account or key is confirmed not to exist. |
| `destination-closed` | The account is confirmed closed. |
| `destination-cannot-receive` | The destination cannot receive supported payouts. |
| `no-rail-available` | No supported payout rail reaches the destination. This is not a temporary outage. |
Branch on `status`, never on `statusReason`. New codes can appear at any time, so tolerate values you do not recognize. `statusReason` never carries provider errors, ownership, or compliance detail. Ownership is evaluated when you create an order, not on the payment method.
## Rails
`rails.supported` lists the rails available to reach the destination straight away, and `rails.pending` lists rails that may become available once confirmed. `rails.supported` can list rails while the method is still `pending`. Only an `active` method accepts an order. A valid account with no rail to reach it is `rejected` with `statusReason: "no-rail-available"`.
## List, Update, and Remove
Reads and non-sensitive updates go through the standard API. Use it to [list a user's payment methods](/offramp/api-reference/payment-methods/list-payment-methods), [fetch one](/offramp/api-reference/payment-methods/get-payment-method), [update the label or other non-sensitive metadata](/offramp/api-reference/payment-methods/update-payment-method), or [remove a payment method](/offramp/api-reference/payment-methods/delete-payment-method).
## Next Steps
Use a saved payment method in an offramp order
Find the institutionId of a bank by country
Full request and response schema in the API reference
# Launch in Production
Source: https://docs.crossmint.com/offramp/guides/going-to-production
Move your offramp integration from staging to live payouts
Everything you build on staging runs end to end with simulated payouts. When you are ready to move funds to users' banks, switch to production.
## What Changes
| | Staging | Production |
| :- | :- | :- |
| API host | `staging.crossmint.com` | `www.crossmint.com` |
| Vault host | `vault.staging.crossmint.com` | `vault.crossmint.com` |
| API key | Staging server key | Production server key |
| Crypto | Testnet USDC | Mainnet USDC |
| Chain (`payment.method`) | Testnet (for example `base-sepolia`, `ethereum-sepolia`) | Mainnet (for example `base`, `solana`, `ethereum`) |
| Payout | Simulated | Fiat settles in the user's bank account |
Get your production API key from the Crossmint console. It needs the `orders`, `payment-methods`, `users.create`, and `users.read` scopes.
## Go-Live Checklist
Production access requires a signed Order Form and completed KYB verification, and your project enabled for KYC data sharing. Contact sales to get set up, and see [account verification](/introduction/platform/account-verification) for what KYB involves.
Verify each user in production. If you import KYC data, confirm it meets the [data requirements](/identity/data-requirements) for each region you serve.
Wire up [order tracking](/offramp/guides/manage-orders) by polling or webhooks so your app reflects payout progress and surfaces failures.
Point your requests at the production hosts and swap in your production API key. Send mainnet USDC on a supported chain.
## Next Steps
Enable production access
Verification requirements
# Import User KYC Data
Source: https://docs.crossmint.com/offramp/guides/import-user-kyc-data
Pass your users' existing KYC data to Crossmint so they skip verification
If your platform already verifies your users, share that data with Crossmint so they skip in-flow verification and are ready to cash out. This guide covers passing that data and confirming the user is verified. For how verification works in depth, see [KYC & Compliance](/offramp/concepts/kyc-and-compliance).
## Register and Verify the User
Each call uses a `userId:` locator, where `` is a unique identifier you choose for the user and reuse across the calls below.
Before sharing any user KYC data with Crossmint, the user must accept Crossmint's Privacy Policy in your app. Consent must be collected with an explicit checkbox and specific wording — see [Register User Data](/identity/register-user-data) for the exact consent-collection requirements. Only once the user has accepted, record their consent:
```bash theme={null}
curl -X PUT 'https://staging.crossmint.com/api/2025-06-09/users/userId:/legal-documents' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{ "type": "crossmint-privacy-policy", "acceptedAt": "2026-01-01T00:00:00.000Z" }'
```
```json theme={null}
{
"acceptedAt": "2026-01-01T00:00:00.000Z"
}
```
Send the user's details along with the KYC data you already hold.
```bash theme={null}
curl -X PUT 'https://staging.crossmint.com/api/2025-06-09/users/userId:' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"userDetails": {
"firstName": "Alice",
"lastName": "Smith",
"dateOfBirth": "1990-01-15",
"countryOfResidence": "US"
},
"kycData": {
"email": "alice@example.com",
"phoneNumber": "+13055551234",
"nationality": "US",
"addressOfResidence": {
"line1": "701 S Miami Ave",
"city": "Miami",
"postalCode": "33156",
"stateOrRegion": "FL"
},
"identityDocument": {
"type": "ssn",
"number": "123-45-6789",
"issuingCountryCode": "US"
},
"ipAddresses": ["203.0.113.1"]
},
"verificationHistory": {
"idVerificationTimestamp": "2026-01-01T00:00:00.000Z",
"livenessVerificationTimestamp": "2026-01-01T00:00:00.000Z"
}
}'
```
The booleans in the response show which sections were stored.
**US residents:** include `kycData.phoneNumber` (required), and provide identity via SSN in `kycData.identityDocument` (no document upload is needed for standard offramp). The `dueDiligence` object is not required for standard US offramp in most states — include it only where required (see [Data Requirements](/identity/data-requirements)).
```json theme={null}
{
"userId": "",
"userDetails": true,
"kycData": true,
"dueDiligence": false,
"verificationHistory": true
}
```
Trigger verification so Crossmint runs its compliance checks against the data you provided.
```bash theme={null}
curl -X PUT 'https://staging.crossmint.com/api/2025-06-09/users/userId:/identity-verification' \
-H 'X-API-KEY: '
```
The response returns the eligibility status per product, starting at `pending-review`.
```json theme={null}
{
"eligibility": [
{ "type": "onramp", "status": "pending-review" },
{ "type": "onramp-light", "status": "pending-review" },
{ "type": "offramp", "status": "pending-review" },
{ "type": "offramp-light", "status": "pending-review" },
{ "type": "regulated-transfer", "status": "pending-review" }
]
}
```
Verification is asynchronous. Poll until the `offramp` eligibility reads `verified`. Only a verified user can save a payment method or create an order.
If the status reads `requires-data` with a document listed in `missingDocuments`, Crossmint's compliance team has requested an additional document — upload it via the [Upload Document API](/api-reference/users/upload-document) and re-trigger verification. See [additional document requests](/identity/quickstart#additional-document-requests).
```bash theme={null}
curl -X GET 'https://staging.crossmint.com/api/2025-06-09/users/userId:/identity-verification' \
-H 'X-API-KEY: '
```
```json theme={null}
{
"eligibility": [
{ "type": "onramp", "status": "verified" },
{ "type": "onramp-light", "status": "verified" },
{ "type": "offramp", "status": "verified" },
{ "type": "offramp-light", "status": "verified" },
{ "type": "regulated-transfer", "status": "verified" }
]
}
```
The exact fields required vary by region. See the [Identity data requirements](/identity/data-requirements) for the full list, and the [Identity quickstart](/identity/quickstart) for uploading documents when a region requires them.
The account holder name on the user's saved bank account must match this verified identity. Offramp supports first-party payouts only.
## Next Steps
Save the verified user's bank account
Build the full flow
# Institution IDs
Source: https://docs.crossmint.com/offramp/guides/institution-ids
The institutionId to send when you save a bank account in a country where the account number does not identify the bank
Some countries' account numbers do not say which bank holds them. For these countries, a [bank-account payment method](/offramp/guides/collect-bank-accounts) needs `bankAccount.institutionId`, a Crossmint ID for the bank. Show your users the bank names from the table below, and send the matching `institutionId`.
| Payment method type | Country | Needs `institutionId` |
| :- | :- | :- |
| `bank-account-us` | United States | No. The routing number identifies the bank |
| `bank-account-sepa-iban` | SEPA countries | No. The IBAN identifies the bank |
| `bank-account-co` | Colombia | Yes |
| `bank-account-id` | Indonesia | Yes |
| `bank-account-mx-clabe` | Mexico | No. The CLABE identifies the bank |
| `bank-account-ng` | Nigeria | Yes |
| `bank-account-ph` | Philippines | Yes |
| `bank-account-th` | Thailand | Yes |
| `bank-account-vn` | Vietnam | Yes |
For example, this request body saves a Bancolombia account for a Colombian user:
```json theme={null}
{
"type": "bank-account-co",
"userLocator": "email:ana@example.com",
"bankAccount": {
"accountNumber": "1013555555",
"institutionId": "co-bancolombia",
"accountType": "checking",
"entityType": "individual",
"accountHolder": {
"name": "Ana Perez Garcia",
"document": { "type": "cc", "number": "1017218529" }
}
}
}
```
## Rules
* Send the `institutionId` exactly as listed. It is an opaque ID: do not build or parse it.
* Do not send a national bank code such as `1007`. The request is rejected with `422`.
* An `institutionId` never changes once published, even if the bank is renamed.
* When a bank closes or becomes unavailable for payouts, it leaves the table and new requests with its ID return `422`. Saved payment methods keep their ID, so keep the bank name your user picked.
* The names are shown as the bank registers them, often in capitals. You may format them for display.
## Colombia
Banks for `bank-account-co`.
| Bank | `institutionId` |
| - | - |
| Alianza fiduciaria | `co-alianza-fiduciaria` |
| BAN100 S.A | `co-ban100` |
| BANCO AGRARIO | `co-banco-agrario` |
| BANCO AV VILLAS | `co-av-villas` |
| BANCO BTG PACTUAL | `co-btg-pactual` |
| BANCO CAJA SOCIAL BCSC SA | `co-banco-caja-social` |
| BANCO COOPERATIVO COOPCENTRAL | `co-coopcentral` |
| BANCO DAVIVIENDA SA | `co-davivienda` |
| BANCO DE BOGOTA | `co-banco-de-bogota` |
| BANCO DE LAS MICROFINANZAS - BANCAMIA S.A. | `co-bancamia` |
| BANCO DE OCCIDENTE | `co-banco-de-occidente` |
| BANCO FALABELLA S.A. | `co-banco-falabella` |
| BANCO FINANDINA S.A. | `co-banco-finandina` |
| BANCO GNB SUDAMERIS | `co-gnb-sudameris` |
| BANCO J.P. MORGAN COLOMBIA S.A. | `co-jp-morgan` |
| BANCO MUNDO MUJER | `co-banco-mundo-mujer` |
| BANCO PICHINCHA | `co-banco-pichincha` |
| BANCO POPULAR | `co-banco-popular` |
| BANCO SANTANDER DE NEGOCIOS COLOMBIA S.A | `co-santander` |
| BANCO SERFINANZA S.A | `co-banco-serfinanza` |
| BANCO UNION S.A | `co-banco-union` |
| BANCO W | `co-banco-w` |
| BANCOLDEX S.A. | `co-bancoldex` |
| BANCOLOMBIA | `co-bancolombia` |
| BANCOOMEVA | `co-bancoomeva` |
| BBVA COLOMBIA | `co-bbva` |
| BOLD CF | `co-bold` |
| CITIBANK | `co-citibank` |
| COINK | `co-coink` |
| COLTEFINANCIERA S.A | `co-coltefinanciera` |
| CONFIAR COOPERATIVA FINANCIERA | `co-confiar` |
| COOPERATIVA FINANCIERA DE ANTIOQUIA | `co-cfa` |
| COOTRAFA COOPERATIVA FINANCIERA | `co-cootrafa` |
| Crezcamos | `co-crezcamos` |
| Dale | `co-dale` |
| DAVIBANK | `co-davibank` |
| DAVIPLATA | `co-daviplata` |
| DING TECNIPAGOS SA | `co-ding` |
| FINANCIERA JURISCOOP S.A. COMPAÑIA DE FINANCIAMIENTO | `co-juriscoop` |
| GLOBAL66 | `co-global66` |
| IRIS | `co-iris` |
| ITAU | `co-itau` |
| JFK COOPERATIVA FINANCIERA | `co-jfk` |
| LULO BANK S.A. | `co-lulo-bank` |
| MIBANCO S.A. | `co-mibanco` |
| MOVII | `co-movii` |
| NEQUI | `co-nequi` |
| NU COLOMBIA | `co-nu` |
| PIBANK | `co-pibank` |
| POWWI | `co-powwi` |
| RAPPIPAY | `co-rappipay` |
| UALA | `co-uala` |
## Indonesia
Banks for `bank-account-id`.
| Bank | `institutionId` |
| - | - |
| Bank Aceh | `id-bank-aceh` |
| Bank Antardaerah | `id-bank-antardaerah` |
| Bank ANZ Indonesia | `id-anz` |
| Bank Artha Graha Internasional | `id-artha-graha` |
| Bank Banten | `id-bank-banten` |
| Bank BCA Syariah | `id-bca-syariah` |
| Bank BPD Bali | `id-bpd-bali` |
| Bank BPD DIY | `id-bpd-diy` |
| Bank BPD Papua | `id-bank-papua` |
| Bank BPR EKA | `id-bpr-eka` |
| Bank BRI Agroniaga | `id-bri-agroniaga` |
| Bank BRI Syariah | `id-bri-syariah` |
| Bank BTPN | `id-btpn` |
| BANK BTPN SYARIAH | `id-btpn-syariah` |
| Bank Bukopin | `id-bukopin` |
| Bank Bumi Arta | `id-bumi-arta` |
| Bank Capital Indonesia | `id-bank-capital` |
| Bank Central Asia (BCA) | `id-bca` |
| Bank Chinatrust Indonesia (CTBC) | `id-ctbc` |
| Bank CIMB Niaga | `id-cimb-niaga` |
| Bank Commonwealth | `id-commonwealth` |
| Bank Danamon | `id-danamon` |
| Bank DBS Indonesia | `id-dbs` |
| Bank DKI | `id-bank-dki` |
| Bank Ganesha | `id-bank-ganesha` |
| Bank HARDA Internasional | `id-bank-harda` |
| Bank HSBC Indonesia | `id-hsbc` |
| Bank IBK Indonesia (Bank Agris) | `id-ibk` |
| Bank ICBC Indonesia | `id-icbc` |
| Bank Ina Perdana | `id-bank-ina-perdana` |
| Bank Index | `id-bank-index` |
| Bank J Trust Indonesia | `id-j-trust` |
| Bank Jabar (BJB) | `id-bjb` |
| Bank Jabar Banten Syariah | `id-bjb-syariah` |
| Bank Jago | `id-bank-jago` |
| Bank Jambi | `id-bank-jambi` |
| Bank Jasa Jakarta | `id-bank-jasa-jakarta` |
| Bank Jateng | `id-bank-jateng` |
| Bank Jatim | `id-bank-jatim` |
| Bank Kalbar | `id-bank-kalbar` |
| Bank Kalsel | `id-bank-kalsel` |
| Bank Kalteng | `id-bank-kalteng` |
| Bank Kaltim | `id-bank-kaltim` |
| Bank Lampung | `id-bank-lampung` |
| Bank Maluku | `id-bank-maluku` |
| Bank Mandiri | `id-mandiri` |
| Bank Mandiri Taspen (Bank Mantap) | `id-bank-mantap` |
| Bank Maspion Indonesia | `id-bank-maspion` |
| Bank Mayapada International | `id-mayapada` |
| Bank Mayora | `id-bank-mayora` |
| Bank Mega | `id-bank-mega` |
| Bank Mega Syariah | `id-bank-mega-syariah` |
| Bank Mestika | `id-bank-mestika` |
| BANK MNC INTERNASIONAL (MNC BANK) | `id-mnc-bank` |
| Bank Muamalat | `id-muamalat` |
| Bank Multiarta Sentosa | `id-bank-multiarta-sentosa` |
| Bank Nagari | `id-bank-nagari` |
| Bank National NOBU | `id-nobu` |
| Bank Negara Indonesia (BNI) | `id-bni` |
| Bank Negara Indonesia Syariah | `id-bni-syariah` |
| Bank NTB | `id-bank-ntb` |
| Bank NTT | `id-bank-ntt` |
| Bank Nusantara Parahyangan | `id-bank-nusantara-parahyangan` |
| Bank OCBC NISP | `id-ocbc-nisp` |
| Bank Of China Indonesia | `id-bank-of-china` |
| Bank of India Indonesia | `id-bank-of-india` |
| Bank Of Tokyo Mitsubishi Ufj Ltd. The | `id-mufg` |
| Bank Oke Indonesia (Bank Dinar) | `id-bank-oke` |
| Bank Panin | `id-panin` |
| Bank Panin Syariah | `id-panin-syariah` |
| Bank Permata | `id-permata` |
| Bank Prima Master | `id-bank-prima-master` |
| Bank QNB Indonesia | `id-qnb` |
| Bank Rakyat Indonesia (BRI) | `id-bri` |
| Bank Riau Kepri | `id-bank-riau-kepri` |
| Bank Royal | `id-bank-royal` |
| Bank Sahabat Sampoerna | `id-bank-sampoerna` |
| Bank Sbi Indonesia | `id-bank-sbi` |
| Bank Shinhan Indonesia | `id-shinhan` |
| Bank Sinarmas | `id-bank-sinarmas` |
| Bank Sulselbar | `id-bank-sulselbar` |
| Bank Sulteng | `id-bank-sulteng` |
| Bank Sultra | `id-bank-sultra` |
| Bank Sulut | `id-bank-sulut` |
| Bank Sumsel Babel | `id-bank-sumsel-babel` |
| Bank Sumut | `id-bank-sumut` |
| Bank Syariah Bukopin | `id-bukopin-syariah` |
| Bank Syariah Indonesia | `id-bsi` |
| Bank Syariah Mandiri | `id-mandiri-syariah` |
| Bank Tabungan Negara (BTN) | `id-btn` |
| Bank UOB Indonesia | `id-uob` |
| Bank Victoria Internasional | `id-bank-victoria` |
| Bank Victoria Syariah | `id-bank-victoria-syariah` |
| Bank Windu (China Construction Bank Indonesia) | `id-ccb` |
| Bank Woori Saudara | `id-woori-saudara` |
| Bank Yudha Bhakti | `id-bank-yudha-bhakti` |
| Centratama Nasional Bank (CNB) | `id-cnb` |
| Citibank N.a. | `id-citibank` |
| KEB Hana Bank | `id-keb-hana` |
| Maybank Indonesia | `id-maybank` |
| PT Bank SeaBank Indonesia (formerly Bank Kesejahteraan Ekonomi) | `id-seabank` |
| Standard Chartered Bank | `id-standard-chartered` |
| Superbank | `id-superbank` |
## Nigeria
Banks for `bank-account-ng`.
| Bank | `institutionId` |
| - | - |
| Access Bank PLC | `ng-access-bank` |
| CITI Bank | `ng-citi-bank` |
| Ecobank Nigeria PLC | `ng-ecobank-nigeria` |
| Fidelity Bank PLC | `ng-fidelity-bank` |
| First Bank Of Nigeria PLC | `ng-first-bank-of-nigeria` |
| First City Monument Bank PLC | `ng-first-city-monument-bank` |
| GT Bank | `ng-gt-bank` |
| Jaiz Bank | `ng-jaiz-bank` |
| Keystone Bank PLC | `ng-keystone-bank` |
| Polaris Bank PLC | `ng-polaris-bank` |
| Providus Bank | `ng-providus-bank` |
| Stanbic IBTC Bank PLC | `ng-stanbic-ibtc-bank` |
| Standard Chartered Bank PLC | `ng-standard-chartered-bank` |
| Sterling Bank PLC | `ng-sterling-bank` |
| Suntrust Bank | `ng-suntrust-bank` |
| Union Bank Of Nigeria PLC | `ng-union-bank-of-nigeria` |
| United Bank For Africa PLC | `ng-united-bank-for-africa` |
| Unity Bank PLC | `ng-unity-bank` |
| Wema Bank PLC | `ng-wema-bank` |
| Zenith International Bank PLC | `ng-zenith-international-bank` |
## Philippines
Banks for `bank-account-ph`.
| Bank | `institutionId` |
| - | - |
| AL AMANAH ISLAMIC INVESTMENT BANK | `ph-al-amanah-islamic-bank` |
| ALLBANK | `ph-allbank` |
| Anz Bank | `ph-anz` |
| Asia united Bank | `ph-aub` |
| Banco De Oro | `ph-bdo` |
| Bangko Kabayan Inc | `ph-bangko-kabayan` |
| Bangko Mabuhay, Inc. | `ph-bangko-mabuhay` |
| Bangko Nuestra Senora Del Pilar Inc | `ph-bangko-nuestra-senora` |
| Bangkok Bank | `ph-bangkok-bank` |
| Bank Of America | `ph-bank-of-america` |
| Bank Of China | `ph-bank-of-china` |
| Bank Of Commerce | `ph-bank-of-commerce` |
| Bank Of Philippines Islands | `ph-bpi` |
| BDO NETWORK BANK | `ph-bdo-network-bank` |
| BDO PRIVATE BANK | `ph-bdo-private-bank` |
| Binan Rural Bank, Inc | `ph-binan-rural-bank` |
| Binangonan Rural Bank (Brbdigital) | `ph-brbdigital` |
| BOF, INC (A Rural Bank) - Bank of Florida | `ph-bank-of-florida` |
| BPI Direct Banko, Inc. | `ph-bpi-banko` |
| BPI Family Savings Bank | `ph-bpi-family-savings` |
| CAMALIG BANK, INC (A Rural Bank) | `ph-camalig-bank` |
| Cantilan Bank, Inc. (A Rural Bank) | `ph-cantilan-bank` |
| Card Bank Inc | `ph-card-bank` |
| Cathay United Bank | `ph-cathay-united-bank` |
| CEBUANA LHUILLIER RURAL BANK INC | `ph-cebuana-lhuillier-bank` |
| China Bank | `ph-china-bank` |
| China Bank Saving | `ph-china-bank-savings` |
| CIMB BANK PHILIPPINES INC | `ph-cimb` |
| Citibank N.a. | `ph-citibank` |
| Cooperative Bank Of Quezon Province | `ph-coop-bank-of-quezon` |
| COUNTRY BUILDERS BANK INC (A Rural Bank) | `ph-country-builders-bank` |
| CTBC Bank (Philippines) Corporation | `ph-ctbc` |
| Deutsche Bank | `ph-deutsche-bank` |
| Development Bank Of Philippines | `ph-dbp` |
| Dungganon Bank, Inc. | `ph-dungganon-bank` |
| East-West Bank | `ph-eastwest-bank` |
| EastWest Rural Bank | `ph-eastwest-rural-bank` |
| Entrepreneur Bank | `ph-entrepreneur-bank` |
| Equicom Saving Bank, Inc. | `ph-equicom-savings-bank` |
| FIRST CONSOLIDATED BANK | `ph-first-consolidated-bank` |
| Gotyme Bank Corporation | `ph-gotyme` |
| Guagua Rural Bank, Inc. (Grbank) | `ph-guagua-rural-bank` |
| HSBC | `ph-hsbc` |
| Industrial And Commercial Bank Of China | `ph-icbc` |
| INDUSTRIAL BANK OF KOREA | `ph-ibk` |
| INNOVATIVE BANK INC | `ph-innovative-bank` |
| Isla Bank, Inc. | `ph-isla-bank` |
| JP Morgan Chase | `ph-jp-morgan` |
| KEB HANA | `ph-keb-hana` |
| Laguna Prestige Bank | `ph-laguna-prestige-bank` |
| Land Bank | `ph-landbank` |
| Luzon Development Bank | `ph-luzon-development-bank` |
| Malarayat Rural Bank, Inc. | `ph-malarayat-rural-bank` |
| Malayan Bank Saving and Mortgage Bank, Inc. | `ph-malayan-bank` |
| Maya Bank, Inc. | `ph-maya` |
| Maybank | `ph-maybank` |
| MEGA INTL COMML BANK CO LTD (ICBC) | `ph-mega-bank` |
| Metrobank | `ph-metrobank` |
| Mindanao Consolidated Cooperative Bank | `ph-mindanao-coop-bank` |
| MIZUHO BANK LTD (Fuji Bank) | `ph-mizuho` |
| Money Mall Rural Bank, Inc | `ph-money-mall-rural-bank` |
| MUFG BANK LTD (Bank Of Tokyo) | `ph-mufg` |
| Mvsm (A Rural Bank Since 1953),inc | `ph-mvsm-bank` |
| Netbank (A Rural Bank), Inc. | `ph-netbank` |
| OmniPay, Inc. | `ph-omnipay` |
| Pacific Ace Savings Bank | `ph-pacific-ace-savings-bank` |
| Partner Rural Bank, Inc. | `ph-partner-rural-bank` |
| Philippine Bank of Communications | `ph-pbcom` |
| Philippine National Bank | `ph-pnb` |
| Philippine Trust Company | `ph-philtrust` |
| Philippine Veterans Bank | `ph-veterans-bank` |
| Philippines Business Bank | `ph-pbb` |
| Philippines Saving Bank | `ph-psbank` |
| PNB Savings Bank | `ph-pnb-savings-bank` |
| PRODUCERS SAVINGS BANK CORP | `ph-producers-bank` |
| QCRB | `ph-qcrb` |
| Queen City Development Bank (Queen Bank) | `ph-queenbank` |
| RANG-AY BANK, INC. (A Rural Bank) | `ph-rang-ay-bank` |
| Rbt Bank, Inc (A Rural Bank), And | `ph-rbt-bank` |
| RCBC | `ph-rcbc` |
| RCBC Saving Bank | `ph-rcbc-savings-bank` |
| Robinson Bank | `ph-robinsons-bank` |
| Rural Bank Of Bacolod City, Inc | `ph-rural-bank-of-bacolod-city` |
| Rural Bank Of Bauang, Inc | `ph-rural-bank-of-bauang` |
| Rural Bank Of Digos Inc (Rbdi) | `ph-rural-bank-of-digos` |
| RURAL BANK OF GUINOBATAN INC (RBGI) | `ph-rural-bank-of-guinobatan` |
| Rural Bank Of La Paz Inc | `ph-rural-bank-of-la-paz` |
| Rural Bank Of Lebak (Sultan Kudarat), Inc. | `ph-rural-bank-of-lebak` |
| Rural Bank Of Montalban Inc | `ph-rural-bank-of-montalban` |
| Rural Bank Of Porac (Pampanga), Inc. | `ph-rural-bank-of-porac` |
| Rural Bank Of Rosario Inc | `ph-rural-bank-of-rosario` |
| Rural Bank Of Sagay, Inc. | `ph-rural-bank-of-sagay` |
| Rural Bank Of San Leonardo | `ph-rural-bank-of-san-leonardo` |
| Rural Bank Of Sta. Ignacia Inc | `ph-rural-bank-of-sta-ignacia` |
| Seabank Philippines , Inc. | `ph-seabank` |
| Security Bank | `ph-security-bank` |
| SHINHAN BANK | `ph-shinhan` |
| SOUTHEAST COUNTRY BANK, INC. (A RURAL, BANK) | `ph-southeast-country-bank` |
| Standard Chartered | `ph-standard-chartered` |
| Sterling Bank of Asia | `ph-sterling-bank-of-asia` |
| SUMITOMO MITSUI BANKING CORP | `ph-smbc` |
| Sun Savings Bank, Inc. | `ph-sun-savings-bank` |
| Tonik Digital Bank, Inc. | `ph-tonik` |
| Union Bank | `ph-unionbank` |
| Union Digital Bank Inc | `ph-union-digital-bank` |
| United Overseas Bank | `ph-uob` |
| Unobank Inc | `ph-unobank` |
| WealthBank | `ph-wealthbank` |
| Yuanta Savings Bank Philippines, Inc. | `ph-yuanta-savings-bank` |
## Thailand
Banks for `bank-account-th`.
| Bank | `institutionId` |
| - | - |
| Bangkok Bank Public Company Limited | `th-bangkok-bank` |
| Bank for Agriculture and Agricultural Cooperatives | `th-baac` |
| Bank of Ayudhya Public Company Limited | `th-bank-of-ayudhya` |
| CIMB Thai Bank Public Company Limited | `th-cimb-thai` |
| Citibank N.A. | `th-citibank` |
| Government Saving Bank | `th-government-savings-bank` |
| Kasikorn Bank Public Company Limited | `th-kasikorn-bank` |
| Kiatnakin Bank Public Company Limited | `th-kiatnakin-bank` |
| Krung Thai Bank Public Company Limited | `th-krung-thai-bank` |
| Siam Commercial Bank Public Company Limited | `th-siam-commercial-bank` |
## Vietnam
Banks for `bank-account-vn`.
| Bank | `institutionId` |
| - | - |
| An Binh Commercial Joint Stock Bank (AnBinh Bank) | `vn-abbank` |
| Asia Commercial Bank (ACB) | `vn-acb` |
| Bank for Investment and Development of Vietnam Joint Stock Commercial Bank (BIDV) | `vn-bidv` |
| BaoViet Commercial Joint Stock Bank (BaoViet Bank) | `vn-baoviet-bank` |
| Cimb Bank (Vietnam) Limited | `vn-cimb` |
| Global Petro Sole Member Limited Commercial Bank (GPBank) | `vn-gpbank` |
| Ho Chi Minh City Development Joint Stock Commercial Bank (HDBank) | `vn-hdbank` |
| Hong Leong Bank Vietnam | `vn-hong-leong-bank` |
| IM BANK | `vn-im-bank` |
| Indovina Bank | `vn-indovina-bank` |
| Industrial Bank Of Korea Ha Noi Branch | `vn-ibk-hanoi` |
| Joint Stock Commercial Bank for Foreign Trade of Vietnam (Vietcombank) | `vn-vietcombank` |
| Kien Long Commercial Joint Stock Bank (KienLong Bank) | `vn-kienlongbank` |
| Lien Viet Post Joint Stock Commercial Bank (LienViet Bank) | `vn-lienviet-bank` |
| Military Commercial Joint Stock bank (MB) | `vn-mb-bank` |
| Modern Bank of Vietnam (MBV) | `vn-mbv` |
| Nam A Commercial Joint Stock Bank (NamA Bank) | `vn-nam-a-bank` |
| National Citizen Commercial Joint Stock Bank (NCB) | `vn-ncb` |
| Orient Commercial Bank (OCB) | `vn-ocb` |
| Petrolimex Group Commercial Joint Stock Bank (PG Bank) | `vn-pgbank` |
| Public Bank Vietnam Limited (Public Bank) | `vn-public-bank` |
| Saigon - Hanoi Commercial Joint Stock Bank (SHB) | `vn-shb` |
| Saigon Bank for Industry and Trade (SaigonBank) | `vn-saigonbank` |
| Saigon Commercial Bank (SGCB) | `vn-scb` |
| Saigon Thuong Tin Commercial Joint Stock Bank (SacomBank) | `vn-sacombank` |
| Shinhan Bank Vietnam (SHBVN) | `vn-shinhan` |
| Southeast Asia Commercial Joint Stock Bank (SeaBank) | `vn-seabank` |
| TAIPEI FUBON BANK HANOI Branch | `vn-taipei-fubon-hanoi` |
| TAIPEI FUBON BANK HCM Branch | `vn-taipei-fubon-hcm` |
| Tienphong commercial Joint Stock Bank (TienPhong Bank) | `vn-tpbank` |
| United Overseas Bank (Vietnam) Limited | `vn-uob` |
| Viet A Joint Stock Commercial Bank (Viet A Bank) | `vn-viet-a-bank` |
| Viet Capital Joint Stock Commercial Bank (Viet Capital Bank) | `vn-viet-capital-bank` |
| Vietnam Bank for Agriculture and Rural Development (AgriBank) | `vn-agribank` |
| Vietnam Export Import Commercial Joint Stock Bank (EximBank) | `vn-eximbank` |
| Vietnam International Commercial Joint Stock Bank (VIB) | `vn-vib` |
| Vietnam Joint Stock Commercial Bank for Intrustry and Trade (VietinBank) | `vn-vietinbank` |
| Vietnam Maritime Commercial Joint Stock Bank (Maritime Bank) | `vn-maritime-bank` |
| Vietnam Prosperity Joint Stock Commercial Bank (VPBank) | `vn-vpbank` |
| Vietnam Public Joint Stock Commercial Bank (PVcomBank) | `vn-pvcombank` |
| Vietnam Russia Joint Venture Bank (VR Bank) | `vn-vrb` |
| Vietnam Technological and commercial Joint Stock Bank (Techcom Bank) | `vn-techcombank` |
| Vietnam Thuong Tin Commercial Joint Stock Bank (Vietbank) | `vn-vietbank` |
| Vikki Digital Bank (Changed from Dong A) | `vn-vikki-bank` |
| Woori Bank | `vn-woori-bank` |
## Next Steps
Use a saved payment method in an offramp order
Full request and response schema in the API reference
# Manage Orders
Source: https://docs.crossmint.com/offramp/guides/manage-orders
Track offramp order status through every phase, handle webhooks, and troubleshoot common issues
After you create an offramp order, you track it from the stablecoin deposit through to the fiat payout arriving in your user's bank account. This guide shows you how to monitor that journey, react to each status correctly, and handle every outcome along the way.
In this guide, you will learn how to:
* Check an order's status and know which fields to watch
* Follow an order through each phase of its lifecycle
* Map each status to the right UI state and next action
* Receive order updates through webhooks instead of polling
* Troubleshoot expired quotes and rejected payouts
## Check Order Status
Fetch an order any time with its `orderId` (see the [Get Order API reference](/offramp/api-reference/get-order) for the complete response schema):
```bash theme={null}
curl -X GET \
'https://staging.crossmint.com/api/2022-06-09/orders/' \
-H 'X-API-KEY: '
```
The response includes the full order object (see the [Quickstart](/offramp/quickstarts/rest-api) for the full end-to-end flow). The key fields to watch are:
* **`phase`**: the high-level stage of the order
* **`payment.status`**: whether the stablecoin deposit has been received
* **`lineItems[0].delivery.status`**: whether the fiat payout is pending, in transit, completed, or failed
* **`lineItems[0].delivery.rail`**: the payout rail the order routed through, snapshotted once resolved and never changed afterward (see [Payment Schemes](/offramp/payment-methods))
* **`lineItems[0].delivery.completedAt`**: the ISO 8601 timestamp when the payout reached its terminal success signal at the rail; on instant rails (RTP, SEPA Instant, SPEI) this is the real credit-confirmation time, while on delayed rails (ACH, standard SEPA) it reflects settlement and is approximate
If you poll instead of using webhooks, use the [status-handling reference](#status-handling-reference) below to decide when to stop and what to show next.
Below is a full completed-order response with the key fields highlighted:
```json {3,22,27,28,46} theme={null}
{
"orderId": "d3d1e4d8-1aae-45f1-804a-4ca674215bae",
"phase": "completed",
"locale": "en-US",
"lineItems": [
{
"metadata": {
"name": "USD",
"description": "United States Dollar",
"imageUrl": "https://www.crossmint.com/assets/ui/flags/us.svg"
},
"quote": {
"status": "expired",
"charges": {
"unit": { "amount": "1", "currency": "usdc" },
"fees": { "type": "exact", "amount": "0.002", "currency": "usdc" }
},
"totalPrice": { "amount": "1", "currency": "usdc" },
"quantityRange": { "lowerBound": "1", "upperBound": "1" }
},
"delivery": {
"status": "completed",
"recipient": {
"locator": "paymentMethodId:fb04b8bb-2bba-47ba-a105-66a93f61d468",
"paymentMethodId": "fb04b8bb-2bba-47ba-a105-66a93f61d468"
},
"rail": "ach-us",
"completedAt": "2026-06-18T16:07:42.000Z"
},
"executionMode": "exact-in",
"callData": {
"mode": "exact-in",
"amount": "1",
"outFiatCurrency": "usd",
"effectiveAmount": "0.998"
},
"quantity": 1
}
],
"quote": {
"status": "expired",
"quotedAt": "2026-06-18T15:47:51.931Z",
"expiresAt": "2026-06-18T15:57:51.931Z"
},
"payment": {
"status": "completed",
"method": "base",
"currency": "usdc",
"preparation": {
"chain": "base",
"payerAddress": "0x64EC529c6525CeD1Bd565a954a725787E31a89b9"
},
"received": {
"chain": "base",
"txId": "0xc40255de1c9028d1c6401249f5f4a2815749b284f2325d94d231808c2633b3d8",
"amount": "1",
"currency": "usdc"
},
"receiptEmail": "user@example.com"
},
"legal": {
"shownBy": "crossmint",
"requirements": [
{
"display": "show-checkbox",
"type": "crossmint-terms-of-service",
"url": "https://www.crossmint.com/legal/crossmint-terms-of-service/TRGUSAALLALLALL"
},
{
"display": "show-text",
"type": "crossmint-privacy-policy",
"url": "https://www.crossmint.com/legal/crossmint-privacy-policy/ALLALLALLALLALL"
}
]
}
}
```
A completed order can still show `quote.status: "expired"` because the quote time window closes independently of payment and delivery. By itself, this does not indicate a problem. Rely on `phase`, `payment.status`, and `lineItems[].delivery.status` to determine the order outcome.
## Order Lifecycle
The full status model has four phases. Offramp orders start in `payment`, with the quote included inside the order, then move through `delivery` to `completed` when the payout reaches a terminal outcome. The `quote` phase appears if the quote expires before the deposit arrives.
After the order is created and the quote is locked, the order sits in the `payment` phase until the stablecoin deposit arrives on-chain and is matched by memo.
```json theme={null}
{
"phase": "payment",
"payment": { "status": "awaiting-payment" },
"lineItems": [{ "delivery": { "status": "awaiting-payment" } }]
}
```
If no deposit arrives before the quote expires, the order returns to the `quote` phase with `payment.status: "requires-quote"`. See [Quote expired](#troubleshooting) below.
Once the deposit lands, Crossmint converts the stablecoin and initiates the fiat payout. The order moves to the `delivery` phase.
```json theme={null}
{
"phase": "delivery",
"payment": {
"status": "completed",
"received": {
"chain": "base-sepolia",
"txId": "0xc953...a338e85",
"amount": "1",
"currency": "usdc"
}
},
"lineItems": [{ "delivery": { "status": "in-progress" } }]
}
```
Offramp settlement has two asynchronous parts: receiving the stablecoin deposit and completing the fiat payout. After `payment.status` becomes `completed`, the order may remain in `delivery` while the payout is being prepared, processed by the rail, or delayed by weekends and bank holidays. For rail-specific timing, see [Payment Schemes](/offramp/payment-methods).
Once the bank transfer completes, the order moves to `completed` with a successful delivery status.
```json theme={null}
{
"phase": "completed",
"payment": {
"status": "completed",
"received": {
"chain": "base-sepolia",
"txId": "0xc953...a338e85",
"amount": "1",
"currency": "usdc"
}
},
"lineItems": [{ "delivery": { "status": "completed", "completedAt": "2026-06-18T16:07:42.000Z" } }]
}
```
`delivery.completedAt` records when the payout reached its terminal success signal at the rail. On instant rails (RTP, SEPA Instant, SPEI) it is the real credit-confirmation time; on delayed rails (ACH, standard SEPA) it reflects settlement and is approximate, not the exact instant the funds were credited.
`phase: "completed"` means the order is **terminal**, not necessarily successful. Always check `lineItems[0].delivery.status` to determine the outcome: `"completed"` (success) or `"failed"` (see [Bank rejection](#troubleshooting)).
## Order Status Reference
Each field below returns one of a fixed set of values. Expand a field for the full list.
| Value | Meaning |
| :- | :- |
| `quote` | Quote pending or expired; no deposit has been taken yet |
| `payment` | Quote locked; waiting for or processing the on-chain stablecoin deposit |
| `delivery` | Stablecoin deposit received; fiat payout is pending or in progress |
| `completed` | Terminal. Check `delivery.status` for the outcome |
In the standard offramp flow, `payment.status` typically moves through `awaiting-payment`, `in-progress`, and `completed`. The other statuses listed below represent missing setup steps, verification requirements, or payer-wallet conditions.
| Value | Meaning |
| :- | :- |
| `draft` | The order was created with `state: "draft"`. It is a quote preview only and cannot be polled or paid (see [Get an Offramp Quote](/offramp/guides/preview-a-quote)) |
| `requires-quote` | A new or refreshed quote is needed (for example, the previous quote expired) |
| `requires-crypto-payer-address` | The payer wallet address has not been set yet |
| `requires-email` | A receipt email is required before payment can proceed |
| `requires-kyc` | Identity verification (KYC) is required before payment can proceed |
| `pending-kyc-review` | KYC submission is under review |
| `manual-kyc` | KYC requires manual review |
| `failed-kyc` | KYC verification was rejected |
| `requires-recipient-verification` | The recipient must complete wallet ownership verification |
| `awaiting-deposit-instructions` | Deposit instructions are being prepared; wait until the order returns `awaiting-payment` before asking the user to send the deposit |
| `awaiting-payment` | Quote accepted; waiting for the user to send the stablecoin deposit |
| `crypto-payer-insufficient-funds` | The payer wallet does not have enough funds for the deposit |
| `crypto-payer-insufficient-funds-for-gas` | The payer wallet does not have enough native token to pay network gas |
| `in-progress` | Stablecoin deposit detected; waiting for on-chain confirmations |
| `completed` | Stablecoin deposit received and matched to the order |
| Value | Meaning |
| :- | :- |
| `draft` | The order is a draft. No payout will run until the order is created for real |
| `awaiting-payment` | Fiat payout not yet initiated |
| `in-progress` | Fiat payout initiated; awaiting rail completion |
| `completed` | Fiat payout completed |
| `failed` | Payout could not be completed (for example, bank rejected the transfer) |
When a deposit cannot be processed or a payout fails, the order response may include a structured failure reason:
* **`payment.failureReason`**: may be present when the deposit cannot be processed. Shape: `{ code, message?, developerMessage? }`.
* **`lineItems[].delivery.failureReason`**: may be present when the fiat payout fails. Shape: `{ code, message? }`. Possible codes: `transfer-failed`, `insufficient-funds`, `invalid-account`, `payout-unauthorized`, `slippage-tolerance-exceeded`, `payment-refunded`. Branch on `code` — `message` is human-readable copy and not a stable contract.
Payment failure reasons are returned on the order object. If you need to detect deposit-processing issues before a delivery event fires, refetch the order with the [Get Order API](/offramp/api-reference/get-order) and inspect `payment.status` and `payment.failureReason`.
## Status-Handling Reference
Use this table to decide what to show the user and what action to take:
| `phase` | `payment.status` | `delivery.status` | Meaning | Developer action |
| :- | :- | :- | :- | :- |
| `payment` | `awaiting-payment` | `awaiting-payment` | Waiting for stablecoin deposit | Show deposit instructions |
| `payment` | `in-progress` | `awaiting-payment` | Stablecoin deposit detected; waiting for on-chain confirmations | Show "Payment confirming"; do not ask the user to resend funds |
| `delivery` | `completed` | `awaiting-payment` | Stablecoin deposit received; fiat payout not yet initiated | Show "Payout pending"; do not ask the user to resend funds |
| `delivery` | `completed` | `in-progress` | Stablecoin deposit received; fiat payout awaiting rail completion | Show "Payout on the way"; do not ask the user to resend funds |
| `completed` | `completed` | `completed` | Payout completed successfully | Show "Funds sent" confirmation |
| `completed` | `completed` | `failed` | Payout failed | Check `payment.refunded` for refund details; prompt user to retry with corrected bank details |
| `quote` | `requires-quote` | `awaiting-payment` | Quote expired before deposit | Create a new order with a fresh quote |
These statuses may appear during order setup or when the payer wallet has issues. Use `payment.status` as the key to decide what to show:
| `payment.status` | Meaning | Developer action |
| :- | :- | :- |
| `requires-crypto-payer-address` | Payer wallet address is missing | Provide the payer wallet address, then refetch the order |
| `requires-email` | Receipt email is required | Collect the email, then refetch the order |
| `requires-kyc` | Identity verification is required | Send the user through verification before showing deposit instructions |
| `pending-kyc-review`, `manual-kyc` | Identity verification is under review | Show verification pending and wait for the status to update |
| `failed-kyc` | Identity verification was rejected | Do not ask the user to send funds; show the verification failure state |
| `requires-recipient-verification` | Wallet ownership verification is required | Complete recipient verification, then refetch the order |
| `awaiting-deposit-instructions` | Deposit instructions are being prepared | Wait until the order returns `awaiting-payment` before showing deposit instructions |
| `crypto-payer-insufficient-funds` | Payer wallet does not have enough stablecoin for the deposit | Ask the user to fund the wallet or use another wallet |
| `crypto-payer-insufficient-funds-for-gas` | Payer wallet does not have enough native token for gas | Ask the user to add gas or use a wallet with gas sponsorship |
The two insufficient-funds statuses come from Crossmint's balance check when it prepares the deposit transaction, before anything is signed. Errors raised by the wallet while signing or broadcasting are not reflected on the order, which stays in `awaiting-payment`; handle them in your app, then refetch the order.
## Webhooks
Instead of polling, subscribe to order events so Crossmint notifies your backend as the order progresses. Configure an endpoint in the Crossmint console and subscribe to events using the [Webhooks guide](/payments/advanced/webhooks).
The events relevant to the standard offramp progression are:
| Event | Fires when |
| :- | :- |
| `orders.payment.succeeded` | The on-chain stablecoin deposit is matched to the order |
| `orders.delivery.initiated` | The fiat payout has been initiated |
| `orders.delivery.completed` | The fiat payout has completed |
| `orders.delivery.failed` | The payout could not be completed |
Use these events to drive your UI updates. For example, show the user "payout on the way" when you receive `orders.delivery.initiated` and "funds sent" when you receive `orders.delivery.completed`.
## Troubleshooting
An offramp order spans two independent systems: an on-chain stablecoin deposit and a fiat payout over banking rails. Most orders complete on their own, but a few situations can interrupt the standard flow, most often a quote that expires before the deposit arrives or a payout the bank declines. Both are expected and recoverable. Expand a case below to see how to detect and resolve it.
`quote.expiresAt` is the deadline for the deposit to be confirmed on-chain and matched to the order, not merely broadcast. If no deposit has been matched by then, the order returns to the `quote` phase.
```json theme={null}
{
"phase": "quote",
"quote": { "status": "expired" },
"payment": { "status": "requires-quote" },
"lineItems": [{ "delivery": { "status": "awaiting-payment" } }]
}
```
The exchange rate is stale. Create a new order to get a fresh quote. Expiry alone does not prove that nothing was sent: if your app already broadcast a transaction, track it by transaction ID rather than asking the user to resend.
If a deposit is detected before expiry (`payment.status: "in-progress"`) but only confirms afterwards, Crossmint refunds it to the payer wallet instead of paying out at the stale rate. The order completes with `payment.refunded` set, `lineItems[].delivery.status: "failed"`, `failureReason.code: "payment-refunded"`, and an `orders.delivery.failed` webhook. Create a new order to retry.
If the bank rejects the payout after the stablecoin deposit has been received (for example, invalid account details), the order moves to `completed` with a failed delivery. The buyer deposit is automatically refunded.
```json theme={null}
{
"phase": "completed",
"payment": {
"status": "completed",
"refunded": { "amount": "1", "currency": "usdc" }
},
"lineItems": [
{
"delivery": {
"status": "failed",
"failureReason": {
"code": "invalid-account",
"message": "The payout couldn't be delivered because the destination bank account is invalid or closed. Please check your bank details and try again."
}
}
}
]
}
```
Check `payment.refunded` to confirm the refund details. Listen for `orders.delivery.failed` so you can surface the issue in your app and prompt the user to try again with corrected bank details.
## Next Steps
Build the full flow
Move from staging to live payouts
# Get an Offramp Quote
Source: https://docs.crossmint.com/offramp/guides/preview-a-quote
Estimate the fiat payout and fees before placing an offramp order
Use a draft quote to show users what to expect before converting stablecoins to fiat. You can start from either side of the conversion. For example, enter 300 USDC to see the estimated payout in Mexican pesos, or enter 4,500 MXN to see how much USDC the user would need to send. The quote also includes the applicable fees.
Crossmint does not store a draft, and a draft cannot be paid or turned into an order. Display the quote, then discard the response. When the user proceeds, create a new order.
## Request a Quote
Call [Create Order](/offramp/api-reference/create-order) with `state: "draft"`. The rest of the body is the same as a real order. Use `exact-in` to fix the stablecoin the user sends, or `exact-out` to fix the fiat they receive (see [Send or Receive an Exact Amount](/offramp/guides/send-or-receive-an-exact-amount)). Both code samples pay out in MXN.
The user sends exactly 300 USDC and the quote estimates how many MXN they receive after fees.
```bash cURL {5,18-19} theme={null}
curl -X POST 'https://staging.crossmint.com/api/2022-06-09/orders' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"state": "draft",
"recipient": {
"paymentMethodId": ""
},
"payment": {
"method": "base-sepolia",
"currency": "usdc",
"payerAddress": "",
"receiptEmail": "alice@example.com"
},
"lineItems": {
"currencyLocator": "fiat:mxn",
"executionParameters": {
"mode": "exact-in",
"amount": "300"
}
}
}'
```
The user wants to receive exactly 4,500 MXN and the quote calculates how much USDC they must send, fees included.
```bash cURL {5,18-19} theme={null}
curl -X POST 'https://staging.crossmint.com/api/2022-06-09/orders' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"state": "draft",
"recipient": {
"paymentMethodId": ""
},
"payment": {
"method": "base-sepolia",
"currency": "usdc",
"payerAddress": "",
"receiptEmail": "alice@example.com"
},
"lineItems": {
"currencyLocator": "fiat:mxn",
"executionParameters": {
"mode": "exact-out",
"amount": "4500"
}
}
}'
```
## Read the Quote
Response excerpts for each mode. The amounts are illustrative.
The user sends 300 USDC, pays 0.24 USDC in fees, and the remaining 299.76 USDC converts into an estimated 5,137.89 MXN (`quantity`).
```json Draft response (excerpt) {17,26,39,48} theme={null}
{
"order": {
"orderId": "",
"phase": "payment",
"lineItems": [
{
"metadata": {
"name": "MXN",
"description": "Mexican Peso",
"imageUrl": "https://www.crossmint.com/assets/ui/flags/mx.svg"
},
"quote": {
"status": "valid",
"charges": {
"fees": {
"type": "exact",
"amount": "0.24",
"currency": "usdc"
}
},
"totalPrice": {
"amount": "300",
"currency": "usdc"
}
},
"quantity": 5137.89,
"delivery": {
"status": "awaiting-payment",
"recipient": {
"locator": "paymentMethodId:",
"paymentMethodId": ""
}
},
"executionMode": "exact-in",
"callData": {
"mode": "exact-in",
"amount": "300",
"outFiatCurrency": "mxn",
"effectiveAmount": "299.76"
}
}
],
"quote": {
"status": "valid",
"quotedAt": "2026-09-15T19:16:57.608Z",
"expiresAt": "2026-09-15T19:26:57.608Z",
"totalPrice": {
"amount": "300",
"currency": "usdc"
}
},
"payment": {
"status": "requires-crypto-payer-address",
"method": "base-sepolia",
"currency": "usdc"
}
}
}
```
The user receives exactly 4,500 MXN, which costs 262.54 USDC to convert; 0.21 USDC in fees is added on top, so they send 262.75 USDC (`totalPrice`). Amounts are rounded here; the response keeps full precision.
```json Draft response (excerpt) {17,26,39,48} theme={null}
{
"order": {
"orderId": "",
"phase": "payment",
"lineItems": [
{
"metadata": {
"name": "MXN",
"description": "Mexican Peso",
"imageUrl": "https://www.crossmint.com/assets/ui/flags/mx.svg"
},
"quote": {
"status": "valid",
"charges": {
"fees": {
"type": "exact",
"amount": "0.210035",
"currency": "usdc"
}
},
"totalPrice": {
"amount": "262.753792",
"currency": "usdc"
}
},
"quantity": 4500,
"delivery": {
"status": "awaiting-payment",
"recipient": {
"locator": "paymentMethodId:",
"paymentMethodId": ""
}
},
"executionMode": "exact-out",
"callData": {
"mode": "exact-out",
"amount": "4500",
"outFiatCurrency": "mxn",
"effectiveAmount": "262.543757"
}
}
],
"quote": {
"status": "valid",
"quotedAt": "2026-09-15T21:13:28.476Z",
"expiresAt": "2026-09-15T21:23:28.476Z",
"totalPrice": {
"amount": "262.753792",
"currency": "usdc"
}
},
"payment": {
"status": "requires-crypto-payer-address",
"method": "base-sepolia",
"currency": "usdc"
}
}
}
```
## Display the Quote
Use these fields to build the quote summary:
| Field | Show the user |
| - | - |
| `quote.totalPrice.amount` | Stablecoin to send, including Crossmint fees |
| `lineItems[0].quote.charges.fees.amount` | Crossmint fees, included in the total |
| `lineItems[0].callData.effectiveAmount` | Stablecoin actually converted, after fees |
| `lineItems[0].quantity` | Estimated fiat payout, expected in the destination account |
## Place the Order
Once the user is ready to continue, discard the draft and send the request again without `state: "draft"`. The draft was only a preview and cannot be reused.
The new order includes a fresh quote and instructions for sending the funds. If the amounts have changed, show the updated quote to the user before continuing.
## Next Steps
Status, webhooks, and failures
Every request and response field
# Send or Receive an Exact Amount
Source: https://docs.crossmint.com/offramp/guides/send-or-receive-an-exact-amount
Choose an exact stablecoin amount to send or an exact fiat amount to receive
Use `exact-in` when you know how much stablecoin you want to send. Use `exact-out` when you know how much fiat should reach the bank account. Crossmint calculates the other amount and includes both in the quote.
**Exact-in**
**Exact-out**
## Before You Start
Both modes use [Create Order](/offramp/api-reference/create-order). Set `payment.currency` to the stablecoin being sent, such as `"usdc"`, and `lineItems.currencyLocator` to the payout currency, such as `"fiat:mxn"`.
Choose the mode with `lineItems.executionParameters.mode`. Pass `executionParameters.amount` as a decimal string, such as `"300"` or `"4500.50"`. The mode determines its currency and allowed precision:
* `exact-in`: the stablecoin amount to send. Use up to the token's supported decimal places, which is 6 for USDC.
* `exact-out`: the fiat amount to receive. Use up to the currency's minor unit, which is 2 decimal places for MXN, USD, EUR, and COP. For MXN, `"4500.50"` is accepted; `"4500.505"` returns a 400 error.
## Exact-in: Send an Exact Amount
To cash out 300 USDC into MXN, use `exact-in` with an amount of `"300"`:
```json {11,12} theme={null}
{
"recipient": { "paymentMethodId": "" },
"payment": {
"method": "solana",
"currency": "usdc",
"payerAddress": ""
},
"lineItems": {
"currencyLocator": "fiat:mxn",
"executionParameters": {
"mode": "exact-in",
"amount": "300"
}
}
}
```
In this example, the quote returns:
```json Quote excerpt {6,9,13} theme={null}
{
"lineItems": [
{
"quote": {
"charges": {
"fees": { "amount": "0.24", "currency": "usdc" }
}
},
"quantity": 5061
}
],
"quote": {
"totalPrice": { "amount": "300", "currency": "usdc" }
}
}
```
The payer sends 300 USDC. After the 0.24 USDC fee, 299.76 USDC is converted into 5,061 MXN for the bank account. Fiat payouts are rounded to the currency's minor unit, which is two decimal places for MXN.
## Exact-out: Receive an Exact Amount
To have the bank account receive exactly 4,500 MXN, use `exact-out` with an amount of `"4500"`:
```json {11,12} theme={null}
{
"recipient": { "paymentMethodId": "" },
"payment": {
"method": "solana",
"currency": "usdc",
"payerAddress": ""
},
"lineItems": {
"currencyLocator": "fiat:mxn",
"executionParameters": {
"mode": "exact-out",
"amount": "4500"
}
}
}
```
The quote tells you how much USDC to deposit:
```json Quote excerpt {6,9,13} theme={null}
{
"lineItems": [
{
"quote": {
"charges": {
"fees": { "amount": "0.2128", "currency": "usdc" }
}
},
"quantity": 4500
}
],
"quote": {
"totalPrice": { "amount": "266.2128", "currency": "usdc" }
}
}
```
The payout requires 266 USDC, plus a 0.2128 USDC fee. The payer deposits 266.2128 USDC, and the bank account receives 4,500 MXN.
Use `quote.totalPrice.amount` to fund the order. In this mode, the `amount` in your request is the fiat payout.
## Reading the Quote
Use these three fields to show the user how much they will send, how much they will receive, and the fee. Paths start inside the `order` object returned by Create Order and [Get Order](/offramp/api-reference/get-order).
* `quote.totalPrice.amount`: stablecoin to send, including the Crossmint fee.
* `lineItems[].quantity`: fiat the bank account will receive, in the payout currency.
* `lineItems[].quote.charges.fees.amount`: the Crossmint fee, in the stablecoin.
`quantity` is a JSON number; the other amounts are strings. Use `quote.totalPrice.amount` for the deposit. The per-unit price in `charges.unit.amount` is rounded, so multiplying it by `quantity` may give a different total.
The prepared transaction represents the deposit in token base units. See the [REST API quickstart](/offramp/quickstarts/rest-api) for how to send it, and the [Create Order reference](/offramp/api-reference/create-order) for the full schema.
## Fees
With `exact-in`, the fee comes out of the deposit. With `exact-out`, it is added to the stablecoin needed for the payout. In both cases, the total deposit already includes the Crossmint fee.
Network gas is separate from the quoted deposit. The payer wallet pays it in the chain's native token unless the transaction is sponsored.
## Rate Lock
The quote locks the exchange rate for about 10 minutes. To keep the quoted amounts, the deposit must be confirmed on-chain before `quote.expiresAt`. If no deposit arrives in time, the order expires and you create a new order to get a fresh quote. If your flow can take longer, such as waiting for user authorization or KYC, create the order only when the payer is ready to deposit.
See [Manage Orders](/offramp/guides/manage-orders#troubleshooting) for expired quotes, including cases where a deposit is already in flight.
## See Also
Request and response fields for creating an offramp order.
Track payouts and handle expired quotes or payment issues.
# Offramp Orders on Solana
Source: https://docs.crossmint.com/offramp/guides/sign-deposit-solana
Sign and broadcast the deposit transaction for an existing Solana offramp order.
Every offramp order returns a prepared deposit transaction that the payer must sign and broadcast before the order leaves the payment phase. On Solana, that transaction has its own format and a blockhash that can expire, so it can't be sent with the EVM steps in the quickstart. This guide shows how to sign and broadcast it — from a Crossmint wallet or your own Solana keypair — so Crossmint matches the deposit to the order and the payout can proceed.
For the complete offramp lifecycle, see the [REST API Quickstart](/offramp/quickstarts/rest-api).
## Prerequisites
You need:
* **Offramp order:** created with `payment.method: "solana"` via [Create Order](/offramp/api-reference/create-order).
* **Payer wallet:** a Crossmint wallet on `chain: "solana"` with a configured signer, or an external Solana wallet whose public key matches the order's `payment.payerAddress`.
* **Funds:** enough USDC to pay the order and enough SOL to cover the network fee.
## Pay the Order
The prepared blockhash may expire before the user approves the transaction. To avoid signing an expired transaction, fetch a current blockhash from your server and rebuild the transaction before sending it to the wallet.
```tsx theme={null} theme={null}
import { useWallet, SolanaWallet } from "@crossmint/client-sdk-react-ui";
import { Transaction } from "@solana/web3.js";
import bs58 from "bs58";
type OfframpOrder = {
orderId: string;
payment: {
preparation: {
serializedTransaction: string;
};
};
};
export function PayOfframpOrder({ order }: { order: OfframpOrder }) {
const { wallet } = useWallet();
async function pay() {
if (wallet == null) {
throw new Error("Wallet not ready");
}
const prepared = Transaction.from(
bs58.decode(order.payment.preparation.serializedTransaction),
);
const blockhash = await fetch("/api/solana-blockhash").then(
(response) => response.text(),
);
const transaction = new Transaction({
feePayer: prepared.feePayer,
recentBlockhash: blockhash,
});
transaction.add(...prepared.instructions);
const { hash } = await SolanaWallet.from(wallet).sendTransaction({
serializedTransaction: bs58.encode(
transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
}),
),
});
console.log(`https://explorer.solana.com/tx/${hash}`, order.orderId);
}
return ;
}
```
This copies every instruction from the prepared transaction while replacing only its blockhash.
See [Custom Solana Transactions](/wallets/guides/send-transaction-solana) for wallet and signer setup.
Use this flow when you control the payer wallet's keypair. The example verifies the payer, replaces the expiring blockhash, signs the transaction, and broadcasts it through your Solana RPC endpoint. `order` is the response from [Create Order](/offramp/api-reference/create-order) or [Get Order](/offramp/api-reference/get-order).
```typescript theme={null} theme={null}
import {
Connection,
Keypair,
SendTransactionError,
Transaction,
} from "@solana/web3.js";
import bs58 from "bs58";
const connection = new Connection(
process.env.SOLANA_RPC_URL!,
"confirmed",
);
const payer = Keypair.fromSecretKey(
bs58.decode(process.env.SOLANA_PRIVATE_KEY!),
);
type OfframpOrder = {
orderId: string;
payment: {
preparation: {
serializedTransaction: string;
};
};
};
export async function payOfframpOrder(order: OfframpOrder) {
const prepared = Transaction.from(
bs58.decode(order.payment.preparation.serializedTransaction),
);
if (prepared.feePayer?.equals(payer.publicKey) !== true) {
throw new Error(
`Order expects payer ${prepared.feePayer?.toBase58()}`,
);
}
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash("confirmed");
const transaction = new Transaction({
feePayer: prepared.feePayer,
recentBlockhash: blockhash,
});
transaction.add(...prepared.instructions);
transaction.partialSign(payer);
let signature: string;
try {
signature = await connection.sendRawTransaction(
transaction.serialize(),
);
} catch (error) {
if (error instanceof SendTransactionError) {
console.error(await error.getLogs(connection));
}
throw error;
}
await connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
"confirmed",
);
console.log(
`https://explorer.solana.com/tx/${signature}`,
order.orderId,
);
}
```
`serializedTransaction` is base58-encoded. If the transaction fails to send, `SendTransactionError.getLogs()` returns the Solana program logs.
## Why the Blockhash Must Be Replaced
Solana rejects transactions after their blockhash expires. The prepared transaction's blockhash expires roughly a minute after Crossmint builds it, well before the order's `quote.expiresAt`.
For this reason, do not treat `quote.expiresAt` as the transaction-signing deadline. Fetch a current blockhash immediately before signing.
Replacing the blockhash does not require a new order or quote. Copy the original transaction's fee payer and instructions into a new transaction, then sign it against the current blockhash.
## Confirm the Payout
A confirmed Solana transaction proves that the USDC left the payer's wallet. It does not prove that the fiat payout reached the recipient.
Use [Get Order](/offramp/api-reference/get-order) or subscribe to the webhooks described in [Manage Orders](/offramp/guides/manage-orders).
## Next Steps
Track order phases, delivery statuses, and webhooks.
Wallet and signer setup per platform.
# Overview
Source: https://docs.crossmint.com/offramp/overview
Allow your users to convert stablecoins to fiat currency
Crossmint Offramp lets your users convert stablecoins into fiat and withdraw funds directly to their bank account through a fully white-labeled, native experience inside your app.
The product is delivered through a single API that handles compliance, liquidity, and bank payouts, so you can add cash-out without managing banking relationships or regulatory infrastructure. Embedded widget and SDK support is coming soon.
**Offramping from a corporate treasury wallet?** See the [Treasury offramps guide](/stablecoin-orchestration/guides/on-off-ramps/withdrawing-from-company-wallets).
## See It in Action
Try it yourself using the following link.
## Key Features
Seamless in-app experiences
Same API for global access
Import user data (optional)
## Next Steps
## Guides
KYC Data Imports
Manage Payment Methods
Track order status
## FAQs
Integrating the product on staging is fully self-serve, simply get an API key on the staging console and follow the docs.
Production access requires a signed Order Form and completed KYB verification. Contact sales to get started. See the [account verification documentation](/introduction/platform/account-verification) for details.
You can monitor purchases onchain or using webhooks. Sales dashboards, logs, and advanced analytics are also
available in the console.
# Payment Schemes
Source: https://docs.crossmint.com/offramp/payment-methods
Supported payout rails and geographies for Crossmint Offramp
Crossmint Offramp lets your users convert USDC into fiat and withdraw it to bank accounts across the US, Europe, and Latin America. Here is where each payout rail stands today, how fast funds arrive, and any limits to keep in mind.
Availability in this table describes the bank account, not the payer. At order time, Crossmint can refuse SEPA and SEPA Instant for a payer whose verified country of residence is outside the SEPA area, even when the payment method lists them as supported.
| Rail/Scheme | Availability | Settlement | Processing Window (Working Days) | Transaction Limits |
| :- | :- | :- | :- | :- |
| 🇺🇸 **ACH** (USD) | Available | 1 business day | GMT 01:45pm to 08:00pm | \$50,000 per transaction |
| 🇺🇸 **Same-day ACH** (USD) | Available | Same day | GMT 01:45pm to 08:00pm | \$50,000 per transaction |
| 🇺🇸 **RTP** (USD) | Available | Instant | 24/7 | \$1,000,000 per transaction |
| 🇺🇸 **Wire** (USD) | Available | Same day | GMT 01:45pm to 08:00pm | No limit |
| 🇪🇺 **SEPA Instant** (EUR) | Available | Instant | 24/7 | €500,000 per transaction |
| 🇪🇺 **SEPA** (EUR) | Available | 1 to 2 business days | GMT 07:00am to 02:00pm | No limit |
| 🇲🇽 **SPEI** (MXN) | Available | Instant | | |
| 🇨🇴 **Bre-B** (COP) | Available | Instant | 24/7 | COP 12,110,000 (about USD 3,600) per transaction |
| 🇨🇴 **Bank Transfer** (COP) | Available | Minutes to 36 hours (up to 72 hours on weekends and holidays) | Depends on the receiving bank ([see below](#colombia-bank-transfer)) | No limit |
| 🇪🇺 **SWIFT** (EUR) | Q4 | 1 to 5 business days | GMT 07:00am to 02:00pm | No limit |
| 🇬🇧 **Faster Payments** (GBP) | Q4 | Minutes to hours | 24/7 | £1,000,000 per transaction |
| 🇧🇷 **Pix** (BRL) | Q4 | Instant | | |
| Additional rails available upon request | Contact sales | | | |
## Processing Windows by Timezone
The settlement times above apply only when you submit the order within the processing window.
### US (ACH, Same-day ACH, Wire)
| Timezone | Winter (Standard) | Summer (Daylight) |
| :- | :- | :- |
| PT | 05:45am to 12:00pm PST | 06:45am to 01:00pm PDT |
| ET | 08:45am to 03:00pm EST | 09:45am to 04:00pm EDT |
### Europe and UK (SEPA, SWIFT)
| Timezone | Winter (Standard) | Summer (Daylight) |
| :- | :- | :- |
| PT | 11:00pm to 06:00am PST | 12:00am to 07:00am PDT |
| ET | 02:00am to 09:00am EST | 03:00am to 10:00am EDT |
### Colombia (Bank Transfer)
Settlement time depends on the receiving bank. All times are Colombia time (COT), which is GMT-5 all year. COT matches ET in winter and is 1 hour behind ET in summer.
| Receiving bank | Receiving window | Typical settlement |
| :- | :- | :- |
| Bancolombia | 24/7 | Most payouts within 30 minutes |
| Nequi | 24/7 | Most payouts within 5 minutes |
| Coltefinanciera | 24/7 | Most payouts within 5 minutes |
| Banco de Bogotá | Monday to Friday, 06:00 to 20:30 (payouts submitted 20:30 to 03:45 are processed at 04:00) | Most payouts within 45 minutes |
| Coopcentral | Monday to Friday, 08:00 to 17:30 | Most payouts within 30 minutes |
| BBVA | Monday to Friday, 06:00 to 19:00 | Most payouts within 60 minutes |
| Banco de Occidente | Monday to Friday, 08:00 to 21:00 | Most payouts within 60 minutes |
| Banco Popular, Banco AV Villas | Monday to Friday, 08:00 to 17:30 | Most payouts within 2 hours |
| Other banks | Business days, 08:30 to 17:00 | Up to 36 hours (72 hours on weekends and holidays) |
If you submit an order outside the receiving bank's window, the bank processes the payout when that window next opens. The typical settlement time above counts from then.
## Next Steps
Build your first offramp end to end
How verification works for offramp
# REST API Quickstart
Source: https://docs.crossmint.com/offramp/quickstarts/rest-api
Allow users to offramp stablecoins to fiat
In this guide, you will run a full USDC to USD offramp in the Staging environment. You will:
* Confirm Offramp eligibility
* Save the user's payout method
* Set up the wallet the user pays from
* Create an Offramp order and read its deposit instructions
* Broadcast the deposit and track the order to completion
## Prerequisites
* A **server-side API key** with the `orders` and `payment-methods` scopes (Crossmint console).
* A user **KYC-verified for Offramp**. To verify a user, follow [Import User KYC Data](/offramp/guides/import-user-kyc-data) or the [Identity quickstart](/identity/quickstart). This quickstart uses `userId:` in examples, but the API accepts any supported `userLocator`.
* **USDC** to cash out. On staging, get testnet USDC from the Circle faucet.
* **Native gas tokens** in the sender wallet to broadcast the deposit transaction (e.g., ETH on Base, SOL on Solana). This does not apply if the wallet sponsors gas.
Offramp uses Crossmint's user KYC API. Contact sales to enable it for your project.
Before creating an order, confirm that the user is verified for Offramp. Verification is asynchronous. Poll the user's status with the Get Identity Verification endpoint until the `offramp` eligibility reads `verified`:
```bash theme={null}
curl -X GET 'https://staging.crossmint.com/api/2025-06-09/users//identity-verification' \
-H 'X-API-KEY: '
```
`` is the Crossmint user locator for the verified user, for example `userId:` or `email:`. See the Get Identity Verification API reference for all supported locator formats.
```json theme={null}
{
"eligibility": [
{ "type": "onramp", "status": "verified" },
{ "type": "onramp-light", "status": "verified" },
{ "type": "offramp", "status": "verified" },
{ "type": "offramp-light", "status": "verified" },
{ "type": "regulated-transfer", "status": "verified" }
]
}
```
Save the user's payout destination as a reusable payment method. The example below saves a US bank account. Sensitive fields are saved securely.
See the Create Payment Method API reference for the full request schema and supported payment method types.
Use a bank account number that is unique to your integration. A sample account number shared across integrations can be disabled upstream, after which this request fails for everyone using it.
```bash theme={null}
curl -X POST 'https://vault.staging.crossmint.com/api/unstable/payment-methods' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"type": "bank-account-us",
"userLocator": "",
"bankAccount": {
"accountNumber": "",
"routingNumber": "071000013",
"accountType": "checking",
"bankName": "Chase",
"bankAddress": {
"line1": "420 Montgomery St",
"city": "San Francisco",
"stateOrRegion": "CA",
"postalCode": "94104",
"country": "US"
},
"currency": "usd",
"country": "US",
"entityType": "individual",
"billing": {
"name": "Alice Smith",
"phone": "+13055551234",
"address": {
"line1": "701 S Miami Ave",
"city": "Miami",
"stateOrRegion": "FL",
"postalCode": "33156",
"country": "US"
}
}
}
}'
```
The response returns the saved payment method. Banks expose a masked `accountSuffix` (not `last4`). Save the `paymentMethodId` for the order step.
```json theme={null}
{
"paymentMethodId": "d5e5c48a-7dec-44b1-8cf4-4615bf00c8fc",
"createdAt": "2026-06-17T21:25:56.542Z",
"updatedAt": "2026-06-17T21:25:56.542Z",
"type": "bank-account-us",
"bankAccount": {
"billing": {
"name": "Alice Smith",
"phone": "+13055551234",
"address": {
"line1": "701 S Miami Ave",
"city": "Miami",
"stateOrRegion": "FL",
"postalCode": "33156",
"country": "US"
}
},
"bankName": "Chase",
"accountSuffix": "",
"currency": "usd",
"country": "US",
"entityType": "individual",
"bankAddress": {
"line1": "420 Montgomery St",
"city": "San Francisco",
"stateOrRegion": "CA",
"postalCode": "94104",
"country": "US"
},
"routingNumber": "071000013",
"accountType": "checking"
}
}
```
For listing, updating, and removing saved accounts, see [Manage Payment Methods](/offramp/guides/collect-bank-accounts).
Choose the wallet the user will send USDC from. The payer wallet must belong to the same verified user who owns the payout method.
Create a wallet owned by the verified user, with a `server` signer so your backend can sign. Ownership links the wallet to the user, so there is no separate linking step, and deposits are gasless.
```typescript theme={null}
import { createCrossmint, CrossmintWallets } from "@crossmint/wallets-sdk";
const wallets = CrossmintWallets.from(
createCrossmint({ apiKey: process.env.CROSSMINT_SERVER_API_KEY })
);
const wallet = await wallets.createWallet({
chain: "base-sepolia",
owner: "userId:", // the user you verified
recoveryMethods: [{ type: "server", secret: process.env.CROSSMINT_SIGNER_SECRET }], // server recovery method
alias: "offramp-", // idempotent per (owner, alias)
});
```
* **`owner`** ties the wallet to the verified user via their locator (e.g., `userId:`). This ownership means no separate linking step is needed.
* **`recoveryMethods`** sets a `server` signer as the recovery method, derived from your `CROSSMINT_SIGNER_SECRET` — a secret you generate and keep on your server (the SDK derives the signing key from it locally). See [Server Signer](/wallets/guides/signers/server-signer) to create one.
* **`alias`** is an optional label that makes the call idempotent, so re-running it with the same owner and alias returns the same wallet.
For all wallet options, see [Create a Wallet](/wallets/guides/create-wallet). Then fund `wallet.address` with USDC and use it as the payer. No native gas is needed.
Link the user's own wallet to the verified user. Make sure it holds USDC plus a little native gas for the deposit.
```bash theme={null}
curl -X PUT 'https://staging.crossmint.com/api/2025-06-09/users//linked-wallets/' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{ "chain": "base-sepolia" }'
```
```json theme={null}
{
"address": "0xAa266270cf90FEEA9760617d9c7B5083e9f08984",
"chain": "base-sepolia",
"type": "external-wallet",
"ownership": {
"verified": false,
"verificationChallenge": "crossmint.com wants you to sign in with your blockchain account..."
}
}
```
Each wallet links to one user, so use a separate wallet per user. The response returns an ownership challenge; on staging you do not need to sign it to proceed.
Create the order with the amount of USDC to cash out (`exact-in`), the chain you will pay from, and the saved bank account as the recipient. Use the payer wallet from the previous step as `payerAddress`.
```bash theme={null}
curl -X POST 'https://staging.crossmint.com/api/2022-06-09/orders' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"recipient": {
"paymentMethodId": ""
},
"payment": {
"method": "base-sepolia",
"currency": "usdc",
"payerAddress": "",
"receiptEmail": "alice@example.com"
},
"lineItems": {
"currencyLocator": "fiat:usd",
"executionParameters": {
"mode": "exact-in",
"amount": "1"
}
}
}'
```
The order comes back in the `payment` phase. Its **deposit instructions** live under `order.payment.preparation`: a ready to broadcast `serializedTransaction` (a USDC transfer that already carries the matching memo in its calldata) and the same memo in `transactionParameters.memo`. Keep the `order.orderId`.
To pin a specific payout rail, pass `recipient.rail` (for example, `"rail": "rtp"`) alongside `paymentMethodId`. The value must be one of the payment method's `bankAccount.rails.supported` entries, or the order fails with a `rail-not-supported` error whose message includes the current status and supported list. Omit it to use the account's default rail. Once the order routes, it snapshots the chosen rail on `lineItems[].delivery.rail`, which never changes afterward. See [Payment Schemes](/offramp/payment-methods) for the current rail catalog.
```json theme={null}
{
"clientSecret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmRlcklkZW50aWZpZXIiOiI0MGY0OTgxMi0xMjNiLTRkMTAtYmRhZS01NjFlNGVkMmI5OTAiLCJjb2xsZWN0aW9uSWQiOiJjMjI0MGQ2Yy00NGM0LTQ1OTMtOGM4Yy1jZDRkYzJiZjE4MzUiLCJpYXQiOjE3ODE3MzI5MzQsImV4cCI6MTc4MTgxOTMzNH0.Na-AHOlge6-pYhpMszvITqe3ry49fk-o66i3gfz9a3I",
"order": {
"orderId": "40f49812-123b-4d10-bdae-561e4ed2b990",
"phase": "payment",
"locale": "en-US",
"lineItems": [
{
"metadata": {
"name": "USD",
"description": "United States Dollar",
"imageUrl": "https://www.crossmint.com/assets/ui/flags/us.svg"
},
"quote": {
"status": "valid",
"charges": {
"unit": { "amount": "1", "currency": "usdc" },
"fees": { "type": "exact", "amount": "0.002", "currency": "usdc" }
},
"totalPrice": { "amount": "1", "currency": "usdc" },
"quantityRange": { "lowerBound": "1", "upperBound": "1" }
},
"delivery": {
"status": "awaiting-payment",
"recipient": {
"locator": "paymentMethodId:d5e5c48a-7dec-44b1-8cf4-4615bf00c8fc",
"paymentMethodId": "d5e5c48a-7dec-44b1-8cf4-4615bf00c8fc"
}
},
"executionMode": "exact-in",
"callData": {
"mode": "exact-in",
"amount": "1",
"outFiatCurrency": "usd",
"effectiveAmount": "0.998"
},
"quantity": 1
}
],
"quote": {
"status": "valid",
"quotedAt": "2026-06-17T21:48:49.059Z",
"expiresAt": "2026-06-17T21:58:49.059Z",
"totalPrice": { "amount": "1", "currency": "usdc" }
},
"payment": {
"status": "awaiting-payment",
"method": "base-sepolia",
"currency": "usdc",
"preparation": {
"chain": "base-sepolia",
"payerAddress": "0xAa266270cf90FEEA9760617d9c7B5083e9f08984",
"serializedTransaction": "0x02f9018e83014a348083171dd9837ec1d582c3be94036cbd53842c5426634e7929541ec2318f3dcf7e80b90164a9059cbb000000000000000000000000bd1af733aed9d9f472a15a4ce13f42097bbc9eaa00000000000000000000000000000000000000000000000000000000000f42400a0a2d2d2d2d2d2d424547494e204d454d4f2d2d2d2d2d2d65794a68624763694f694a49557a49314e694973496e523563434936496b705856434a392e65794a756232356a5a534936496a4d774d57566d5a5467344c5755324d6a49744e444d30595330344e4459324c5467774e57517a4e5751345a54466a59694973496d39795a4756795357526c626e52705a6d6c6c63694936496a51775a6a51354f4445794c5445794d3249744e4751784d4331695a47466c4c5455324d5755305a575179596a6b354d434973496d6c68644349364d5463344d54637a4d6a6b794f58302e5a6f6b68766e30626e796a4c734f70754a797a5131666e73465a486b4d36456d3965724c7a6678576443492d2d2d2d2d2d454e44204d454d4f2d2d2d2d2d2dc0",
"transactionParameters": {
"amount": "1000000",
"memo": "------BEGIN MEMO------eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6IjMwMWVmZTg4LWU2MjItNDM0YS04NDY2LTgwNWQzNWQ4ZTFjYiIsIm9yZGVySWRlbnRpZmllciI6IjQwZjQ5ODEyLTEyM2ItNGQxMC1iZGFlLTU2MWU0ZWQyYjk5MCIsImlhdCI6MTc4MTczMjkyOX0.Zokhvn0bnyjLsOpuJyzQ1fnsFZHkM6Em9erLzfxWdCI------END MEMO------"
}
},
"receiptEmail": "alice@example.com"
},
"legal": {
"shownBy": "crossmint",
"requirements": [
{ "display": "show-checkbox", "type": "crossmint-terms-of-service", "url": "https://staging.crossmint.com/legal/crossmint-terms-of-service/TRGUSAALLALLALL" },
{ "display": "show-text", "type": "crossmint-privacy-policy", "url": "https://staging.crossmint.com/legal/crossmint-privacy-policy/ALLALLALLALLALL" }
]
}
}
}
```
The order's `payment.preparation.serializedTransaction` is a prepared USDC transfer that already carries the order's memo in its calldata. Sign and broadcast it from the payer wallet. Crossmint matches the deposit to the order by that memo automatically, so no further call is needed. The transfer moves `quote.totalPrice.amount` expressed in token base units, which is what `transactionParameters.amount` shows (`1` USDC is `1000000` at 6 decimals); the payer wallet also needs native token for gas unless the transaction is sponsored.
Sign and send it from the wallet you created, using its `server` signer (a gasless transaction):
```typescript theme={null}
import { EVMWallet } from "@crossmint/wallets-sdk";
import { parseTransaction } from "viem";
// `wallet` from the setup step, `order` from the create-order step
await wallet.useSigner({ type: "server", secret: process.env.CROSSMINT_SIGNER_SECRET });
const { to, data } = parseTransaction(order.payment.preparation.serializedTransaction);
await EVMWallet.from(wallet).sendTransaction({ to, value: 0n, data });
```
See [Send a Transaction](/wallets/guides/send-transaction-evm) for signer setup and other platforms. To pay from a Solana wallet instead, see [Offramp Orders on Solana](/offramp/guides/sign-deposit-solana); the `parseTransaction` pattern above does not port to Solana.
Sign and broadcast it with the payer's key using viem:
```typescript theme={null}
import { createWalletClient, http, parseTransaction } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";
const account = privateKeyToAccount(process.env.PAYER_PRIVATE_KEY);
const walletClient = createWalletClient({ account, chain: baseSepolia, transport: http() });
// `order` from the create-order step
const { to, data } = parseTransaction(order.payment.preparation.serializedTransaction);
const hash = await walletClient.sendTransaction({ to, value: 0n, data });
```
Poll the order until the payout completes, or subscribe to webhooks (see [Manage Orders](/offramp/guides/manage-orders)).
```bash theme={null}
curl -X GET 'https://staging.crossmint.com/api/2022-06-09/orders/' \
-H 'X-API-KEY: '
```
`GET` returns the order flat at the top level (no `order` wrapper and no `clientSecret`, unlike the create response). Watch `phase`, which moves from `payment` to `completed`, along with `payment.status` and `lineItems[0].delivery.status`. When it completes, `payment.received` is populated with the matched on-chain transaction.
```json theme={null}
{
"orderId": "40f49812-123b-4d10-bdae-561e4ed2b990",
"phase": "completed",
"locale": "en-US",
"lineItems": [
{
"metadata": {
"name": "USD",
"description": "United States Dollar",
"imageUrl": "https://www.crossmint.com/assets/ui/flags/us.svg"
},
"quote": {
"status": "valid",
"charges": {
"unit": { "amount": "1", "currency": "usdc" },
"fees": { "type": "exact", "amount": "0.002", "currency": "usdc" }
},
"totalPrice": { "amount": "1", "currency": "usdc" },
"quantityRange": { "lowerBound": "1", "upperBound": "1" }
},
"delivery": {
"status": "completed",
"rail": "ach-same-day",
"recipient": {
"locator": "paymentMethodId:d5e5c48a-7dec-44b1-8cf4-4615bf00c8fc",
"paymentMethodId": "d5e5c48a-7dec-44b1-8cf4-4615bf00c8fc"
}
},
"executionMode": "exact-in",
"callData": {
"mode": "exact-in",
"amount": "1",
"outFiatCurrency": "usd",
"effectiveAmount": "0.998"
},
"quantity": 1
}
],
"quote": {
"status": "valid",
"quotedAt": "2026-06-17T21:48:49.059Z",
"expiresAt": "2026-06-17T21:58:49.059Z",
"totalPrice": { "amount": "1", "currency": "usdc" }
},
"payment": {
"status": "completed",
"method": "base-sepolia",
"currency": "usdc",
"preparation": {
"chain": "base-sepolia",
"payerAddress": "0xAa266270cf90FEEA9760617d9c7B5083e9f08984",
"serializedTransaction": "0x02f9018e83014a348083171dd9837ec1d582c3be94036cbd53842c5426634e7929541ec2318f3dcf7e80b90164a9059cbb000000000000000000000000bd1af733aed9d9f472a15a4ce13f42097bbc9eaa00000000000000000000000000000000000000000000000000000000000f42400a0a2d2d2d2d2d2d424547494e204d454d4f2d2d2d2d2d2d65794a68624763694f694a49557a49314e694973496e523563434936496b705856434a392e65794a756232356a5a534936496a4d774d57566d5a5467344c5755324d6a49744e444d30595330344e4459324c5467774e57517a4e5751345a54466a59694973496d39795a4756795357526c626e52705a6d6c6c63694936496a51775a6a51354f4445794c5445794d3249744e4751784d4331695a47466c4c5455324d5755305a575179596a6b354d434973496d6c68644349364d5463344d54637a4d6a6b794f58302e5a6f6b68766e30626e796a4c734f70754a797a5131666e73465a486b4d36456d3965724c7a6678576443492d2d2d2d2d2d454e44204d454d4f2d2d2d2d2d2dc0",
"transactionParameters": {
"amount": "1000000",
"memo": "------BEGIN MEMO------eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6IjMwMWVmZTg4LWU2MjItNDM0YS04NDY2LTgwNWQzNWQ4ZTFjYiIsIm9yZGVySWRlbnRpZmllciI6IjQwZjQ5ODEyLTEyM2ItNGQxMC1iZGFlLTU2MWU0ZWQyYjk5MCIsImlhdCI6MTc4MTczMjkyOX0.Zokhvn0bnyjLsOpuJyzQ1fnsFZHkM6Em9erLzfxWdCI------END MEMO------"
}
},
"received": {
"chain": "base-sepolia",
"txId": "0xc9537a0e4ee4794101514d30418bd20f09cf66c3ce86b82b0be916489a338e85",
"amount": "1",
"currency": "usdc"
},
"receiptEmail": "alice@example.com"
}
}
```
On staging the payout is simulated. To pay out to a bank in production, see [Launch in Production](/offramp/guides/going-to-production).
## Troubleshooting
Offramp's KYC endpoints are turned on per project. Contact sales to enable KYC for yours. The response `project not configured to support the users KYC endpoints` is the signal to reach out.
The user and KYC endpoints expect a `userId:` locator, for example `userId:alice-123`. The `email:` and other locator formats apply to Crossmint auth wallets, so use `userId:` for these endpoints.
Verification is asynchronous. Poll `GET /users/{userId}/identity-verification` until the `offramp` eligibility reads `verified`, then create the order.
Create the payer wallet with `owner: "userId:"`. With no owner it is company-owned, which the order does not accept. A wallet's owner is set once at creation.
After you broadcast the prepared transaction, Crossmint matches it to the order by its memo, so the deposit is already assigned. You do not need to call `POST /orders/{orderId}/payment`.
If `recipient.rail` is not one of the payment method's `bankAccount.rails.supported` values, order creation fails with a `rail-not-supported` error. The error message text includes the account's current rail status and supported list; refetch the payment method to read those values as structured fields.
## Next Steps
Status, webhooks, and failures
The end-to-end offramp flow
# Create Order
Source: https://docs.crossmint.com/onramp/api-reference/create-order
api-reference/_open-api/OnrampAPIs.json POST /2022-06-09/orders
Creates an onramp order to buy tokens with a card. The order returns a payment intent that the buyer completes to receive tokens in the specified wallet.
# Payment Error Codes
Source: https://docs.crossmint.com/onramp/api-reference/error-codes
Reference of the normalized error categories and codes returned in an Onramp order's payment.failureReason
## See Also
REST endpoint that returns an Onramp order, including `payment.failureReason`
Payment methods available in Onramp and how buyers use them
What to collect from a buyer before escalating a checkout issue to Crossmint
Receive order updates, including declines, server-side
# Get Order
Source: https://docs.crossmint.com/onramp/api-reference/get-order
api-reference/_open-api/OnrampAPIs.json GET /2022-06-09/orders/{orderId}
Retrieves an onramp order by its ID to track its progress through KYC, recipient verification, payment, and delivery.
# List Orders
Source: https://docs.crossmint.com/onramp/api-reference/list-orders
api-reference/_open-api/OnrampAPIs.json GET /2022-06-09/orders
Retrieve a paginated list of onramp orders for your project to track purchases through quote, payment, KYC, and delivery.
# Payment Methods
Source: https://docs.crossmint.com/onramp/concepts/payment-methods
Understanding the payment methods available for onramp
Crossmint Onramp lets users buy stablecoins with the payment methods they already have: debit and credit cards, Apple Pay, and Google Pay. This page explains how each method works, where it is available, and why an onramp transaction is different from a token checkout.
## Supported Payment Methods
| Method | Description | Availability |
| :- | :- | :- |
| **Card** | Debit and credit cards (Visa, Mastercard) | All platforms |
| **Apple Pay** | One-click payment with cards stored in Apple Wallet | Safari on macOS and iOS (web), native iOS apps |
| **Google Pay** | One-click payment with cards stored in Google Wallet | Web browsers, Android apps (React Native, Flutter, Kotlin) |
All three methods are selected by the user inside the same embedded checkout component. You control which methods are offered through a single configuration option (`allowedMethods`), and the component only displays the methods that the user's device and browser actually support. For example, Apple Pay is never shown on an Android device, even when it is enabled.
## How Each Method Works
### Card
The user enters their card details directly in the embedded checkout. Card data is captured in a PCI-compliant environment and never touches your servers. Cards are the baseline method: they work on every platform without additional configuration.
### Apple Pay
Apple Pay wraps the user's stored cards behind Face ID, Touch ID, or a device passcode. Instead of typing card details, the user confirms the payment through the native Apple Pay sheet.
Apple requires every web domain that displays the Apple Pay button to be verified with Apple. Crossmint manages the Apple merchant relationship, so you only need to verify your domain once through the Crossmint Console. Native iOS apps do not require domain verification; the Swift SDK supports Apple Pay automatically.
### Google Pay
Google Pay works the same way for cards stored in Google Wallet: the user confirms the payment through the native Google Pay sheet instead of typing card details.
On the web, Google Pay works without additional configuration. Mobile apps require native configuration (permissions and intent declarations) because the payment sheet is rendered by the operating system, and production Android apps must be approved by Google before Google Pay is shown to real users.
## Why Onramp Is Not a Standard Checkout
A standard card payment in a checkout pays for a good or service; the cardholder receives a product, and the transaction ends there. An onramp transaction is different: the user's fiat is converted into stablecoins that are delivered to a wallet. This is a **regulated transfer** — a money movement that must run on licensed infrastructure.
Because of this, every onramp transaction is routed through Crossmint's regulated infrastructure, which handles:
* **Licensing** — Crossmint operates the required licenses, so you do not need your own.
* **Identity verification** — users complete a lightweight KYC once, valid for up to \$1,000 per day. See [User Onboarding](/onramp/introduction/user-onboarding).
* **AML screening and sanctions checks** — every transaction is screened before the stablecoins are delivered.
* **Chargeback liability** — Crossmint assumes liability for fraudulent disputes.
The payment method only changes how the user funds the transaction. Whether they pay with a card, Apple Pay, or Google Pay, the compliance flow and the on-chain delivery are identical.
The user experience differs mainly in friction: Apple Pay and Google Pay skip manual card entry and typically convert better on mobile devices, while cards remain the universal fallback.
## Choosing Which Methods to Offer
Offering every method is usually the right default: the checkout only shows what the device supports, so enabling all three costs nothing. Restricting methods makes sense when:
* Your app is native-only on one platform and you want to hide methods that will never appear.
* You have a business reason to route users through a specific method.
## See Also
Enable Apple Pay, Google Pay, and cards in your integration
How identity verification works for onramp
# Embed the KYC Step in Your App
Source: https://docs.crossmint.com/onramp/guides/decouple-identity-verification
Render the identity verification step in your own view instead of inside the embedded checkout, for more control over when and where verification happens
By default, when an onramp order requires identity verification (KYC), Crossmint collects it inside the embedded checkout component. If you would like more control over when and where the verification step shows up, you can decouple it: create the order via API, read the verification credentials that Crossmint already returns on the order, and render the step yourself with Crossmint's `CrossmintIdentityVerification` component.
This mostly affects where the verification step lives in your UI. You can surface it as its own screen, a modal, or a step in your flow, rather than having it appear inside the checkout iframe. The underlying flow stays Crossmint's, and the credentials on the order are enough to render it.
Crossmint runs the underlying identity verification provider on your behalf, and your application only ever integrates with Crossmint's component.
If you instead want to run verification with your own UI **and** your own identity verification provider, see the [Identity quickstart](/identity/quickstart) for sharing the resulting KYC data with Crossmint.
## How It Works
Create an onramp order via API. If the buyer needs to verify their identity, it comes back with `payment.status: "requires-kyc"`.
The `requires-kyc` response includes `payment.preparation.kyc`, the verification session Crossmint has prepared for this buyer. Pass it through as it comes; its shape depends on the verification flow of the order.
Pass those credentials to `CrossmintIdentityVerification` and render it wherever you want in your app.
Once the buyer finishes verifying, poll the order until the status moves off `requires-kyc`, then continue with payment as usual.
## Prerequisites
Install the Crossmint React SDK, along with the base package that exports the shared types. No provider SDK is needed, since the verification UI is rendered by Crossmint:
```bash npm theme={null}
npm install @crossmint/client-sdk-react-ui @crossmint/client-sdk-base
```
```bash pnpm theme={null}
pnpm add @crossmint/client-sdk-react-ui @crossmint/client-sdk-base
```
```bash yarn theme={null}
yarn add @crossmint/client-sdk-react-ui @crossmint/client-sdk-base
```
```bash bun theme={null}
bun add @crossmint/client-sdk-react-ui @crossmint/client-sdk-base
```
The `appearance` prop needs `@crossmint/client-sdk-react-ui` 4.10.0 or later.
## Create the Order and Detect the Verification Requirement
Create the order server-side as you normally would (see the [onramp quickstart](/onramp/quickstarts/react)). When the response comes back, inspect `payment.status`. If it is `requires-kyc`, read the credentials from `payment.preparation.kyc`:
```json Example order response theme={null}
{
"orderIdentifier": "123",
// ...
"payment": {
"method": "card",
"currency": "usd",
"status": "requires-kyc",
"preparation": {
"kyc": {
"verificationId": "68e6a9c4f1b2a3d4e5f60718",
"clientSecret": "YOUR_VERIFICATION_CLIENT_SECRET"
}
}
}
}
```
Pass the `payment.preparation.kyc` object straight to the component as it comes, not the whole order.
If you only want to preview what an order would look like, you can pass `state: "draft"` on creation. A draft order is not persisted and starts no identity verification, so its `payment.status` is `draft` and it cannot be polled or paid: create the order for real (omit `state`, or pass `state: "create"`) and use that order's credentials and `orderId` for the verification step and the polling below.
## Render the Verification Step in Your Own View
Instead of letting the checkout collect the verification, render `CrossmintIdentityVerification` yourself with those credentials. You can place it wherever suits your app, such as a dedicated route, a modal, or a step in your onboarding. The component must be rendered inside `CrossmintProvider`:
```tsx components/VerificationStep.tsx theme={null}
"use client";
import { CrossmintIdentityVerification } from "@crossmint/client-sdk-react-ui";
import type { IdentityVerificationAppearance, IdentityVerificationCredentials } from "@crossmint/client-sdk-base";
// The same object you pass to the embedded checkout. See /identity/ui-customization.
const appearance: IdentityVerificationAppearance = {
variables: {
colors: { backgroundPrimary: "#FFFFFF", textPrimary: "#00150D", accent: "#0074D9" },
},
rules: {
PrimaryButton: { colors: { background: "#0074D9", text: "#FFFFFF" } },
},
};
interface VerificationStepProps {
credentials: IdentityVerificationCredentials;
onComplete: () => void;
onCancel: () => void;
}
export function VerificationStep({ credentials, onComplete, onCancel }: VerificationStepProps) {
return (
{/* Your own heading, copy, progress indicator, etc. */}
console.log("Verification UI loaded")}
onComplete={() => onComplete()}
onCancel={() => onCancel()}
onError={({ retriable, reason, message }) => {
console.error("Verification error", reason, message);
if (!retriable) {
// The buyer cannot finish this attempt. Show a terminal state.
}
}}
/>
);
}
```
Since you own the surrounding view, the verification tends to feel more like a native step in your product than something embedded inside the checkout. The component renders a Crossmint-hosted iframe that resizes itself to its content, so your container controls the width and placement, and the height follows the current verification screen. A few rules keep it working:
* **Keep it mounted until `onComplete` or `onCancel`.** The order leaves `requires-kyc` before the flow shows its result screen, so do not unmount the component when the credentials go away.
* **Handle `onCancel`.** The Done button on a blocked screen, and closing the document capture, send it. Without a handler, Done does nothing.
* **Leave 24px around the iframe and no fixed height.** The screens have no outer padding of their own, and a fixed height cuts off the Continue button. See [placing the screens on your page](/identity/ui-customization#place-the-screens-on-your-page).
* **Allow the camera.** The ID document and selfie step uses it. The component already sets `allow="camera"` on the iframe; the browser still asks the user for permission.
### Match Your Theme
`appearance` takes the same object as the embedded checkout's `appearance` prop, so one theme covers both. The KYC screens read light or dark from `variables.colors.backgroundPrimary`. The [identity verification UI customization guide](/identity/ui-customization) lists the settings that apply and the defaults.
### Verification Outcomes
`onComplete` reports one of `verified`, `pending-review`, `pending-manual-review`, `declined`, `expired`, `failed`, or `unknown`. Treat `unknown` as an unresolved outcome rather than a success, and fall back to the order status.
### If you also use the embedded checkout for payment
If the buyer pays through the embedded checkout rather than a fully headless integration, tell the checkout that your application owns the verification step by passing `identityVerificationHandling="external"`. The checkout then renders nothing for verification and keeps polling the order in the background, so it picks the flow back up at the payment step on its own:
```tsx components/OnrampCheckout.tsx theme={null}
"use client";
import { useEffect, useState } from "react";
import {
CrossmintProvider,
CrossmintCheckoutProvider,
CrossmintEmbeddedCheckout,
useIdentityVerificationCredentials,
} from "@crossmint/client-sdk-react-ui";
import type { IdentityVerificationCredentials } from "@crossmint/client-sdk-base";
import { VerificationStep } from "./VerificationStep";
export function OnrampCheckout({ orderId, clientSecret }: { orderId: string; clientSecret: string }) {
return (
);
}
function DecoupledVerification() {
// The checkout emits order updates to this hook, so the credentials arrive
// without a second API call. Undefined until verification is required, and
// undefined again once the order moves on, before the flow shows its result.
const credentials = useIdentityVerificationCredentials();
const [activeCredentials, setActiveCredentials] = useState();
useEffect(() => {
if (credentials != null) {
setActiveCredentials(credentials);
}
}, [credentials]);
if (activeCredentials == null) {
return null;
}
return (
setActiveCredentials(undefined)}
onCancel={() => setActiveCredentials(undefined)}
/>
);
}
```
For an order that does not come from the checkout context, the plain `getIdentityVerificationCredentials(order)` function is also exported from `@crossmint/client-sdk-react-ui`.
Passing `identityVerificationHandling="external"` without rendering `CrossmintIdentityVerification` leaves the buyer with no way to finish the order. The checkout suppresses the verification step and all of its outcome screens, on the assumption that your application shows them instead.
## Poll the Order Until Verification Resolves
The component callbacks fire when the buyer finishes, but the order status is the source of truth. After `onComplete`, poll the order until `payment.status` moves off `requires-kyc`:
```typescript theme={null}
async function waitForVerification(orderId: string, maxAttempts: number = 60): Promise {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const order = await getOrder(orderId); // GET /2022-06-09/orders/{orderId}
const status = order.payment?.status;
if (status !== "requires-kyc") {
return status; // "awaiting-payment", "manual-kyc", "failed-kyc", ...
}
await new Promise((r) => setTimeout(r, 2000));
}
throw new Error("Verification did not resolve within the expected time");
}
```
Verification usually resolves within 15 seconds, so the loop above is generous on purpose: it allows for about two minutes before giving up. Adjust `maxAttempts` or add your own deadline to fit your UX. If you use the embedded checkout with `identityVerificationHandling="external"`, the checkout already polls for you, and `useCrossmintCheckout` returns a fresh order instead.
The resulting status tells you what to do next:
| Status | Meaning | What to do |
| :- | :- | :- |
| `awaiting-payment` | Verification passed and the order is ready to be paid | Continue to the payment step (embedded component or headless API). |
| `pending-kyc-review` | The submission is being reviewed | Show a pending state and keep polling. |
| `manual-kyc` | Verification needs manual review | Show a "Crossmint is reviewing your identity" state. The buyer is notified of the outcome. |
| `failed-kyc` | Verification failed | The buyer cannot proceed with this order. Show a terminal state. |
Because the checkout suppresses its own verification screens when your application owns the step, the pending, review, and rejected states are yours to render as well.
See the [Status Codes](/payments/headless/guides/status-codes) page for the authoritative list of order statuses.
## Continue with Payment
Once the order reaches `awaiting-payment`, continue as you would in the standard flow, rendering the embedded checkout for payment or driving it yourself if you are fully headless. Decoupling verification only affects that step; the rest of the order lifecycle stays the same.
## Next Steps
# Get an Onramp Quote
Source: https://docs.crossmint.com/onramp/guides/get-a-quote
Estimate the stablecoin amount and fees before placing an onramp order
Use a draft quote to show users what to expect before they buy stablecoins with a card. For example, enter 100 USD to see roughly how much USDC the user receives after fees.
Crossmint does not store a draft, and a draft cannot be paid or turned into an order. A draft also starts no identity verification. Display the quote, then discard the response. When the user proceeds, create a new order.
## Request a Quote
Call [Create Order](/onramp/api-reference/create-order) with `state: "draft"`. The rest of the body is the same as a real order. The code sample buys USDC on Base Sepolia with exactly 100 USD.
```bash cURL {5} theme={null}
curl -X POST 'https://staging.crossmint.com/api/2022-06-09/orders' \
-H 'X-API-KEY: ' \
-H 'Content-Type: application/json' \
-d '{
"state": "draft",
"recipient": {
"walletAddress": ""
},
"payment": {
"method": "card",
"receiptEmail": "alice@example.com"
},
"lineItems": [
{
"tokenLocator": "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"executionParameters": {
"mode": "exact-in",
"amount": "100"
}
}
]
}'
```
## Read the Quote
The user is charged 100 USD in total and receives between 96.50 and 97.50 USDC (`quantityRange`). Part of the fee depends on the card the user pays with, so a draft returns a range rather than an exact amount and does not break fees down. The amounts are illustrative.
```json Draft response (excerpt) {21,24-27,46} theme={null}
{
"order": {
"orderId": "",
"phase": "payment",
"lineItems": [
{
"chain": "base-sepolia",
"metadata": {
"name": "USDC",
"description": "USDC Token"
},
"quote": {
"status": "valid",
"charges": {
"unit": {
"amount": "1",
"currency": "usd"
}
},
"totalPrice": {
"amount": "100",
"currency": "usd"
},
"quantityRange": {
"lowerBound": "96.500000",
"upperBound": "97.500000"
}
},
"delivery": {
"status": "awaiting-payment",
"recipient": {
"locator": "base-sepolia:",
"walletAddress": ""
}
},
"executionMode": "exact-in",
"executionParams": {
"mode": "exact-in",
"amount": "100"
}
}
],
"quote": {
"status": "valid",
"quotedAt": "2026-09-22T12:55:01.803Z",
"expiresAt": "2026-09-22T13:25:01.803Z",
"totalPrice": {
"amount": "100",
"currency": "usd"
}
}
}
}
```
## Display the Quote
Use these fields to build the quote summary:
| Field | Show the user |
| - | - |
| `quote.totalPrice.amount` | Fiat charged to the card, all fees included |
| `lineItems[0].quote.quantityRange` | Lowest and highest stablecoin amount the user can expect to receive |
| `lineItems[0].quote.charges.unit.amount` | Fiat price of one unit of the stablecoin |
| `lineItems[0].quote.charges.gas.amount` | Gas paid on the destination chain, when applicable |
| `lineItems[0].quote.charges.networkFee.amount` | Network fee such as Solana account creation, when applicable |
| `quote.expiresAt` | When the quoted price stops being valid |
`gas` and `networkFee` only appear when they are not zero. Once the user enters card details on the real order, the quote resolves to an exact amount: `quantityRange` collapses to a single value and `charges.crossmintFees` lists the Crossmint fee included in the total.
## Place the Order
Once the user is ready to continue, discard the draft and send the request again without `state: "draft"`. The draft was only a preview and cannot be reused.
The new order includes a fresh quote and a `clientSecret` for the embedded checkout. If the amounts have changed, show the updated quote to the user before continuing.
## Next Steps
Create the order and render the checkout
Every request and response field
# Import User KYC Data
Source: https://docs.crossmint.com/onramp/guides/import-user-kyc-data
Use existing KYC data for faster onboarding
If you already verify users with an identity verification (IDV) provider like Sumsub or Persona, you can send that data to Crossmint before creating an onramp order. When the data is complete, users can skip in-checkout identity verification.
If you prefer to have individual users provide their KYC data client-side, Crossmint supports that as well. See the [React Quickstart](/onramp/quickstarts/react) for that approach.
## How It Works
1. Verify the user with your IDV provider
2. Have the user accept Crossmint's Privacy Policy
3. [Register the user's identity data](/identity/quickstart) with Crossmint and trigger verification
4. Create an onramp order to the wallet linked to that user
5. Crossmint runs sanctions, Politically Exposed Persons (PEP), and compliance checks
6. The user completes payment
## Register and Verify the User
User registration, KYC data, document uploads, and identity verification are managed by the identity APIs. Follow the [Identity Quickstart](/identity/quickstart) to:
* Record the user's privacy-policy acceptance (consent must be collected following the requirements in [Register User Data](/identity/register-user-data))
* Register the user's details and KYC data
* Upload any required documents
* Trigger identity verification and poll until the `onramp` eligibility reads `verified`
The exact fields required vary by region and KYC tier. See [Data Requirements](/identity/data-requirements) for the full list, and [User Onboarding and KYC](/onramp/introduction/user-onboarding) for how the onramp KYC tiers work.
## Create an Onramp Order
Once the user is registered with their KYC data, you can create an onramp order. The order will use the registered KYC data for compliance checks.
**Important**: Crossmint links pre-registered KYC data to an order using the user's email address. The `payment.receiptEmail` value must match the email used in `kycData.email` when registering the user's KYC data with the [Create User API](/api-reference/users/upsert-user).
```js Node.js theme={null}
const options = {
method: 'POST',
headers: {
'X-API-KEY': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
recipient: {
walletAddress: "YOUR_WALLET_ADDRESS"
},
payment: {
method: "card",
receiptEmail: "YOUR_USER_EMAIL"
},
lineItems: [
{
tokenLocator: "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
executionParameters: {
mode: "exact-in",
amount: "1"
}
}
]
})
};
fetch('https://staging.crossmint.com/api/2022-06-09/orders', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/orders \
--header 'X-API-KEY: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"recipient": {
"walletAddress": "YOUR_WALLET_ADDRESS"
},
"payment": {
"method": "card",
"receiptEmail": "YOUR_USER_EMAIL"
},
"lineItems": [
{
"tokenLocator": "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"executionParameters": {
"mode": "exact-in",
"amount": "1"
}
}
]
}'
```
```python Python theme={null}
import requests
url = "https://staging.crossmint.com/api/2022-06-09/orders"
payload = {
"recipient": {
"walletAddress": "YOUR_WALLET_ADDRESS"
},
"payment": {
"method": "card",
"receiptEmail": "YOUR_USER_EMAIL"
},
"lineItems": [
{
"tokenLocator": "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"executionParameters": {
"mode": "exact-in",
"amount": "1"
}
}
]
}
headers = {
"X-API-KEY": "YOUR_API_KEY",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
The `lineItems` field accepts either a single object or an array. The examples above use an array for consistency, but you can also pass a single line item object directly.
## Order Responses
When all required KYC data has been provided and validated, the order moves to the payment phase:
```json theme={null}
{
"clientSecret": "...",
"order": {
"orderId": "987e81ab-8c8f-464e-95e9-11ceda80d559",
"phase": "payment",
"locale": "en-US",
"lineItems": [...],
"quote": {...},
"payment": {
"method": "card",
"currency": "usd",
"status": "awaiting-payment"
}
}
}
```
When full KYC data is present but Crossmint needs to run additional compliance checks, the order enters a processing state. This typically resolves within a few seconds as Crossmint runs the appropriate checks:
```json theme={null}
{
"clientSecret": "...",
"order": {
"orderId": "987e81ab-8c8f-464e-95e9-11ceda80d559",
"phase": "payment",
"locale": "en-US",
"lineItems": [...],
"quote": {...},
"payment": {
"method": "card",
"currency": "usd",
"status": "pending-kyc-review"
}
}
}
```
If no user personal data is provided, or if the data is insufficient, the order will fail:
```json theme={null}
{
"error": true,
"message": "Required full KYC user data is missing to complete the order"
}
```
## Guides
# Local Currencies
Source: https://docs.crossmint.com/onramp/guides/local-currencies
Charge users in their local currency by creating the onramp order in USD or EUR
This guide covers how to charge your users in their local currency. When you create an order with a local currency, the quote, the checkout, and the card charge are all denominated in the selected currency.
## Available Currencies
| Currency | Code | Availability |
| :- | :- | :-: |
| USD | `usd` | ✅ |
| EUR | `eur` | ✅ |
| GBP | `gbp` | ✱ |
| AUD | `aud` | ✱ |
| COP | `cop` | ✱ |
\*EUR, GBP, AUD, and COP are not available for US residents.
**Legend:**
* `✅` Supported
* `✱` Available, but not self-serve. Contact us to request it for your project.
If you are interested in a local currency that is not listed, contact Crossmint to confirm support.
## Create the Order in the Selected Currency
From your server, pass the selected currency in `payment.currency`. With `exact-in`, `amount` is expressed in the payment currency, so `"100"` with `"currency": "eur"` means 100 EUR.
**Recommended:** pre-select the default currency from the user's country of residence if your app already knows it, or from their IP address otherwise, and add a currency selector to your UI so the user can change it.
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/orders \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: YOUR_SERVER_API_KEY' \
--data '{
"recipient": { "walletAddress": "USER_WALLET_ADDRESS" },
"lineItems": [{
"tokenLocator": "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"executionParameters": { "mode": "exact-in", "amount": "100" }
}],
"payment": {
"method": "card",
"currency": "eur",
"receiptEmail": "user@example.com"
}
}'
```
```js Node.js theme={null}
const response = await fetch("https://staging.crossmint.com/api/2022-06-09/orders", {
method: "POST",
headers: {
"X-API-KEY": "YOUR_SERVER_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
recipient: { walletAddress: "USER_WALLET_ADDRESS" },
lineItems: [
{
tokenLocator: "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
executionParameters: { mode: "exact-in", amount: "100" },
},
],
payment: {
method: "card",
currency: "eur",
receiptEmail: "user@example.com",
},
}),
});
const { order, clientSecret } = await response.json();
```
```python Python theme={null}
import requests
response = requests.post(
"https://staging.crossmint.com/api/2022-06-09/orders",
headers={"X-API-KEY": "YOUR_SERVER_API_KEY", "Content-Type": "application/json"},
json={
"recipient": {"walletAddress": "USER_WALLET_ADDRESS"},
"lineItems": [
{
"tokenLocator": "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"executionParameters": {"mode": "exact-in", "amount": "100"},
}
],
"payment": {"method": "card", "currency": "eur", "receiptEmail": "user@example.com"},
},
)
print(response.json())
```
The response contains `order.payment.currency: "eur"` and a quote priced in EUR, for example `order.quote.totalPrice: { "amount": "100", "currency": "eur" }`. The amount of stablecoin the user receives is in `order.lineItems[0].quote.quantityRange`.
If the user changes the currency after the order exists, update it from your server with a `PATCH` to `/api/2022-06-09/orders/{orderId}` and the new `payment.currency`, authenticated with your server-side API key. See [Edit Order](/api-reference/headless/edit-order).
Onramp orders cannot be updated from the client side. Update requests authenticated with the order `clientSecret` are rejected. Use your server-side API key.
## Render the Checkout in the Same Currency
Set `payment.fiat.defaultCurrency` on the checkout component to the currency you used to create the order. The checkout compares this value with the order currency and, if they differ, tries to update the order to match `defaultCurrency`. Because onramp orders cannot be updated from the client side, that request fails and the checkout shows an error to the user. The SDK default is `usd`, so always set it explicitly on a EUR order.
```tsx theme={null}
```
```tsx theme={null}
```
```swift theme={null}
CheckoutPayment(
crypto: CheckoutCryptoPayment(enabled: false),
fiat: CheckoutFiatPayment(
enabled: true,
defaultCurrency: "eur",
allowedMethods: CheckoutAllowedMethods(card: true, applePay: true)
)
)
```
```kotlin theme={null}
CheckoutPayment(
crypto = CheckoutCryptoPayment(enabled = false),
fiat = CheckoutFiatPayment(
enabled = true,
defaultCurrency = "eur",
allowedMethods = CheckoutAllowedMethods(card = true, googlePay = true)
)
)
```
## Verify the Currency
Fetch the order with [Get Order](/onramp/api-reference/get-order) and confirm that `payment.currency` and `quote.totalPrice.currency` are both `"eur"`. In the checkout, the price and the card charge are shown in EUR.
## Troubleshooting
`payment.fiat.defaultCurrency` on the checkout component does not match the currency of the order. The checkout tries to update the order from the client side, which is not allowed for onramp orders. Pass the same value you used in `payment.currency` when creating the order.
You are updating an onramp order with the order `clientSecret`. Send the `PATCH` from your server with your server-side API key.
The currency is not enabled for your project. The error response lists the currencies you can use in `parameters.supportedCurrencies`. To enable a non-self-serve currency (`gbp`, `aud`, `cop`), contact Crossmint.
## Next Steps
Enable cards, Apple Pay, and Google Pay for onramp
Reuse the country of residence you already collected
Full reference for the onramp order payload
# Onramp to Non-Crossmint Wallets
Source: https://docs.crossmint.com/onramp/guides/onramp-to-external-wallets
Enable onramp orders to wallets not powered by Crossmint
This guide walks you through how to enable onramp orders to external wallets.
**Internal vs. external wallets:** When you create a Crossmint wallet for a user, set the [`owner`](/wallets/guides/create-wallet) to that user's locator so the wallet is recognized as theirs — onramps to those wallets do not require ownership verification. A wallet created without an `owner`, or any wallet not created through Crossmint, is treated as **external**: you must link it to the user and verify ownership, as shown below.
## When Is Proof of Ownership Required?
Proof of ownership confirms that an onramp user controls the wallet receiving their assets. It helps Crossmint meet anti-money laundering (AML) and counter-terrorism financing (CTF) requirements by linking the user's verified identity to the recipient wallet.
Proof of ownership is triggered based on transaction volume thresholds:
| Condition | Verification Required? |
| :- | :- |
| Transaction below **\$1,000** AND 30-day volume below **\$1,000** | No |
| Transaction above **\$1,000** OR 30-day volume above **\$1,000** | Yes |
These thresholds apply to users verified outside the United States who onramp to external wallets.
To link the user's identity to an external wallet, Crossmint asks the user to sign a challenge with the recipient wallet and submit the signature in the `proof` field. This flow works the same across chains and for all wallet types, including externally owned accounts (EOAs) and smart contract wallets.
## Integration Steps
Before creating an onramp order to an external wallet, you must link the wallet to a Crossmint user (Crossmint creates the user automatically if the locator does not exist yet), using the Link External Wallet API:
```bash cURL theme={null}
curl --request PUT \
--url https://staging.crossmint.com/api/2025-06-09/users/{userLocator}/linked-wallets/{address} \
--header 'X-API-KEY: ' \
--header 'Content-Type: application/json' \
--data '{
"chain": "base-sepolia",
"proof": ""
}'
```
```js Node.js theme={null}
const options = {
method: 'PUT',
headers: {
'X-API-KEY': '',
'Content-Type': 'application/json'
},
body: JSON.stringify({
chain: "base-sepolia",
proof: ""
})
};
const userLocator = "email:user@example.com";
const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}/linked-wallets/${walletAddress}`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
The `proof` field is optional:
* If you omit it, you are simply linking the external wallet to the user without proving ownership yet.
* If you pass it, you prove ownership of the wallet in the same request.
When linking without proof, the API responds with a verification challenge:
```json theme={null}
{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"chain": "base-sepolia",
"type": "external-wallet",
"ownership": {
"verified": false,
"verificationChallenge": "crossmint.com wants you to sign in with your blockchain account:\n0x1234...5678\n\nI am signing this message to prove ownership of my wallet address 0x1234...5678 for Crossmint verification.\n\nURI: https://..."
}
}
```
The `verificationChallenge` is a message in the CAIP-122 format for the recipient wallet to sign. CAIP-122 defines only the message format; all wallets, including externally owned accounts (EOAs) and smart contract wallets, use the same verification flow.
Create an onramp order using the [Create Order API](/api-reference/headless/create-order) with the linked external wallet address as the recipient.
Depending on the transaction value and the user's past activity, the order may require wallet ownership verification (steps 3 and 4). If verification is not required, the order proceeds directly to the KYC/payment phase and you can skip to step 5.
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/orders \
--header 'X-API-KEY: ' \
--header 'Content-Type: application/json' \
--data '{
"recipient": {
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678"
},
"payment": {
"method": "card",
"receiptEmail": "user@example.com"
},
"lineItems": [
{
"tokenLocator": "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"executionParameters": {
"mode": "exact-in",
"amount": "10"
}
}
]
}'
```
```js Node.js theme={null}
const options = {
method: 'POST',
headers: {
'X-API-KEY': '',
'Content-Type': 'application/json'
},
body: JSON.stringify({
recipient: {
walletAddress: "0x1234567890abcdef1234567890abcdef12345678"
},
payment: {
method: "card",
receiptEmail: "user@example.com"
},
lineItems: [
{
tokenLocator: "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
executionParameters: {
mode: "exact-in",
amount: "10"
}
}
]
})
};
fetch('https://staging.crossmint.com/api/2022-06-09/orders', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
The order may or may not require wallet ownership verification:
* **Below threshold**: If the transaction value is below **\$1,000** **and** the user's past 30-day onramp transaction volume is below **\$1,000**, ownership verification is not required. The order proceeds directly to the KYC/payment phase.
* **Above threshold**: If either threshold is surpassed, the order status will be `requires-recipient-verification` and the user must sign a message to prove wallet ownership (steps 3 and 4).
When ownership verification is required, the order response will look like this:
```json theme={null}
{
"clientSecret": "...",
"order": {
"orderId": "987e81ab-8c8f-464e-95e9-11ceda80d559",
"phase": "payment",
"lineItems": [...],
"quote": {...},
"payment": {
"status": "requires-recipient-verification",
"method": "card",
"currency": "usd",
"preparation": {
"message": "crossmint.com wants you to sign in with your blockchain account:\n0x1234...5678\n\nI am signing this message to prove ownership of my wallet address 0x1234...5678 for Crossmint verification.\n\nURI: https://...\nVersion: 1\nNonce: ...\nIssued At: ...\nExpiration Time: ...\nRequest ID: ...\nChain ID: base-sepolia"
}
}
}
}
```
This step is only required if the order status is `requires-recipient-verification`.
Have the user sign the `preparation.message` with the recipient wallet. All wallets, including EOAs and smart contract wallets, use the same verification flow.
```js EVM (ethers.js) theme={null}
import { Wallet } from "ethers";
const PRIVATE_KEY = "YOUR_WALLET_PRIVATE_KEY"; // Replace with your wallet's private key
const wallet = new Wallet(PRIVATE_KEY);
const message = order.payment.preparation.message;
const signature = await wallet.signMessage(message);
```
```js Solana (tweetnacl) theme={null}
import nacl from "tweetnacl";
import { decodeUTF8 } from "tweetnacl-util";
import { encode as base64Encode } from "@stablelib/base64";
const message = order.payment.preparation.message;
const messageBytes = decodeUTF8(message);
const signatureBytes = nacl.sign.detached(messageBytes, keypair.secretKey);
const signature = base64Encode(signatureBytes);
```
This step is only required if the order status is `requires-recipient-verification`.
Use the same Link External Wallet API to submit the signature as proof of ownership. Pass the signature in the `proof` field:
```bash cURL theme={null}
curl --request PUT \
--url https://staging.crossmint.com/api/2025-06-09/users/{userLocator}/linked-wallets/{address} \
--header 'X-API-KEY: ' \
--header 'Content-Type: application/json' \
--data '{
"chain": "base-sepolia",
"proof": ""
}'
```
```js Node.js theme={null}
const options = {
method: 'PUT',
headers: {
'X-API-KEY': '',
'Content-Type': 'application/json'
},
body: JSON.stringify({
chain: "base-sepolia",
proof: ""
})
};
const userLocator = "email:user@example.com";
const walletAddress = "0x1234567890abcdef1234567890abcdef12345678";
fetch(`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}/linked-wallets/${walletAddress}`, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
Once ownership is verified, the response will confirm verification:
```json theme={null}
{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"chain": "base-sepolia",
"type": "external-wallet",
"ownership": {
"verified": true
}
}
```
Once ownership is verified (or if verification was not required), fetch the order using the [Get Order API](/api-reference/headless/get-order). The order status should now have proceeded to the next phase:
* **`requires-kyc`**: The user hasn't completed KYC yet and will be guided through the KYC flow first.
* **`awaiting-payment`**: The user has completed KYC and can proceed to complete payment.
The user will be guided through Crossmint's embedded components to complete their order, handling payment and any remaining compliance steps automatically.
## Pre-Verifying Ownership
You can verify wallet ownership at the time of linking (before any order is created) by including the `proof` field in the initial Link External Wallet call, as shown in step 1. This avoids requiring verification during the checkout flow.
## Related Resources
* [React Quickstart (External Wallets tab)](/onramp/quickstarts/react) — quickstart with external wallet support
* [Import User KYC Data](/onramp/guides/import-user-kyc-data) — pre-register KYC data so users skip in-checkout verification
# Add Payment Methods
Source: https://docs.crossmint.com/onramp/guides/payment-methods
Enable cards, Apple Pay, and Google Pay for onramp
This guide covers how to enable each supported payment method — cards, Apple Pay, and Google Pay — in your onramp integration, including the platform-specific configuration each one requires.
All payment methods are controlled through the payment configuration of the checkout component. The checkout only displays methods that the user's device supports, so enabling a method is safe even on platforms where it cannot appear. For an explanation of how each method works, see [Payment Methods](/onramp/concepts/payment-methods).
## Prerequisites
* A working onramp integration — complete a [quickstart](/onramp/quickstarts/react) first.
## Configure Allowed Methods
Set the methods you want to offer in the fiat payment configuration:
```tsx theme={null}
```
```tsx theme={null}
```
```dart theme={null}
CrossmintEmbeddedCheckout(
apiKey: clientApiKey,
config: CrossmintCheckoutConfig(
order: CrossmintCheckoutExistingOrder(
orderId: order.orderId,
clientSecret: order.clientSecret,
),
payment: CrossmintCheckoutPayment(
crypto: CrossmintCheckoutCryptoPayment(enabled: false),
fiat: CrossmintCheckoutFiatPayment(
enabled: true,
allowCard: true,
allowApplePay: true,
allowGooglePay: true,
),
),
),
)
```
```kotlin theme={null}
CrossmintEmbeddedCheckout(
orderId = "YOUR_ORDER_ID",
clientSecret = "YOUR_CLIENT_SECRET",
payment = CheckoutPayment(
crypto = CheckoutCryptoPayment(enabled = false),
fiat = CheckoutFiatPayment(
enabled = true,
allowedMethods = CheckoutAllowedMethods(
card = true,
applePay = true,
googlePay = true
)
)
),
environment = CheckoutEnvironment.STAGING
)
```
```swift theme={null}
CrossmintEmbeddedCheckout(
orderId: "YOUR_ORDER_ID",
clientSecret: "YOUR_CLIENT_SECRET",
payment: CheckoutPayment(
crypto: CheckoutCryptoPayment(enabled: false),
fiat: CheckoutFiatPayment(
enabled: true,
allowedMethods: CheckoutAllowedMethods(
googlePay: false,
applePay: true,
card: true
)
)
)
)
```
Cards work on every platform with no additional setup. Apple Pay and Google Pay require the platform-specific configuration below.
## Apple Pay
To display Apple Pay on your website (Safari on macOS and iOS), you must verify your domain with Apple through the Crossmint Console. This is a one-time setup per domain.
### Prerequisites
1. **HTTPS hosting** — your site must be served over HTTPS.
2. **Crossmint account** — you need an active Crossmint developer account.
Go to the Apple Pay Domains page in the Crossmint Console and click **Download file** to get the Apple Developer Merchant ID Domain Association file.
Host the downloaded file on your server at the following path:
```text theme={null}
https://YOUR_DOMAIN/.well-known/apple-developer-merchantid-domain-association
```
**Option 1: Using the Public Folder**
1. Create the `.well-known` folder inside your `public` directory.
2. Place the downloaded file in `public/.well-known/` with the filename `apple-developer-merchantid-domain-association` (no extension).
**Option 2: Using a Route**
1. Create a folder at `app/.well-known/apple-developer-merchantid-domain-association/`
2. Add a `route.ts` file with the following content:
```typescript route.ts theme={null}
import { NextResponse } from "next/server";
export async function GET() {
return new NextResponse("PASTE_FILE_CONTENT_HERE", {
headers: {
"Content-Type": "text/plain",
},
});
}
```
Replace `PASTE_FILE_CONTENT_HERE` with the content of the downloaded verification file.
1. Create the `.well-known` folder inside your `public` directory.
2. Place the downloaded file in `public/.well-known/` with the filename `apple-developer-merchantid-domain-association` (no extension).
1. Create the `.well-known` folder inside your `public` directory.
2. Place the downloaded file in `public/.well-known/` with the filename `apple-developer-merchantid-domain-association` (no extension).
1. Return to the Apple Pay Domains page in the Crossmint Console.
2. Enter your domain (e.g., `example.com` or `checkout.example.com`) in the input field.
3. Click **Verify domain**.
If verification succeeds, your domain will appear in the list of registered domains and Apple Pay will be available as a payment method in the onramp checkout.
If verification fails, ensure the file is publicly accessible at the correct path. You can test by visiting `https://YOUR_DOMAIN/.well-known/apple-developer-merchantid-domain-association` in your browser.
### Local Testing
Using ngrok is recommended for local testing:
1. Start ngrok with a fixed domain if possible for consistent testing.
2. Host the verification file on your local server.
3. Register the ngrok domain in the Crossmint Console.
4. Test Apple Pay on Safari (macOS) or any iOS device.
In React Native, `CrossmintEmbeddedCheckout` renders the checkout page in a WebView, and Apple Pay runs inside that page rather than through a native Apple Pay integration in your app. This changes the setup compared with the web:
* **No domain verification.** The checkout page is served from `crossmint.com`, which is already enabled for Apple Pay, so the domain steps in the Web tab do not apply.
* **No Apple Pay native configuration.** Your app does not need an Apple merchant ID or the Apple Pay capability. The `@crossmint/client-sdk-react-native-ui` Expo plugin has no Apple Pay option; its `enableGooglePay` setting only affects Android.
* **iOS 16 or higher.** Apple Pay is available inside a WebView on iOS 16 and later, including when the SDK injects its bridge scripts into the page. Do not set the `enableApplePay` prop of react-native-webview: it exists for iOS 13 to 15, where injected scripts disabled Apple Pay, and it breaks the communication between the SDK and the checkout page.
* **Test on a physical iPhone.** Use a device running iOS 17 or higher with a card added to Apple Wallet. The iOS Simulator does not render Apple Pay. A development build installed on the device is enough; a TestFlight build is not required.
Enable Apple Pay with `applePay: true` in `allowedMethods` as shown in [Configure Allowed Methods](#configure-allowed-methods). To render the checkout in your own WebView instead of the SDK component, see the [Mobile WebView Integration guide](/payments/embedded/guides/webview-integration).
Apple Pay is supported automatically in native iOS apps via the Swift SDK — no domain verification is required. The checkout renders inside a WebView, so Apple Pay is available on iOS 16 and later; test on a physical iPhone, as the iOS Simulator does not render Apple Pay. Enable it through `allowedMethods` as shown in [Configure Allowed Methods](#configure-allowed-methods), and see the [Swift Quickstart](/onramp/quickstarts/swift) for the full integration.
## Google Pay
On the web, Google Pay works out of the box — enabling it via `allowedMethods` is all that is needed. Mobile apps require additional native configuration because the payment sheet is rendered by the operating system.
### Prerequisites
* React Native project with Expo or bare workflow
* Android device or emulator with Google Play Services
```bash theme={null}
pnpm add @crossmint/client-sdk-react-native-ui
```
The SDK includes `react-native-webview` as a dependency. If you need to install it separately, ensure version **13.15.0 or higher**:
```bash theme={null}
pnpm add react-native-webview@^13.15.0
```
Update your `app.json` to enable Google Pay:
```json theme={null}
{
"expo": {
"plugins": [
[
"@crossmint/client-sdk-react-native-ui",
{
"enableGooglePay": true
}
]
]
}
}
```
This plugin modifies native Android permissions required for Google Pay. The app must be rebuilt after adding this configuration.
Hot reload cannot apply the permission changes. You must rebuild:
```bash theme={null}
npx expo run:android
```
Or for bare React Native:
```bash theme={null}
npx react-native run-android
```
Set `googlePay: true` in `allowedMethods` as shown in [Configure Allowed Methods](#configure-allowed-methods).
### Prerequisites
* Minimum Android SDK version 24 (Android 7.0)
* Jetpack Compose
In your app-level `build.gradle.kts`:
```kotlin theme={null}
dependencies {
implementation("com.crossmint:crossmint-sdk-android:0.0.9")
}
```
Add the required query intents to enable Google Pay functionality:
```xml theme={null}
```
Set `googlePay = true` in `allowedMethods` as shown in [Configure Allowed Methods](#configure-allowed-methods).
### Google Pay Production Approval
Google Pay works immediately in **staging** environments. For **production**, Google requires explicit approval of your Android app.
1. Navigate to the Google Pay & Wallet Console
2. Select "Merchant" as your business type
3. Complete all business profile information
4. Navigate to **Google Pay API** and click "Get Started"
5. Accept the Google Pay API Terms of Service
6. Note your **Merchant ID** (top-right corner after completion)
In the Google Pay & Wallet Console:
1. Navigate to **Google Pay API** then **Integrations** then **Integrate with your Android app**
2. Locate your Android application and click "Manage"
3. Select your integration type (typically "Gateway")
4. Upload screenshots of your TEST Google Pay integration
5. Click "Save" then "Submit for approval"
**Required Screenshots:**
* Product/item selection showing Google Pay option
* Cart or checkout view with payment options
* Google Pay payment sheet with card selection
* Confirmation or receipt screen
After Google approval (typically \~1 business day):
1. Sign your APK with a **release key** (debug keys do not work in production)
2. Update environment settings:
* Kotlin: `environment = CheckoutEnvironment.PRODUCTION`
* React Native: Use production API key
3. Publish your app to Google Play Store
## Troubleshooting
1. **Domain not verified (web):** Verify your domain on the Apple Pay Domains page
2. **Unsupported browser:** Apple Pay on web only appears in Safari on macOS and iOS
3. **iOS Simulator (mobile apps):** Apple Pay does not render in the Simulator; test on a physical iPhone running iOS 17 or higher with a card in Apple Wallet
4. **iOS version (mobile apps):** Apple Pay inside a WebView requires iOS 16 or higher
5. **Payment method disabled:** Check `applePay: true` in `allowedMethods`
1. **Plugin not configured:** Verify `enableGooglePay: true` in `app.json` plugins
2. **App not rebuilt:** Run `npx expo run:android` after adding the plugin
3. **Environment mismatch:** Staging works without approval; production requires it
4. **Payment method disabled:** Check `googlePay: true` in `allowedMethods`
1. **AndroidManifest missing queries:** Add all three Chromium intent queries
2. **Native rebuild required:** Hot reload cannot apply permission changes
3. **Device requirements:** User needs Google Play Services 25.18.30+ and WebView 137+
## Next Steps
Understand how each payment method works and why onramp is a regulated transfer
Customize the onramp checkout UI to match your brand identity
Register users and handle KYC before their first onramp transaction
Build a complete onramp integration from scratch
# UI Customization
Source: https://docs.crossmint.com/onramp/guides/ui-customization
Customize the onramp checkout UI to match your brand identity and design system
The onramp embedded checkout component supports extensive customization through the `appearance` prop, allowing you to match the look and feel of your application.
## Quick Start
```jsx theme={null}
```
## Identity Verification Screens
## Customization Options
### Background
The embedded checkout component has a **transparent background** by default. The `backgroundPrimary` color only affects internal components (inputs, cards, etc.) within the iframe, not the iframe body itself.
To customize the overall background, wrap the component:
```jsx theme={null}
```
### Custom Fonts
Load custom fonts using the `fonts` array:
```jsx theme={null}
appearance={{
fonts: [
{ cssSrc: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" }
]
}}
```
### Global Variables
Control the overall look and feel:
```jsx theme={null}
appearance={{
variables: {
fontFamily: "Inter, system-ui, sans-serif",
fontSizeUnit: "16px",
spacingUnit: "1rem",
borderRadius: "8px",
colors: {
borderPrimary: "#E0E0E0",
backgroundPrimary: "#FFFFFF",
textPrimary: "#000000",
textSecondary: "#666666",
danger: "#FF0000",
warning: "#FFA500",
accent: "#0074D9"
}
}
}}
```
### Component Rules
Control the visibility of specific inputs:
```jsx theme={null}
appearance={{
rules: {
DestinationInput: {
display: "hidden"
},
ReceiptEmailInput: {
display: "hidden"
}
}
}}
```
When hiding inputs, make sure to provide the corresponding values via props:
* For hidden `DestinationInput`, provide `recipient.walletAddress`
* For hidden `ReceiptEmailInput`, provide `payment.receiptEmail`
Customize label styling:
```jsx theme={null}
appearance={{
rules: {
Label: {
font: {
family: "Inter",
size: "14px",
weight: "500",
},
colors: {
text: "#333333",
},
},
},
}}
```
Style input fields:
```jsx theme={null}
appearance={{
rules: {
Input: {
borderRadius: "8px",
font: {
family: "Inter",
size: "16px",
weight: "400",
},
colors: {
text: "#000000",
background: "#FFFFFF",
border: "#E0E0E0",
boxShadow: "none",
placeholder: "#999999",
},
hover: {
colors: {
border: "#0074D9",
},
},
focus: {
colors: {
border: "#0074D9",
boxShadow: "0 0 0 2px rgba(0,116,217,0.2)",
},
},
},
},
}}
```
Customize tab appearance:
```jsx theme={null}
appearance={{
rules: {
Tab: {
borderRadius: "4px",
font: {
family: "Inter",
size: "14px",
weight: "500",
},
colors: {
text: "#666666",
background: "transparent",
},
selected: {
colors: {
text: "#000000",
background: "#F5F5F5",
},
},
hover: {
colors: {
background: "#F0F0F0",
},
},
},
},
}}
```
Style primary buttons:
```jsx theme={null}
appearance={{
rules: {
PrimaryButton: {
borderRadius: "8px",
font: {
family: "Inter",
size: "16px",
weight: "600"
},
colors: {
text: "#FFFFFF",
background: "#0074D9"
},
hover: {
colors: {
background: "#0063B8"
}
},
disabled: {
colors: {
text: "#FFFFFF",
background: "#CCCCCC"
}
}
}
}
}}
```
## Common Examples
### Modern Dark Theme
```jsx theme={null}
appearance={{
variables: {
fontFamily: "Inter, system-ui, sans-serif",
colors: {
backgroundPrimary: "#1A1A1A",
textPrimary: "#FFFFFF",
textSecondary: "#A0A0A0",
borderPrimary: "#333333",
accent: "#7928CA"
}
},
rules: {
Input: {
colors: {
background: "#2D2D2D",
border: "#404040",
text: "#FFFFFF",
placeholder: "#666666"
}
},
PrimaryButton: {
colors: {
background: "#7928CA",
text: "#FFFFFF"
},
hover: {
colors: {
background: "#6B24B2"
}
}
}
}
}}
```
### Minimal Light Theme
```jsx theme={null}
appearance={{
variables: {
fontFamily: "system-ui, sans-serif",
borderRadius: "4px",
colors: {
backgroundPrimary: "#FFFFFF",
textPrimary: "#000000",
borderPrimary: "#E0E0E0",
accent: "#000000"
}
},
rules: {
Input: {
borderRadius: "4px",
colors: {
background: "#F5F5F5",
border: "transparent"
},
focus: {
colors: {
border: "#000000",
boxShadow: "none"
}
}
},
PrimaryButton: {
borderRadius: "4px",
colors: {
background: "#000000",
text: "#FFFFFF"
}
}
}
}}
```
## Related Resources
* [React Quickstart](/onramp/quickstarts/react)
* [React Native Quickstart](/onramp/quickstarts/react-native)
* [User Onboarding](/onramp/introduction/user-onboarding)
* [Identity Verification UI Customization](/identity/ui-customization)
# User Onboarding and KYC
Source: https://docs.crossmint.com/onramp/introduction/user-onboarding
Register users for the Onramp
Every end user must complete KYC before buying stablecoins through the Onramp. This page explains the two decisions you need to make: who collects KYC data, and which KYC tier applies.
## Who Collects KYC Data?
This is independent of the integration mode you choose (embedded component or headless API). In either case, you decide whether Crossmint collects KYC directly from the user, or you supply data you have already collected.
| Approach | Best For | How It Works |
| :- | :- | :- |
| **Crossmint-Hosted** | Teams that want the fastest integration | The user completes KYC inside Crossmint's secure embedded components. You do not need to handle any KYC data yourself. |
| **You supply KYC data** | Platforms that already verify users through their own identity verification (IDV) provider | You pass verified user data to Crossmint via API before creating an order. |
## KYC Tiers
Crossmint applies two KYC tiers based on cumulative transaction volume, transaction frequency, and risk profile:
| Tier | Applies When | Data Required |
| :- | :- | :- |
| **Progressive KYC** | Up to \$1,000 in total volume in the last 12 months | Name, date of birth, nationality, country, address, email, and phone number (phone required for US residents). An ID number or SSN is required for US (SSN) and EU/EEA residents; optional elsewhere. |
| **Full KYC** | Over \$1,000 in total volume in the last 12 months, or a high number of transactions | Progressive KYC data plus `dueDiligence`, `verificationHistory`, identity document, and selfie. See the [data requirements](/identity/data-requirements) page. |
Independently of the tier, Crossmint's compliance team may request a specific document from a user at any time — proof of address, source of wealth, or an identity document. See [additional document requests](/identity/quickstart#additional-document-requests).
### How It Works
Crossmint handles all KYC data collection and tier transitions. You create the order via API and render the embedded component in your app.
Create an order via API and render the Crossmint embedded checkout component in your app. The [onramp quickstart](/onramp/quickstarts/react) walks through both steps.
The embedded checkout component prompts the user for the required identity data. You do not collect or handle any KYC data yourself. The KYC screens follow the checkout `appearance` you already pass; see [UI Customization](/identity/ui-customization).
When a user crosses the \$1,000 threshold or reaches a high number of transactions, the component automatically prompts them for the additional Full KYC data. If Crossmint's compliance team requests additional documents from the user, the component collects those as well. You do not need to build or manage any tier logic.
You verify users through your own identity verification (IDV) provider and pass the data to Crossmint via API. You are responsible for supplying the correct tier of data.
Before submitting any user data, you must record the user's acceptance of Crossmint's Terms of Service and Privacy Policy. The [Identity quickstart](/identity/quickstart) walks through this step.
Register the user's basic identity data via the [Create User API](/api-reference/users/upsert-user).
Call the [Trigger Identity Verification API](/api-reference/users/trigger-identity-verification) to run Crossmint's compliance checks. Verification runs asynchronously.
Poll the [Get Identity Verification Status API](/api-reference/users/get-identity-verification) until the `onramp` eligibility reads `verified`. If the status is `requires-data`, the response includes `missingData` and `missingDocuments` arrays that tell you exactly what additional information is needed. Supply it via the [Create User API](/api-reference/users/upsert-user) and/or [Upload Document API](/api-reference/users/upload-document), then re-trigger verification.
If the status is `rejected`, check the entry's `failureReasons` array for a machine-readable cause before assuming the case needs manual escalation. See [Failure Reasons](/identity/failure-reasons) for the full list of codes.
Create an order via API and render the Crossmint embedded checkout component in your app. The [onramp quickstart](/onramp/quickstarts/react) walks through both steps.
On order creation, if the user has exceeded the progressive KYC threshold and full KYC data has not been provided, the order returns an error: `"Insufficient KYC data provided for this user to perform onramp"`. Supply the required data via the [Upsert User API](/api-reference/users/upsert-user) and/or [Upload Document API](/api-reference/users/upload-document), then retry order creation. See the [data requirements](/identity/data-requirements) page for the full list of fields.
Separately, Crossmint's compliance team may request a specific document from a user at any time: the verification status reads `requires-data` with the document listed in `missingDocuments`. Upload it via the [Upload Document API](/api-reference/users/upload-document) and re-trigger verification — see [additional document requests](/identity/quickstart#additional-document-requests).
Tier transitions are not automatic. You must detect when additional data is needed and provide it. The verification status response tells you exactly what is missing.
## Onboarding for AI Agents
For AI agent use cases where a human operator has already completed KYC through your platform, you can pre-register the agent operator's identity data via the API. The agent can then execute onramp transactions without being prompted for KYC in the checkout flow.
See the [Agent Wallets Onramp](/solutions/ai-agents/agent-wallets/onramp) guide for agent-specific integration details.
## Guides
# Overview
Source: https://docs.crossmint.com/onramp/overview
Allow your users to buy stablecoins with any payment method
Crossmint Onramp lets your customers buy stablecoins directly inside your app through a white-labeled, native experience.
With plug-and-play SDKs, your team can focus on building great products instead of building checkout, compliance, and chargeback operations in-house.
The product is available in 160+ countries, with lightweight KYC up to \$1,000. Supported payments include debit and credit cards, Apple Pay, and Google Pay, with bank transfers coming soon.
**Need stablecoin payouts only?** If you already have your own checkout and only need stablecoin payouts or
licensing support, see the [Payouts guide](/stablecoin-orchestration/regulated-transfers/overview). For treasury
onramps, check the [treasury funding guide](/stablecoin-orchestration/guides/on-off-ramps/funding-company-wallets).
## See It in Action
Try it yourself using the following link.
## Key Features
Seamless in-app experience
Faster onboarding, higher conversion
One-click stablecoin onramps
Global coverage out of the box
Crossmint assumes liability
Import user data (optional)
## Get Started
### Embedded
Select your framework to embed pre-built Crossmint components for onramp. Testing in staging is self-serve. Contact sales for production.
### Headless
Use your own custom components with Crossmint APIs for onramp. Contact sales for more details.
## Learn More
* Try It Yourself
* [User Onboarding](/onramp/introduction/user-onboarding)
* [Data Requirements](/identity/data-requirements)
## Advanced Guides
Import user KYC data
Onramp to any wallet
Match your brand
Enable Apple Pay & Google Pay
## FAQs
You can try out the user journey here.
Integrating the product on staging is fully self-serve - simply get an API key on the staging console and follow the docs.
Production access requires a signed Order Form and completed KYB verification. Contact sales to get started. See the [account verification documentation](/introduction/platform/account-verification) for details.
You can monitor purchases onchain or using webhooks. Sales dashboards, logs, and advanced analytics are also
available in the console.
The standard transaction limits are **\$2,000 per user and day**. Limits reset daily at midnight US Eastern Time (05:00 UTC during standard time, 04:00 UTC during daylight saving time). If you need higher limits, contact the sales team.
Crossmint Onramp is available in 160+ countries. Contact sales for the latest country list and coverage details.
# Flutter
Source: https://docs.crossmint.com/onramp/quickstarts/flutter
Allow your mobile users to buy stablecoins with cards
See the Flutter SDK checkout playground.
Crossmint Checkout lets you build a seamless onramp for Flutter users to buy stablecoins with a credit card, Apple Pay, or Google Pay.
In this guide, you will learn how to:
* Create a Crossmint order via an API endpoint
* Use Crossmint's embedded checkout widget to handle KYC, payment, and delivery automatically
## Prerequisites
* Flutter 3.x+
* Dart SDK ^3.11.4
* iOS 13.0+ / Android 5.0+
You also need the `http` package for making API requests to your backend:
```bash theme={null}
flutter pub add http
```
* Create a [server-side API key](/introduction/platform/api-keys/server-side) with the
`orders.create` and `orders.read` scopes enabled.
Create a [client-side API key](/introduction/platform/api-keys/client-side) for the
embedded checkout component.
* Pass your API key and backend URL via Dart defines when running the app:
```bash theme={null}
flutter run \
--dart-define=CROSSMINT_CLIENT_API_KEY=YOUR_CLIENT_API_KEY \
--dart-define=API_URL=http://10.0.2.2:3000
```
`localhost` on a mobile device or emulator does not resolve to your development machine.
Use `10.0.2.2` for Android emulators, `127.0.0.1` for iOS simulators, or your machine's
LAN IP / an ngrok tunnel for physical devices.
## Add Onramp to Your App
If you want to enable onramp orders to external wallets (EOAs or third-party smart wallets) instead of Crossmint wallets, see the [Onramp to External Wallets](/onramp/guides/onramp-to-external-wallets) guide before proceeding.
To initiate an onramp, you must first create a purchase order on your backend. This example uses Express, but you can use any backend framework:
```typescript server/createOrder.ts theme={null}
import express from "express";
const app = express();
app.use(express.json());
const USDC_TOKEN_LOCATORS = {
solanaDevnet: "solana:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
baseSepolia: "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
stellarTestnet: "stellar:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
};
app.post("/api/create-order", async (req, res) => {
const { walletAddress, receiptEmail, amount, chain } = req.body;
const serverApiKey = process.env.CROSSMINT_SERVER_SIDE_API_KEY;
if (serverApiKey == null) {
return res.status(500).json({ error: "Server API key not configured" });
}
// Use `https://www.crossmint.com` in production.
const baseUrl = process.env.CROSSMINT_BASE_URL ?? "https://staging.crossmint.com";
const response = await fetch(`${baseUrl}/api/2022-06-09/orders`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": serverApiKey,
},
body: JSON.stringify({
lineItems: [
{
tokenLocator: USDC_TOKEN_LOCATORS[chain],
executionParameters: {
mode: "exact-in",
amount,
},
},
],
payment: {
method: "card",
receiptEmail,
},
recipient: {
walletAddress,
},
}),
});
if (!response.ok) {
const error = await response.json();
return res.status(response.status).json(error);
}
const data = await response.json();
res.json(data);
});
app.listen(3000);
```
Create a Flutter screen that handles order creation and displays a checkout:
```dart lib/screens/onramp_screen.dart theme={null}
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:crossmint_flutter/crossmint_flutter_ui.dart';
const clientApiKey = String.fromEnvironment('CROSSMINT_CLIENT_API_KEY');
// Replace with your backend URL.
// On Android emulator use 10.0.2.2 instead of localhost.
// On a physical device use your machine's LAN IP or an ngrok tunnel.
const apiUrl = String.fromEnvironment('API_URL', defaultValue: 'http://10.0.2.2:3000');
class OnrampScreen extends StatefulWidget {
const OnrampScreen({super.key});
@override
State createState() => _OnrampScreenState();
}
class _OnrampScreenState extends State {
final _emailController = TextEditingController(text: 'user@example.com');
final _walletController = TextEditingController();
final _amountController = TextEditingController(text: '5');
String _chain = 'solanaDevnet';
bool _isLoading = false;
Map? _order;
Future _createOrder() async {
setState(() => _isLoading = true);
try {
final response = await http.post(
Uri.parse('$apiUrl/api/create-order'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'walletAddress': _walletController.text,
'receiptEmail': _emailController.text,
'amount': _amountController.text,
'chain': _chain,
}),
);
if (response.statusCode != 200) {
throw Exception('Failed to create order');
}
final data = jsonDecode(response.body);
setState(() {
_order = {
'orderId': data['order']['orderId'],
'clientSecret': data['clientSecret'],
};
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to create order: $e')),
);
} finally {
setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
if (_order != null) {
return Scaffold(
body: CrossmintEmbeddedCheckout(
apiKey: clientApiKey,
config: CrossmintCheckoutConfig(
order: CrossmintCheckoutExistingOrder(
orderId: _order!['orderId']!,
clientSecret: _order!['clientSecret'],
),
payment: CrossmintCheckoutPayment(
fiat: CrossmintCheckoutFiatPayment(enabled: true),
crypto: CrossmintCheckoutCryptoPayment(enabled: false),
),
),
onOrderUpdated: (order) => debugPrint('Order: $order'),
),
);
}
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Buy USDC',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.w600)),
const SizedBox(height: 24),
const Text('Network', style: TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(height: 8),
SegmentedButton(
segments: const [
ButtonSegment(value: 'solanaDevnet', label: Text('Solana')),
ButtonSegment(value: 'baseSepolia', label: Text('Base')),
ButtonSegment(value: 'stellarTestnet', label: Text('Stellar')),
],
selected: {_chain},
onSelectionChanged: (v) => setState(() => _chain = v.first),
),
const SizedBox(height: 20),
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _walletController,
decoration: const InputDecoration(
labelText: 'Recipient Wallet',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _amountController,
decoration: const InputDecoration(
labelText: 'Amount (USD)',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _createOrder,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
padding: const EdgeInsets.all(16),
),
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('Continue to Checkout',
style: TextStyle(color: Colors.white, fontSize: 16)),
),
),
],
),
),
);
}
@override
void dispose() {
_emailController.dispose();
_walletController.dispose();
_amountController.dispose();
super.dispose();
}
}
```
Import and render the `OnrampScreen` in your app:
```dart lib/main.dart theme={null}
import 'package:flutter/material.dart';
import 'screens/onramp_screen.dart';
void main() {
runApp(const MaterialApp(home: OnrampScreen()));
}
```
The integration is complete. When users fill in their details and tap "Continue to Checkout", the API creates an order and the embedded checkout widget handles KYC, payment, and delivery automatically.
**Testing:** Use the test credit card number `4242 4242 4242 4242` with any future expiration date and any 3-digit CVC.
## Transaction Completion
## Next Steps
# Kotlin
Source: https://docs.crossmint.com/onramp/quickstarts/kotlin
Allow your Android users to buy stablecoins with credit cards and Google Pay
In this guide, you'll learn how to:
* Install the Kotlin SDK
* Create an Onramp order for your authenticated users
* Use the embedded checkout component to handle KYC, payment, and delivery
View the Kotlin checkout example repository.
## Prerequisites
* **Android SDK 24+**
* **Jetpack Compose**
* Android Studio Arctic Fox or later
Install the Crossmint Kotlin SDK using Maven Central.
Add the following dependency to your app's `build.gradle.kts` file:
```kotlin theme={null}
dependencies {
implementation("com.crossmint:crossmint-sdk-android:0.0.9")
}
```
* Create a [server-side API key](https://docs.crossmint.com/introduction/platform/api-keys/server-side) with the
`orders.create` and `orders.read` scopes enabled. In staging, all scopes are included in your API key.
## Add Onramp to Your App
If you want to enable onramp orders to external wallets (EOAs or third-party smart wallets) instead of Crossmint wallets, see the [Onramp to External Wallets](/onramp/guides/onramp-to-external-wallets) guide before proceeding.
Use the [Create Order API](/api-reference/headless/create-order) to initiate the purchase process from your backend.
Once you have the order response with `orderId` and `clientSecret`, use Crossmint's embedded checkout component in your Jetpack Compose view.
The component supports **native Google Pay** for a seamless Android payment experience.
```kotlin theme={null}
import com.crossmint.kotlin.checkout.CheckoutEnvironment
import com.crossmint.kotlin.checkout.CrossmintEmbeddedCheckout
import com.crossmint.kotlin.checkout.models.*
CrossmintEmbeddedCheckout(
orderId = "",
clientSecret = "",
payment = CheckoutPayment(
crypto = CheckoutCryptoPayment(enabled = false),
fiat = CheckoutFiatPayment(
enabled = true,
allowedMethods = CheckoutAllowedMethods(
googlePay = true, // Native Google Pay support
applePay = false,
card = true
)
)
),
appearance = CheckoutAppearance(
rules = CheckoutAppearanceRules(
destinationInput = CheckoutDestinationInputRule(display = "hidden"),
receiptEmailInput = CheckoutReceiptEmailInputRule(display = "hidden")
)
),
environment = CheckoutEnvironment.STAGING
)
```
**Key features:**
* **Native Google Pay**: Seamless integration with Android payment system
* **Jetpack Compose Components**: Modern declarative UI framework
* **Automatic KYC**: Handles verification flow automatically
* **Customizable Appearance**: Control visibility of UI elements
## Transaction Completion
## Next Steps
* View the Kotlin checkout example repo on GitHub
# React
Source: https://docs.crossmint.com/onramp/quickstarts/react
Allow your users to buy stablecoins with cards
In this guide, you will learn how to:
* Install the React SDK
* Create an Onramp order for your authenticated users
* Use the embedded checkout component to handle KYC, payment, and delivery
See a full working example.
You can test the Onramp self-serve in staging. To enable it in production, contact sales.
## Prerequisites
Install the Crossmint client SDK:
* Create a [server-side API key](https://docs.crossmint.com/introduction/platform/api-keys/server-side) with the
`orders.create` and `orders.read` scopes enabled, and a [client-side API key](https://docs.crossmint.com/introduction/platform/api-keys/client-side)
for the embedded checkout component.
* Add environment variables to your `.env.local`:
```bash .env.local theme={null}
NEXT_PUBLIC_CROSSMINT_CLIENT_SIDE_API_KEY="_YOUR_CLIENT_API_KEY_"
CROSSMINT_SERVER_SIDE_API_KEY="_YOUR_SERVER_API_KEY_"
```
## Onramp to Crossmint Wallets
Use this flow to onramp funds into wallets created with Crossmint.
Create a server action that will create orders via the Crossmint API:
```typescript app/actions/createOrder.ts theme={null}
"use server";
const USDC_TOKEN_LOCATORS = {
solanaDevnet: "solana:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
baseSepolia: "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
stellarTestnet: "stellar:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
};
interface CreateOrderParams {
walletAddress: string;
receiptEmail: string;
amount: string;
chain: "solanaDevnet" | "baseSepolia" | "stellarTestnet";
}
export async function createOrder({ walletAddress, receiptEmail, amount, chain }: CreateOrderParams) {
const serverApiKey = process.env.CROSSMINT_SERVER_SIDE_API_KEY;
if (serverApiKey == null) {
throw new Error("CROSSMINT_SERVER_SIDE_API_KEY is not set");
}
const response = await fetch("https://staging.crossmint.com/api/2022-06-09/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": serverApiKey,
},
body: JSON.stringify({
lineItems: [
{
tokenLocator: USDC_TOKEN_LOCATORS[chain],
executionParameters: {
mode: "exact-in",
amount,
},
},
],
payment: {
method: "card",
receiptEmail,
},
recipient: {
walletAddress,
},
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Failed to create order");
}
return response.json();
}
```
Create a client component that handles the order creation and displays the embedded checkout:
```tsx app/components/OnrampCheckout.tsx theme={null}
"use client";
import { useState } from "react";
import { CrossmintProvider, CrossmintEmbeddedCheckout } from "@crossmint/client-sdk-react-ui";
import { createOrder } from "../actions/createOrder";
const CLIENT_API_KEY = process.env.NEXT_PUBLIC_CROSSMINT_CLIENT_SIDE_API_KEY || "";
// Destination wallets for testing transfers on each network
const TEST_WALLETS = {
solanaDevnet: "",
baseSepolia: "",
stellarTestnet: "",
};
export default function OnrampCheckout() {
const [order, setOrder] = useState<{ orderId: string; clientSecret: string } | null>(null);
const [chain, setChain] = useState<"solanaDevnet" | "baseSepolia" | "stellarTestnet">("solanaDevnet");
const [email, setEmail] = useState("user@example.com");
const [wallet, setWallet] = useState(TEST_WALLETS.solanaDevnet);
const [amount, setAmount] = useState("5");
const [isLoading, setIsLoading] = useState(false);
function handleChainChange(newChain: "solanaDevnet" | "baseSepolia" | "stellarTestnet") {
setChain(newChain);
setWallet(TEST_WALLETS[newChain]);
}
async function handleSubmit() {
setIsLoading(true);
try {
const result = await createOrder({ walletAddress: wallet, receiptEmail: email, amount, chain });
setOrder({ orderId: result.order.orderId, clientSecret: result.clientSecret });
} catch (error) {
console.error("Failed to create order:", error);
} finally {
setIsLoading(false);
}
}
if (order != null) {
return (
);
}
```
Import and render the `OnrampCheckout` component in your page:
```tsx app/page.tsx theme={null}
import OnrampCheckout from "./components/OnrampCheckout";
export default function Home() {
return (
);
}
```
That is it! When users fill in their details and click "Continue to Checkout", the server action creates an order and the embedded checkout component handles KYC, payment, and delivery automatically.
## Onramp to External Wallets
Use this flow to onramp to external wallets — externally owned accounts (EOAs) or smart wallets not created by Crossmint.
External wallet onramp requires two additional steps compared to Crossmint wallets: linking the wallet to a Crossmint user, and handling ownership verification when transaction thresholds are exceeded.
Before creating an onramp order to an external wallet, you must link the wallet to a Crossmint user using the Link External Wallet API.
Create a server action to handle the wallet linking:
```typescript app/actions/linkWallet.ts theme={null}
"use server";
interface LinkWalletParams {
userEmail: string;
walletAddress: string;
chain: string;
proof?: string;
}
export async function linkWallet({ userEmail, walletAddress, chain, proof }: LinkWalletParams) {
const serverApiKey = process.env.CROSSMINT_SERVER_SIDE_API_KEY;
if (serverApiKey == null) {
throw new Error("CROSSMINT_SERVER_SIDE_API_KEY is not set");
}
const userLocator = `email:${userEmail}`;
const body: Record = { chain };
if (proof) {
body.proof = proof;
}
const response = await fetch(
`https://staging.crossmint.com/api/2025-06-09/users/${userLocator}/linked-wallets/${walletAddress}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
"x-api-key": serverApiKey,
},
body: JSON.stringify(body),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Failed to link wallet");
}
return response.json();
}
```
The `proof` field is optional:
* If you omit it, the wallet is linked without proving ownership yet. The API responds with a `verificationChallenge` that you can use later to verify ownership.
* If you pass it, you prove ownership of the wallet in the same request.
Create a server action that will create orders via the Crossmint API:
```typescript app/actions/createOrder.ts theme={null}
"use server";
const USDC_TOKEN_LOCATORS = {
solanaDevnet: "solana:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
baseSepolia: "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
stellarTestnet: "stellar:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
};
interface CreateOrderParams {
walletAddress: string;
receiptEmail: string;
amount: string;
chain: "solanaDevnet" | "baseSepolia" | "stellarTestnet";
}
export async function createOrder({ walletAddress, receiptEmail, amount, chain }: CreateOrderParams) {
const serverApiKey = process.env.CROSSMINT_SERVER_SIDE_API_KEY;
if (serverApiKey == null) {
throw new Error("CROSSMINT_SERVER_SIDE_API_KEY is not set");
}
const response = await fetch("https://staging.crossmint.com/api/2022-06-09/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": serverApiKey,
},
body: JSON.stringify({
lineItems: [
{
tokenLocator: USDC_TOKEN_LOCATORS[chain],
executionParameters: {
mode: "exact-in",
amount,
},
},
],
payment: {
method: "card",
receiptEmail,
},
recipient: {
walletAddress,
},
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Failed to create order");
}
return response.json();
}
```
**Ownership verification**
Depending on the transaction value and the user's past activity, the order may or may not require wallet ownership verification:
* **Below threshold**: If the transaction value is below **\$1,000** **and** the user's past 30-day onramp transaction volume is below **\$1,000**, ownership verification is not required. The order proceeds directly to the KYC/payment phase.
* **Above threshold**: If either threshold is surpassed, the order status will be `requires-recipient-verification` and the user must sign a message to prove wallet ownership.
When ownership verification is required, the order response includes a `preparation.message` field that the user must sign:
```json theme={null}
{
"payment": {
"status": "requires-recipient-verification",
"preparation": {
"message": "crossmint.com wants you to sign in with your blockchain account:\n0x1234...5678\n\n..."
}
}
}
```
If the order requires ownership verification, have the user sign the `preparation.message` with the recipient wallet. See [Onramp to Non-Crossmint Wallets](/onramp/guides/onramp-to-external-wallets) for how all wallets, including EOAs and smart contract wallets, use the same verification flow.
```js EVM (ethers.js) theme={null}
import { Wallet } from "ethers";
const wallet = new Wallet("YOUR_PRIVATE_KEY");
const message = order.payment.preparation.message;
const signature = await wallet.signMessage(message);
```
```js Solana (tweetnacl) theme={null}
import nacl from "tweetnacl";
import { decodeUTF8 } from "tweetnacl-util";
import { encode as base64Encode } from "@stablelib/base64";
const message = order.payment.preparation.message;
const messageBytes = decodeUTF8(message);
const signatureBytes = nacl.sign.detached(messageBytes, keypair.secretKey);
const signature = base64Encode(signatureBytes);
```
Once signed, submit the signature using the same Link External Wallet API by calling the `linkWallet` server action with the `proof` parameter set to the signature. Once ownership is verified, the order proceeds to the next phase (`requires-kyc` or `awaiting-payment`).
Create a client component that handles wallet linking, order creation, and displays the embedded checkout:
```tsx app/components/OnrampCheckout.tsx theme={null}
"use client";
import { useState } from "react";
import { CrossmintProvider, CrossmintEmbeddedCheckout } from "@crossmint/client-sdk-react-ui";
import { createOrder } from "../actions/createOrder";
import { linkWallet } from "../actions/linkWallet";
const CLIENT_API_KEY = process.env.NEXT_PUBLIC_CROSSMINT_CLIENT_SIDE_API_KEY || "";
// Destination wallets for testing transfers on each network
const TEST_WALLETS = {
solanaDevnet: "YOUR_DESTINATION_WALLET_ADDRESS",
baseSepolia: "YOUR_DESTINATION_WALLET_ADDRESS",
stellarTestnet: "YOUR_DESTINATION_WALLET_ADDRESS",
};
const CHAIN_MAP = {
solanaDevnet: "solana",
baseSepolia: "base-sepolia",
stellarTestnet: "stellar",
};
export default function OnrampCheckout() {
const [order, setOrder] = useState<{ orderId: string; clientSecret: string } | null>(null);
const [chain, setChain] = useState<"solanaDevnet" | "baseSepolia" | "stellarTestnet">("solanaDevnet");
const [email, setEmail] = useState("user@example.com");
const [wallet, setWallet] = useState(TEST_WALLETS.solanaDevnet);
const [amount, setAmount] = useState("5");
const [isLoading, setIsLoading] = useState(false);
function handleChainChange(newChain: "solanaDevnet" | "baseSepolia" | "stellarTestnet") {
setChain(newChain);
setWallet(TEST_WALLETS[newChain]);
}
async function handleSubmit() {
setIsLoading(true);
try {
// Step 1: Link the external wallet to the Crossmint user
await linkWallet({
userEmail: email,
walletAddress: wallet,
chain: CHAIN_MAP[chain],
});
// Step 2: Create the onramp order
const result = await createOrder({ walletAddress: wallet, receiptEmail: email, amount, chain });
setOrder({ orderId: result.order.orderId, clientSecret: result.clientSecret });
} catch (error) {
console.error("Failed to create order:", error);
} finally {
setIsLoading(false);
}
}
if (order != null) {
return (
);
}
```
Import and render the `OnrampCheckout` component in your page:
```tsx app/page.tsx theme={null}
import OnrampCheckout from "./components/OnrampCheckout";
export default function Home() {
return (
);
}
```
That is it! When users fill in their details and click "Continue to Checkout", the app links the external wallet, creates an order, and the embedded checkout component handles KYC, payment, and delivery automatically.
**Testing:** Use the test credit card number `4242 4242 4242 4242` with any future expiration date and any 3-digit CVC.
## Transaction Completion
## Next Steps
# React Native
Source: https://docs.crossmint.com/onramp/quickstarts/react-native
Allow your mobile users to buy stablecoins with cards
In this guide, you will learn how to:
* Install the React Native SDK
* Create an Onramp order for your authenticated users
* Use the embedded checkout component to handle KYC, payment, and delivery
See a full working example.
You can test the Onramp self-serve in staging. To enable it in production, contact sales.
## Prerequisites
* React Native 0.60+
* Expo (recommended) or bare React Native CLI
* iOS 13.0+ / Android 5.0+
* Create a [server-side API key](https://docs.crossmint.com/introduction/platform/api-keys/server-side) with the
`orders.create` and `orders.read` scopes enabled.
Create a [client-side API key](https://docs.crossmint.com/introduction/platform/api-keys/client-side) for the
embedded checkout component.
* Add environment variables to your `.env.local`:
```bash .env.local theme={null}
EXPO_PUBLIC_CROSSMINT_CLIENT_SIDE_API_KEY="_YOUR_CLIENT_API_KEY_"
```
For Expo projects, use the `EXPO_PUBLIC_` prefix. For bare React Native, configure environment variables according to your setup.
## Add Onramp to Your App
If you want to enable onramp orders to external wallets (EOAs or third-party smart wallets) instead of Crossmint wallets, see the [Onramp to External Wallets](/onramp/guides/onramp-to-external-wallets) guide before proceeding.
To initiate an onramp, you must first create a purchase order on your backend. This example uses Express, but you can use any backend framework:
```typescript server/createOrder.ts theme={null}
import express from "express";
const app = express();
app.use(express.json());
const USDC_TOKEN_LOCATORS = {
solanaDevnet: "solana:4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
baseSepolia: "base-sepolia:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
stellarTestnet: "stellar:CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA",
};
app.post("/api/create-order", async (req, res) => {
const { walletAddress, receiptEmail, amount, chain } = req.body;
const serverApiKey = process.env.CROSSMINT_SERVER_SIDE_API_KEY;
if (serverApiKey == null) {
return res.status(500).json({ error: "Server API key not configured" });
}
const response = await fetch("https://staging.crossmint.com/api/2022-06-09/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": serverApiKey,
},
body: JSON.stringify({
lineItems: [
{
tokenLocator: USDC_TOKEN_LOCATORS[chain],
executionParameters: {
mode: "exact-in",
amount,
},
},
],
payment: {
method: "card",
receiptEmail,
},
recipient: {
walletAddress,
},
}),
});
if (!response.ok) {
const error = await response.json();
return res.status(response.status).json(error);
}
const data = await response.json();
res.json(data);
});
app.listen(3000);
```
Now let's go to your app. Create a React Native screen that handles order creation and displays a checkout:
```tsx screens/OnrampScreen.tsx theme={null}
import { useState } from "react";
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ActivityIndicator } from "react-native";
import { CrossmintProvider, CrossmintEmbeddedCheckout } from "@crossmint/client-sdk-react-native-ui";
const CLIENT_API_KEY = process.env.EXPO_PUBLIC_CROSSMINT_CLIENT_SIDE_API_KEY || "";
const API_URL = "http://localhost:3000"; // Replace with your backend URL
// Destination wallets for testing transfers on each network
const TEST_WALLETS = {
solanaDevnet: "",
baseSepolia: "",
stellarTestnet: "",
};
type Chain = "solanaDevnet" | "baseSepolia" | "stellarTestnet";
export default function OnrampScreen() {
const [order, setOrder] = useState<{ orderId: string; clientSecret: string } | null>(null);
const [chain, setChain] = useState("solanaDevnet");
const [email, setEmail] = useState("user@example.com");
const [wallet, setWallet] = useState(TEST_WALLETS.solanaDevnet);
const [amount, setAmount] = useState("5");
const [isLoading, setIsLoading] = useState(false);
function handleChainChange(newChain: Chain) {
setChain(newChain);
setWallet(TEST_WALLETS[newChain]);
}
async function handleSubmit() {
setIsLoading(true);
try {
const response = await fetch(`${API_URL}/api/create-order`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ walletAddress: wallet, receiptEmail: email, amount, chain }),
});
if (!response.ok) {
throw new Error("Failed to create order");
}
const result = await response.json();
setOrder({ orderId: result.order.orderId, clientSecret: result.clientSecret });
} catch (error) {
console.error("Failed to create order:", error);
} finally {
setIsLoading(false);
}
}
if (order != null) {
return (
);
}
return (
Buy USDCNetwork handleChainChange("solanaDevnet")}
>
Solana Devnet
handleChainChange("baseSepolia")}
>
Base Sepolia
handleChainChange("stellarTestnet")}
>
Stellar Testnet
EmailRecipient WalletAmount (USD)
{isLoading ? (
) : (
Continue to Checkout
)}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 24,
backgroundColor: "#fff",
},
checkoutContainer: {
flex: 1,
backgroundColor: "#fff",
},
title: {
fontSize: 24,
fontWeight: "600",
color: "#111",
marginBottom: 24,
},
fieldGroup: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: "500",
color: "#111",
marginBottom: 8,
},
toggleRow: {
flexDirection: "row",
gap: 8,
},
toggleButton: {
flex: 1,
padding: 12,
borderRadius: 8,
borderWidth: 1,
borderColor: "#d1d5db",
backgroundColor: "#fff",
alignItems: "center",
},
toggleButtonActiveSolana: {
backgroundColor: "#16a34a",
borderColor: "#16a34a",
},
toggleButtonActiveBase: {
backgroundColor: "#2563eb",
borderColor: "#2563eb",
},
toggleButtonActiveStellar: {
backgroundColor: "#9333ea",
borderColor: "#9333ea",
},
toggleText: {
fontSize: 14,
fontWeight: "500",
color: "#111",
},
toggleTextActive: {
color: "#fff",
},
input: {
borderWidth: 1,
borderColor: "#d1d5db",
borderRadius: 8,
padding: 12,
fontSize: 14,
color: "#111",
backgroundColor: "#fff",
},
monoInput: {
fontFamily: "monospace",
},
submitButton: {
backgroundColor: "#000",
padding: 16,
borderRadius: 8,
alignItems: "center",
marginTop: 8,
},
submitButtonDisabled: {
opacity: 0.5,
},
submitButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "500",
},
});
```
Import and render the `OnrampScreen` in your app:
```tsx App.tsx theme={null}
import { SafeAreaView, StyleSheet } from "react-native";
import OnrampScreen from "./screens/OnrampScreen";
export default function App() {
return (
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
});
```
That is it! When users fill in their details and tap "Continue to Checkout", the API creates an order and the embedded checkout component handles KYC, payment, and delivery automatically.
**Testing:** Use the test credit card number `4242 4242 4242 4242` with any future expiration date and any 3-digit CVC.
## Transaction Completion
## Next Steps
# Swift
Source: https://docs.crossmint.com/onramp/quickstarts/swift
Allow your iOS users to buy stablecoins with credit cards and Apple Pay
In this guide, you'll learn how to:
* Install the Swift SDK
* Create an Onramp order for your authenticated users
* Use the embedded checkout component to handle KYC, payment, and delivery
View the open source Swift Checkout SDK repository.
## Prerequisites
* **iOS 14.0+**
* **SwiftUI** framework
* Xcode 13.0 or later
Install the Crossmint Swift SDK using Swift Package Manager.
In Xcode, go to **File > Add Package Dependencies** and add:
```text theme={null}
https://github.com/Crossmint/crossmint-checkout-swift
```
Select the `CrossmintCheckout` product for your target.
* Create a [server-side API key](https://docs.crossmint.com/introduction/platform/api-keys/server-side) with the
`orders.create` and `orders.read` scopes enabled. In staging, all scopes are included in your API key.
## Add Onramp to Your App
If you want to enable onramp orders to external wallets (EOAs or third-party smart wallets) instead of Crossmint wallets, see the [Onramp to External Wallets](/onramp/guides/onramp-to-external-wallets) guide before proceeding.
Use the [Create Order API](/api-reference/headless/create-order) to initiate the purchase process from your backend.
Once you have the order response with `orderId` and `clientSecret`, use Crossmint's embedded checkout component in your SwiftUI view.
The component supports **native Apple Pay** for a seamless iOS payment experience.
```swift theme={null}
import SwiftUI
import CrossmintCheckout
struct ContentView: View {
var body: some View {
CrossmintEmbeddedCheckout(
orderId: ,
clientSecret: ,
payment: CheckoutPayment(
crypto: CheckoutCryptoPayment(enabled: false),
fiat: CheckoutFiatPayment(
enabled: true,
allowedMethods: CheckoutAllowedMethods(
googlePay: false,
applePay: true, // Native Apple Pay support
card: true
)
)
),
appearance: CheckoutAppearance(
rules: CheckoutAppearanceRules(
destinationInput: CheckoutDestinationInputRule(display: "hidden"),
receiptEmailInput: CheckoutReceiptEmailInputRule(display: "hidden")
)
)
)
}
}
```
**Key features:**
* **Native Apple Pay**: Smooth integration with iOS payment system
* **SwiftUI Components**: Modern declarative UI framework
* **Automatic KYC**: Handles verification flow automatically
* **Customizable Appearance**: Control visibility of UI elements
## Transaction Completion
## Next Steps
* View the Swift Checkout SDK source code on GitHub
# NFT Drops Gated by Accesslists
Source: https://docs.crossmint.com/payments/advanced/accesslists
Allow purchasing in accesslist-protected NFT drops
## Access Lists
Accesslist support is a premium feature. To get started, [contact sales](https://www.crossmint.com/contact/sales)
Crossmint allows you to whitelist both wallet and email addresses. Once the team
has approved your request, you will need to provide the following information:
1. Your Crossmint `clientId` or `collectionId` for the collection.
2. The list of wallet and email addresses to be included in the accesslist.
If you whitelist users' email addresses, Crossmint will create wallets associated with those email addresses and
provide them to you for inclusion in the accesslist. Users will then be able to authenticate during the purchase and
execute the transaction.
You can set up accesslists using Merkle Trees or inserting the list of whitelisted addresses directly on the smart contract:
### A. Merkle Tree Accesslists
While using Merkle Trees is more gas-efficient, it involves a slightly more complex setup.
Here is an example accesslist mint function:
```solidity Solidity theme={null}
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract Accesslist is ERC721, Ownable {
bytes32 public merkleRoot;
uint256 public nextTokenId;
mapping(address => bool) public claimed;
constructor() ERC721("ExampleNFT", "NFT") {
merkleRoot = 0x0;
}
function isWhiteListed(bytes32[] memory proof, bytes32 leaf) public view returns(bool) {
return MerkleProof.verify(proof, merkleRoot, leaf);
}
function setMerkleRoot(bytes32 merkleRoot_) external onlyOwner {
merkleRoot = merkleRoot_;
}
function mint(address to, bytes32[] calldata merkleProof) public payable {
require(claimed[to] == false, "already claimed");
claimed[to] = true;
require(
isWhiteListed(
merkleProof,
keccak256(abi.encodePacked(to))
),
"invalid merkle proof"
);
nextTokenId++;
_mint(to, nextTokenId);
}
}
```
### B. Mapping-Based Accesslists
The most straightforward method of whitelisting involves maintaining
a mapping of the whitelisted addresses within your smart contract.
This approach is faster but consumes more gas and savvy developers
may be able to determine the addresses of the accesslist.
```solidity Solidity theme={null}
mapping(address => uint256) public whitelistMapping;
```
```solidity Solidity theme={null}
function presale(address _to, uint256 _count) external payable {
// ensure _to address has tokens left to mint
require(whitelistMapping[_to] > 0, "no whitelist tokens for user");
// decrement mapping for user
whitelistMapping[_to] -= _count;
// presale minting logic here
}
```
You can set up accesslists on your Candy Machine using Merkle Trees or SPL Tokens
### A. Merkle Tree Accesslists
Simply send the user IDs to Crossmint. Once you have the Merkle proof, you can pass it into the Crossmint Hosted Checkout `lineItems.callData` as a string, as in the example below:
```jsx React theme={null}
```
### B. SPL Token-based Accesslists
You can whitelist wallets by airdropping them SPL tokens.
## Geofencing
Geofencing support is a premium feature only available to customers on a paid plan. To get started, [contact
sales](https://www.crossmint.com/contact/sales)
Crossmint supports geofencing capabilities through our Customer Success Engineering (CSE) team. Our CSE team can work with you to block transactions from specific countries or geographic regions via Stripe rules.
To set up geofencing for your project, please contact our sales team to discuss your specific requirements and implementation details.
# NFT Drops with Dynamic Price or Quantity
Source: https://docs.crossmint.com/payments/advanced/dynamic-quantity-and-price
Details about implementing dynamic functionality into the Checkout
See the below examples to understand how to implement dynamic pricing or quantity into your dApp.
The examples below use the [Hosted Checkout](/payments/pay-button/overview), but the same approach is compatible
with the [Embedded Checkout](/payments/embedded/overview) as well.
```jsx React theme={null}
import { useState } from 'react';
import { CrossmintProvider, CrossmintHostedCheckout } from "@crossmint/client-sdk-react-ui";
function App() {
const [mintAmount, setMintAmount] = useState(1);
const nftCost = 0.001;
const apiClientId = '_YOUR_API_CLIENT_ID_';
const collectionId = '_YOUR_COLLECTION_ID_';
const handleDecrement = () => {
if (mintAmount <= 1) return;
setMintAmount(mintAmount - 1);
}
const handleIncrement = () => {
if (mintAmount >= 3) return;
setMintAmount(mintAmount + 1);
}
return (
<>
>
);
}
export default App;
```
# Improving Conversion
Source: https://docs.crossmint.com/payments/advanced/improving-conversion
Best practices to maximize checkout conversion
Here are a selection of best practices you can follow to optimize conversion on your checkout flow.
## 1. Ensure your checkout has the right locale
Improper locales can significantly reduce conversion. Double check that your checkout is configured to work in the locales you have customers.
You can manage the locale in your [integrated checkout components](/sdk-reference/checkout/react/components), or your [payment order objects.](/payments/headless/guides/order-lifecycle/quote-phase#update-the-locale)
## 2. Choose the best payment methods
You may have data that shows your customers convert better with one payment method over another one. Customizing your checkout to feature these preferred payment methods by default is a good way to improve your conversion.
**Embedded Checkout** - Set the default payment method as your customers most preferred one (i.e. Apple Pay, Sol, etc.)
**Headless Checkout** - you have full control over your UI. Put the payment method your customers prefer as default option, and pre-fill as much information as you can.
## 3. Further customize your checkout flow
You may know your customer very well, and what exactly increases conversion for them: large images, progress bars, loyalty point reward offers.
Building these bespoke checkout experiences can increase your conversion. [Embedded](/payments/embedded/overview) or [headless checkout](/payments/headless/overview) are more customizable checkout tools.
# NFT Marketplaces & Launchpads
Source: https://docs.crossmint.com/payments/advanced/marketplaces-and-launchpads
Add fiat and cross-chain crypto payments to your platform
Crossmint offers additional services to marketplaces and launchpads,
including:
* Custom branding and email receipts
* Dedicated Slack channel with Crossmint's support team
* Custom SLAs
* APIs to register new collections programmatically
* Increased purchase limits
* Cart mode and ability to sweep the floor
...and more
### Integrating Crossmint into a marketplace
Integrating Crossmint into a marketplace can be as simple as
adding five lines of code, depending on the level of customization required.
The following snippet would be sufficient to render a button that enables
the purchase of one of your listings with credit card.
```jsx EVM theme={null}
```
```jsx Solana theme={null}
```
[Contact sales](https://www.crossmint.com/contact/sales) to get started.If you are building a new marketplace, use one of the supported contracts for a faster integration. If you are looking for help building a marketplace, [contact us](https://www.crossmint.com/contact/sales) and the Crossmint team will introduce you to the right partner.
### FAQs
Crossmint supports a wide range of contracts across chains, including:
| Blockchain | Contract |
| :- | :- |
| EVM | Rarible, Seaport, OpenSea, Zora, LooksRare, 0x, Foundation, CoinbaseNFT, Sudoswap, Manifold |
| Solana | Auction House, Magic Eden API, Exchange.art |
Custom integrations available only for enterprise clients.
You can see a live implementation of the payment button on [Tensor](https://tensor.trade/).
The default transaction limit is 7,000 USD. If you need a higher limit, [contact us](https://www.crossmint.com/contact/sales)
### Integrating Crossmint into a launchpad
The integration is identical to that of the standard digital asset checkout. However, Crossmint offers additional services to improve the creator experience, including the ability to register collections via API.
[Contact sales](https://www.crossmint.com/contact/sales) to get started.
### FAQs
Crossmint powers most major launchpads across chains. You can see in action on [Manifold](https://app.manifold.xyz/c/sleeping-potato) and [Highlight](https://highlight.xyz/mint/6525a4b606bc6fa5b8acdcfa).
For primary sales, Crossmint requires creators to do light KYC and confirm their project meets Crossmint's [content policy](https://drive.google.com/file/d/1kXv5gX7C8xHJFDAnCQTNUmXrDP05YYk1/view). There are multiple options to achieve this:
* You can conduct project verification yourself.
* You can send creators to register their collection at Crossmint.com and share their collectionID with you, so you can pass it to the digital asset checkout.
* (Coming soon) You can use Crossmint's collection verification SDK to verify creators directly on your site.
# Mint to Specific Template
Source: https://docs.crossmint.com/payments/advanced/mint-to-specific-template
Specify which NFT template to mint from during checkout (for Managed collections only)
## Overview
When minting NFTs from a collection with multiple templates, you can specify which template to mint to using your Crossmint integration. This feature must be explicitly enabled in the Crossmint Console.
This gives you control over exactly which template gets minted when processing a purchase, rather than relying on random template selection.
## Supported Collection Types
Selecting a specific token template for purchase is supported on:
* EVM ERC-721 and ERC-1155 collections
* Solana collections
In the following chains, tokens will always be selected randomly:
* Aptos collections
## Enabling Template Minting
Follow these steps in the Crossmint Console to enable template-specific minting:
1. Go to your collection in the Crossmint Console
2. Navigate to the "Checkout" section
3. Under "NFT Template Settings", enable "Mint to specific NFT Template"
## Using Template IDs
If template-specific minting is disabled for your collection and you pass a template ID in the collection locator,
the request will select a template randomly. Make sure to enable this feature in the Console before using template IDs.
To see all your available templates, navigate to the "NFTs" section in the Console for your managed collection.
You can also get all your available templates for a collection using the [NFT Template API](/api-reference/minting/template/get-all-templates).
Once enabled, specify which template to mint by adding the template ID to your collection locator. The collection locator format without templateId is:
```
crossmint:
```
and with templateId is:
```
crossmint::
```
The `templateId` portion is optional with mint to specific template enabled. If omitted, the system will use your collection's default minting behavior.
Here's how to implement template minting in your integration:
Add the template ID to your collection locator string:
```jsx theme={null}
```
Add the template ID to your collection locator string when creating an order:
```json theme={null}
{
"payment": {
"method": "card" // or your preferred payment method
},
"lineItems": [
{
// For a specific template:
"collectionLocator": "crossmint::",
// Or for random template selection:
// "collectionLocator": "crossmint:",
"callData": {
"totalPrice": "0.001"
}
}
]
}
```
## Error Handling
When implementing template minting, handle these potential error cases:
1. **Invalid Template ID**
* Error message: Transaction fails with template validation error
* Occurs when: The specified template ID doesn't exist in the collection
* Resolution: Verify the template ID exists in your collection
Here's how to handle these errors in your code:
```jsx theme={null}
try {
{
if (error.message.includes(`Template ${templateId} was not found in collection`)) {
// Invalid template ID
throw new Error("Invalid template ID provided");
}
throw error
}}
/>
} catch (error) {
// Handle other errors
console.error("Minting failed:", error);
}
```
```typescript theme={null}
try {
const response = await fetch("/api/create-order", {
method: "POST",
body: JSON.stringify({
lineItems: [
{
collectionLocator: `crossmint:${collectionId}:${templateId}`,
callData: {
totalPrice: "0.001"
}
}
]
})
});
if (!response.ok) {
const error = await response.json();
if (error.message.includes(`Template ${templateId} was not found in collection`)) {
// Invalid template ID
throw new Error("Invalid template ID provided");
}
throw error;
}
} catch (error) {
// Handle the error appropriately
console.error("Order creation failed:", error);
}
```
# Receipt Customization
Source: https://docs.crossmint.com/payments/advanced/receipt-customization
Customize your users' order receipt
Customizing the order receipt that gets sent to your users' email addresses is possible under our Enterprise plan.
## Elements that can be customized
* **Subject**: the subject of the email.
* **Header**: the header of the email.
* **Body**: the body of the receipt within the email.
* **Company logo**: you can add your company logo.
* **Remove Crossmint's links**: you can also remove Crossmint's social links and buttons (included in the default receipt sent).
* **Remove transaction hash**: you can also remove the transaction hash (included in the default receipt sent).
## Elements that cannot be customized
* You cannot include the user's name in the receipt as this information is not always available.
* You cannot remove the "Payment powered by Crossmint" message on the footer for compliance.
Please contact your Crossmint customer success engineer and provide them with the above information.
Here is an example of the default purchase receipt that is emailed to users:
Here is an example of a customized purchase receipt that is emailed to users:
# Collection Registration and Verification API
Source: https://docs.crossmint.com/payments/advanced/registration-and-verification-api
Launchpads now use Crossmint's API to register and verify a collection.
Crossmint provides an API to facilitate collection registration and verification. This functionality is for launchpads that want to use Crossmint's checkout and allow their users to go through the KYC and collection verification flow.
## Before you get started:
Contact your Crossmint customer success engineer to request the `collections.write` scope be added to an existing API key. You will need to provide the projectId associated with the API key you will be using.
Your contract also needs to meet certain requirements. Please refer to the [Register External Collection](/payments/guides/register-collection) documentation page for further information.
Please do not share your API key secret with the Crossmint team or anyone else.
## Collection Registration and Verification process
Once a user has created a collection on your launchpad, you can optionally choose to register the collection with Crossmint to enable Credit Card payments. This API abstracts away much of the complexity involved from the user when they perform KYC and the collection verification process.
As the launchpad, you will pass information about the collection as parameters to the API. Crossmint will then return a URL where the user can complete the KYC and collection verification. This API call's structure is as follows:
Sample code for the collection registration and verification API. Click on "Response" to see what a successful response will look like.
```jsx JavaScript theme={null}
// Define the API URL
const apiUrl = "https://staging.crossmint.com/api/v1-alpha1/collections";
// Set up the request body with actual data instead of placeholders
const requestBody = {
chain: "ethereum", // replace with actual chain name
contractType: "erc-721", // choose between "erc-721" or "erc-1155" or "candy-machine"
args: {
contractAddress: "0x123...", // replace with actual contract address
mintFunctionName: "mintUSDC(address,uint256)", // specify the mint function
abi: [], // provide the actual contract ABI array
toParamName: "toAddress", // specify the 'to' parameter name in the mint function
erc20MintCurrency: "usdc", // the ERC20 currency to be used for minting
},
metadata: {
title: "", // replace '' with the actual collection name
description: "", // replace `` with the actual collection's description
imageUrl: "", // replace '' with actual URL to an image
},
ownership: "external", // optional - if you are regestering the collection on behalf of a creator or yourself, choose "external". Else, choose "self".
category: "art", // specify the verification category - this expedites the collection review
scopes: ["payments:credit-card"], // required scopes- "payments:cross-chain" or "payments:credit-card", must specify at least one
};
// Set up the request options
const requestOptions = {
method: "POST",
headers: {
'Content-Type': 'application/json',
'X-API-KEY': '', // replace '' with the API Key that has the scope `collections.write`
},
body: JSON.stringify(requestBody), // Convert the JavaScript object to a JSON string
};
// Make the fetch request
fetch(apiUrl, requestOptions)
.then((response) => response.json()) // Parsing the JSON response
.then((data) => console.log("Success:", data))
.catch((error) => console.error("Error:", error));
```
```json Response (Successful) theme={null}
{
"collectionId": ""
}
```
The returned URL must be the URL that your user needs to be directed to. The user can complete the verification with that URL.
## Get Collection status
You can check the status of your user's verification with the get collection status endpoint. Upon a successful request, the verification `status` for the `seller` and `collection` may have either of the following values:
* `verified` - The user has successfully completed the verification.
* `failed` - The verification failed.
* `crossmint-review` - It is being reviewed by the Crossmint team.
* `unverified` - The user has not initiated or completed the verification process.
```jsx JavaScript theme={null}
// Replace '' with the collectionId returned in the successful POST response
const collectionId = 'your_collection_id'; // Replace 'your_collection_id' with the actual ID
const apiUrl = `https://staging.crossmint.com/api/v1-alpha1/collections/${collectionId}`;
// Set up the request options
const requestOptions = {
method: 'GET',
headers: {
'X-API-KEY': '', /// replace '' with your API Key
'Content-Type': 'application/json',
}
};
// Make the fetch request
fetch(apiUrl, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.statusText);
}
return response.json(); // Parse the JSON from the response
})
.then(data => console.log('Success:', data)) // Handle the parsed data
.catch(error => console.error('Error:', error)); // Handle any errors
```
```json Response (Successful) theme={null}
{
"verificationStatus": {
"seller": {
"status": "verified"
},
"collection": {
"status": "verified"
}
}
}
```
***
## FAQs
You can find all the possible inputs below:
```json VerificationCategory inputs theme={null}
VerificationCategory {
LOYALTY: "loyalty",
ART: "art",
MUSIC: "music",
GAMING: "gaming",
TICKETING: "ticketing",
CHARITY: "charity",
OTHER: "other",
}
```
The verification requirement varies by the payment type selected for the collection.
* Credit card: Seller and collection status must be “verified”.
* Crypto: Only collection status must be “verified”.
# Checking Supported Tokens
Source: https://docs.crossmint.com/payments/advanced/supported-tokens
Learn how to check if a memecoin or token is supported by Crossmint Checkout
## Introduction
Before integrating a memecoin or token using Crossmint's checkout, it's important to verify that it's supported by Crossmint. This guide will show you how to use the Crossmint API to check if a specific token is supported and what features are available for it.
## Prerequisites
Get your API keys from the [Crossmint Console](https://www.crossmint.com/console/projects/apiKeys)
[More info on creating API keys here](/introduction/platform/api-keys#how-to-get-and-use-api-keys)
Make sure the `orders.create` scope is enabled for your API key.
## Getting Started
The Supported Tokens API allows you to query the status of any token to determine if it's supported by Crossmint and what features are available.
* **Endpoint**: `https://www.crossmint.com/api/v1-alpha2/tokens/:tokenLocator`
* **Method**: GET
* **Authentication**: Requires either a server or client API key
### Token Locator Format
Memecoin checkout supports the **Solana** and **Base** networks.
The token locator follows this format:
```
network:tokenAddress
```
Example:
* **Staging**: Use the XMEME test token: `solana:7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu`
* **Production**: Use any supported token, e.g. TRUMP: `solana:6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN`
Production memecoin tokens **will not work** in the **staging** environment.
## Checking Token Support
### Using JavaScript
Here's how to check if a token is supported using JavaScript:
```javascript theme={null}
// Example usage
const apiKey = "your-api-key"; // Replace with your actual API key
const tokenLocator = "solana:6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN"; // Example: TRUMP token
const checkTokenSupport = async (apiKey, tokenLocator) => {
try {
const response = await fetch(`https://www.crossmint.com/api/v1-alpha2/tokens/${tokenLocator}`, {
method: "GET",
headers: {
"x-api-key": apiKey,
"Content-Type": "application/json",
},
});
const data = await response.json();
return data;
} catch (error) {
console.error("Error checking token support:", error);
throw error;
}
};
checkTokenSupport(apiKey, tokenLocator)
.then((result) => {
console.log("Token support details:", result);
if (result.available) {
console.log("This token is supported by Crossmint!");
} else {
console.log("This token is not currently supported.");
}
console.log("Available features:", result.features);
})
.catch((err) => {
console.error("Failed to check token support:", err);
});
```
### Sample Response
A successful response will look similar to this:
```json theme={null}
{
"token": "solana:6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN",
"available": true,
"features": {
"creditCardPayment": true
}
}
```
## Integrating with Your Checkout Flow
Once you've confirmed that a token is supported, you can use the information from the API response to customize your checkout experience.
### Example: Pre-checkout Validation
Here's an example of how to incorporate token support checking before initiating a checkout:
```javascript theme={null}
const initiateCheckout = async (apiKey, tokenLocator, amount, recipientWallet) => {
// First, check if the token is supported
const tokenSupport = await checkTokenSupport(apiKey, tokenLocator);
if (!tokenSupport.available) {
throw new Error("This token is not currently supported by Crossmint.");
}
if (!tokenSupport.features.creditCardPayment) {
throw new Error("Credit card payment is not available for this token.");
}
// If we get here, the token is supported with credit card checkout
// Proceed with creating an order
const orderResponse = await fetch("https://www.crossmint.com/api/2022-06-09/orders", {
method: "POST",
headers: {
"x-api-key": apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
lineItems: [
{
tokenLocator: tokenLocator,
executionParameters: {
mode: "exact-in",
amount: amount,
maxSlippageBps: "500",
},
},
],
payment: {
method: "card",
receiptEmail: "user@example.com", // Replace with user's email
},
recipient: {
walletAddress: recipientWallet,
},
}),
});
return await orderResponse.json();
};
```
## Best Practices
1. **Cache Results (with caution)**: Token support status can change, but for frequently accessed tokens, consider caching results for a short period (e.g., 1 hour) to reduce API calls.
2. **Handle Errors Gracefully**: Implement proper error handling to provide clear feedback to users when a token is not supported.
3. **Check Before Displaying Options**: Only display payment methods that are actually available for the token.
## Next Steps
Now that you can check token support, learn how to implement a credit card checkout for memecoins
Understand the lifecycle of orders in Crossmint's checkout flow
## FAQ
Yes, if you'd like to request support for a token that is currently unsupported, contact our [support team](https://help.crossmint.com).
Transactions for unsupported tokens will fail. This is why it's crucial to check token support before initiating
a checkout process.
No, each token may have different available payment features. The API response includes a `features` object that
specifies which payment methods are available for each token.
The API is rate-limited to 50 requests per second. For most applications, this should be more than sufficient.
# Testing Tips
Source: https://docs.crossmint.com/payments/advanced/testing-tips
This document is intended to share some of the best practices and tips for testing.
## Limits in Staging
In order to preserve testnet ETH/tokens, Crossmint imposes very low upper bound limits on NFT prices in the staging
environment. The default limit on production is USD $0.75 - $1,500 per transaction.
To avoid issues, set the `totalPrice` and the onchain NFT price to no more than:
* Ethereum (Sepolia): 0.000005 ETH (this applies for other EVM chains as well)
* Polygon (Amoy): 0.005 MATIC
* Solana (Devnet): 0.001 SOL
* USDC (all chains): \$10
* Other chains: the equivalent of \$0.10 in the native token
The minimum credit card charge amount is \$0.50. Even though the price of the NFT at the above rates will often be
less the \$0.01, the checkout will still show \$0.50. This will be less of an issue on production when you're
charging full price.
## Test Credit Card Numbers
When testing the Checkout and credit card payments on staging you'll need to use test card numbers. You can use these cards to test the flow end-to-end without actually processing real payments.
You can use the following test credit card numbers:
* `4242424242424242` (Visa)
* `4000056655665556` (Visa Debit)
* `5555555555554444` (Mastercard)
* `5200828282828210` (Mastercard Debit)
For `CVV` or `CVC`, use any 3 digit number
For `Expiry Date` or `MM/YY`, enter any future date
### Advanced Card Testing
If you want to simulate purchases from [different countries](https://stripe.com/docs/testing?testing-method=card-numbers#international-cards), [decline conditions](https://stripe.com/docs/testing?testing-method=card-numbers#declined-payments), or other [brands](https://stripe.com/docs/testing?testing-method=card-numbers#cards) refer to the [Stripe documentation](https://stripe.com/docs/testing?testing-method=card-numbers) for more test card numbers.
## Reviewing Orders in the Developer Console
You can get insights into the purchases being made against your collection by navigating to the Orders tab for your collection in the Developer Console. You can use this to get insights about orders in production, but also to see the status of orders when testing out your integration in the staging environment.
When looking for pre-completion phase orders while testing [Headless Checkout](/payments/headless/overview), you'll
need to enter the order ID in the search box. This is because the console view purposefully has a reduced list of
status options for filtering.
# NFT Drops Priced in USDC
Source: https://docs.crossmint.com/payments/advanced/usdc-support
How to set up Crossmint if your collection is denominated in USDC
By denominating your collection in USDC, you can eliminate price volatility, and offer a more accessible experience
to users.
## Ensure you are using a supported USDC token
First of all, make sure your collection is denominated in one of the USDC token addresses supported by Crossmint.
Chain
Network
USDC Token Address
USDXM Token Address
USDC.e is only supported on previously registered integrations on Polygon mainnet. Please use the native USDC
address for new projects.
If you need some testnet USDC you can mint it freely from the contract or from this faucet app that uses the same
testnet token contracts as the Checkout tools.
[Crossmint Testnet USDXM Faucet](https://usdc-faucet.vercel.app/)
## Validate your collection meets the requirements
In addition to the standard requirements, keep in mind:
1. **The mint function should be `nonpayable`.**
2. **The USDC token has 6 decimals** instead of 18 like ETH, Matic, and most other tokens.
3. **The `totalPrice` attribute in the `callData` of your `lineItems` should be in units of USDC**. If your intended price is 100 USD, then you will set `totalPrice` to `100` e.g. `totalPrice="100"`.
4. **Your ERC-20 transfer call must request the funds from `msg.sender`** instead of the address parameter. This allows Crossmint to pay from our fleet of treasury wallets and deliver the NFTs directly to the user's wallet. See an example mint function below.
```solidity Solidity theme={null}
function mintUSDC(address _to) public {
// pre payment logic here
// note that you need to transfer from msg.sender, NOT the _to address
tokenInstance.transferFrom(msg.sender, address(this), priceUSDC );
// actual minting logic here
}
```
```tsx theme={null}
```
The `totalPrice` attribute in the `callData` of your `lineItems` should be in units of USDC. If your intended price is 100 USD, then you will set `totalPrice` to `100` e.g. `totalPrice="100"`.
Denominate your candy machine on USDC and Crossmint will automatically detect it. You can learn how on this [guide](https://medium.com/crossmint-tech/how-to-create-a-fixed-price-usdc-nft-drop-on-solana-2ab251e61384).
# Webhooks
Source: https://docs.crossmint.com/payments/advanced/webhooks
Track order lifecycle events and payment status with webhooks
# Webhooks Overview
Webhooks allow you to track the status of payments and order lifecycle events in your application. They provide real-time notifications for various events like checkout, NFT minting, and order fulfillment.
## Webhook Systems by Checkout Version
Crossmint offers different webhook systems for Checkout V2 and V3:
### Checkout V2 Webhooks (Legacy)
The Checkout V2 webhook system provides basic payment tracking with a single event type:
* `purchase.succeeded` - Triggered when an NFT has been successfully purchased and delivered
### Checkout V3 Webhooks (Current)
The Checkout V3 webhook system offers comprehensive order lifecycle tracking with multiple event types:
**Quote Phase**
* `orders.quote.created` - Triggered when a new order is created
* `orders.quote.updated` - Triggered when order details are modified
**Payment Phase**
* `orders.payment.succeeded` - Triggered when payment is successfully processed
* `orders.payment.failed` - Triggered when payment fails
**Delivery Phase**
* `orders.delivery.initiated` - Triggered when delivery begins
* `orders.delivery.completed` - Triggered when delivery succeeds
* `orders.delivery.failed` - Triggered when delivery fails
## Setting Up Webhooks
### 1. Create an endpoint route
Using a standard nodejs API server, create an endpoint.
You can test locally by installing [ngrok](https://ngrok.com/docs/getting-started/) and creating a routed endpoint
to a specified port. > **Note**: Use ngrok only for testing. In production, ensure your endpoint is properly secured
with HTTPS and appropriate access controls.
### 2. Configure the endpoint
Your endpoint should:
* Handle POST requests only
* Parse webhook events from the request body
* Respond with a `200` status code to acknowledge receipt
Your server must return a 2xx HTTP status quickly so the webhook is marked as delivered.
Example handler:
```javascript theme={null}
// endpoint.js
export default function handler(req, res) {
if (req.method === "POST") {
console.log(`[webhook] Event received:`, req.body);
}
res.status(200).json({});
}
```
Don't be strict with payload validations as Crossmint may add new fields to the webhooks as products evolve.
Your server must return a 2xx HTTP status quickly so the webhook is marked as delivered.
### 3. Example Webhook Responses
Every Checkout V3 webhook (`orders.*`) is delivered as `{ actionId, type, data }`:
* `actionId` — the order identifier
* `type` — the event name (for example, `orders.delivery.completed`)
* `data` — the complete order object, identical to the [Get Order API](/api-reference/headless/get-order) response
The fields inside `data.lineItems` depend on the order execution mode. `exact-out` orders (for example, NFT purchases) carry `callData` (when present) and `quantity`. `exact-in` orders (for example, token or memecoin purchases) carry `executionParams` and `maxSlippageBps`.
```json EVM theme={null}
{
"type": "purchase.succeeded",
"status": "success",
"walletAddress": "",
"projectId": "",
"collectionId": "",
"clientId": "",
"txId": "",
"contractAddress": "",
"tokenIds": [], // only present for EVM collections
"passThroughArgs": "" // only if whPassThroughArgs set
}
```
```json Solana theme={null}
{
"type": "purchase.succeeded",
"status": "success",
"walletAddress": "",
"clientId": "",
"txId": "",
"mintAddress": "",
"passThroughArgs": ""
}
```
```json theme={null}
{
"actionId": "7723139d-fba3-474d-8e52-0ac7512d5c7b",
"type": "orders.quote.created",
"data": {
"orderId": "7723139d-fba3-474d-8e52-0ac7512d5c7b",
"phase": "payment",
"locale": "en-US",
"lineItems": [
{
"chain": "polygon",
"executionMode": "exact-out",
"quantity": 1,
"callData": { "quantity": 1 },
"metadata": {
"name": "Collection Name",
"description": "Collection Description",
"imageUrl": "https://..."
},
"quote": {
"status": "valid",
"charges": { "unit": { "amount": "0.50", "currency": "usd" } },
"totalPrice": { "amount": "0.50", "currency": "usd" }
},
"delivery": {
"status": "awaiting-payment",
"recipient": {
"locator": "email:user@example.com:polygon",
"email": "user@example.com",
"walletAddress": "0x1234..."
}
}
}
],
"quote": {
"status": "valid",
"quotedAt": "2024-11-20T12:00:00.000Z",
"expiresAt": "2024-11-20T13:00:00.000Z",
"totalPrice": { "amount": "0.50", "currency": "usd" }
},
"payment": {
"status": "awaiting-payment",
"method": "stripe-payment-element",
"currency": "usd",
"receiptEmail": "user@example.com"
}
}
}
```
```json theme={null}
{
"actionId": "7723139d-fba3-474d-8e52-0ac7512d5c7b",
"type": "orders.payment.succeeded",
"data": {
"orderId": "7723139d-fba3-474d-8e52-0ac7512d5c7b",
"phase": "delivery",
"locale": "en-US",
"lineItems": [
{
"chain": "polygon",
"executionMode": "exact-out",
"quantity": 1,
"callData": { "quantity": 1 },
"metadata": { "name": "Collection Name", "imageUrl": "https://..." },
"quote": {
"status": "valid",
"totalPrice": { "amount": "0.50", "currency": "usd" }
},
"delivery": {
"status": "in-progress",
"recipient": {
"locator": "email:user@example.com:polygon",
"email": "user@example.com",
"walletAddress": "0x1234..."
}
}
}
],
"quote": {
"status": "valid",
"totalPrice": { "amount": "0.50", "currency": "usd" }
},
"payment": {
"status": "completed",
"method": "stripe-payment-element",
"currency": "usd",
"receiptEmail": "user@example.com",
"received": { "amount": "0.50", "currency": "usd" }
}
}
}
```
```json theme={null}
{
"actionId": "7723139d-fba3-474d-8e52-0ac7512d5c7b",
"type": "orders.delivery.completed",
"data": {
"orderId": "7723139d-fba3-474d-8e52-0ac7512d5c7b",
"phase": "completed",
"locale": "en-US",
"lineItems": [
{
"chain": "polygon",
"executionMode": "exact-out",
"quantity": 1,
"callData": { "quantity": 1 },
"metadata": { "name": "Collection Name", "imageUrl": "https://..." },
"quote": {
"status": "valid",
"totalPrice": { "amount": "0.50", "currency": "usd" }
},
"delivery": {
"status": "completed",
"recipient": {
"locator": "email:user@example.com:polygon",
"email": "user@example.com",
"walletAddress": "0x1234..."
},
"txId": "0x2e69f11dae7869b92e3d5eaf4cadd50c48b5c6803d1232815f979d744521ad4c",
"tokens": [
{
"locator": "polygon:0xE04Cf294985282Ddc088E6433c064cfB85eD9EdA:3",
"contractAddress": "0xE04Cf294985282Ddc088E6433c064cfB85eD9EdA",
"tokenId": "3"
}
]
}
}
],
"quote": {
"status": "valid",
"totalPrice": { "amount": "0.50", "currency": "usd" }
},
"payment": {
"status": "completed",
"method": "stripe-payment-element",
"currency": "usd",
"receiptEmail": "user@example.com",
"received": { "amount": "0.50", "currency": "usd" }
}
}
}
```
```json theme={null}
{
"actionId": "2795199f-f599-44c4-84dc-38d944853a05",
"type": "orders.delivery.completed",
"data": {
"orderId": "2795199f-f599-44c4-84dc-38d944853a05",
"phase": "completed",
"locale": "en-US",
"lineItems": [
{
"chain": "base",
"executionMode": "exact-in",
"executionParams": {
"mode": "exact-in",
"amount": "5",
"contractAddress": "0x4ed4e862860bed51a9570b96d89af5e1b0efefed",
"quantity": 1
},
"maxSlippageBps": "50",
"metadata": {
"name": "DEGEN",
"description": "DEGEN is a community-driven token..."
},
"quote": {
"status": "valid",
"totalPrice": { "amount": "5", "currency": "usd" }
},
"delivery": {
"status": "completed",
"recipient": {
"locator": "base:0x24573a80ae60c0e75735843f119ab4623a45e523",
"walletAddress": "0x24573a80ae60c0e75735843f119ab4623a45e523"
},
"txId": "0x8e20e4b1edff7375fb5d4fc87f6cdb66aa03573e725d5eacc7053800a4f1c11b",
"tokens": [
{
"locator": "base:0x4ed4e862860bed51a9570b96d89af5e1b0efefed:0",
"contractAddress": "0x4ed4e862860bed51a9570b96d89af5e1b0efefed",
"tokenId": "0",
"quantity": "50000000000000000000",
"symbol": "DEGEN",
"decimals": 18
}
]
}
}
],
"quote": {
"status": "valid",
"totalPrice": { "amount": "5", "currency": "usd" }
},
"payment": {
"status": "completed",
"method": "basis-theory",
"currency": "usd",
"received": { "amount": "5", "currency": "usd" }
}
}
}
```
```json theme={null}
{
"actionId": "2795199f-f599-44c4-84dc-38d944853a05",
"type": "orders.delivery.failed",
"data": {
"orderId": "2795199f-f599-44c4-84dc-38d944853a05",
"phase": "completed",
"locale": "en-US",
"lineItems": [
{
"chain": "base",
"executionMode": "exact-in",
"executionParams": {
"mode": "exact-in",
"amount": "5",
"contractAddress": "0x4ed4e862860bed51a9570b96d89af5e1b0efefed",
"quantity": 1
},
"maxSlippageBps": "50",
"metadata": {
"name": "DEGEN",
"description": "DEGEN is a community-driven token..."
},
"quote": {
"status": "valid",
"totalPrice": { "amount": "5", "currency": "usd" }
},
"delivery": {
"status": "failed",
"failureReason": {
"code": "slippage-tolerance-exceeded"
},
"recipient": {
"locator": "base:0x24573a80ae60c0e75735843f119ab4623a45e523",
"walletAddress": "0x24573a80ae60c0e75735843f119ab4623a45e523"
}
}
}
],
"quote": {
"status": "valid",
"totalPrice": { "amount": "5", "currency": "usd" }
},
"payment": {
"status": "completed",
"method": "basis-theory",
"currency": "usd",
"received": { "amount": "5", "currency": "usd" },
"refunded": { "amount": "5", "currency": "usd" }
}
}
}
```
### 4. Pass Custom Arguments (Optional)
Custom arguments (whPassThroughArgs) are only supported in Checkout V2 webhooks. This feature is not available in
Checkout V3 webhooks (orders.\*).
You can pass custom arguments through Checkout V2 webhooks to track additional information:
* User IDs (If you want additional security, sign this ID with a custom key, or send it as a signed JWT, and verify its integrity later on your server)
* Timestamps
* Product SKUs
* Custom metadata
Example of passing arguments:
```jsx theme={null}
function NFTSalePage() {
const whArgs = {
uid: 123424,
sku: 123123123,
metadata: { custom: "data" },
};
const whArgsSerialized = JSON.stringify(whArgs);
return (
);
}
```
Then, extract them on the server:
```javascript theme={null}
export default function handler(req, res) {
const { whPassThroughArgs } = req.body;
if (whPassThroughArgs) {
const whArgsDeserialized = JSON.parse(whPassThroughArgs);
console.log(whArgsDeserialized);
}
res.status(200).json({});
}
```
### 5. Pre & Post Processing
Add your pre and post processing logic when setting up your webhook listener. For example, you can call back to your database when a certain id has succeeded or even use Sendgrid or EmailJS to send an email to a recipient when a mint completes.
### 6. Configure in Crossmint Console
1. Navigate to the [Webhooks page](https://www.crossmint.com/console/webhooks) in the console
2. Click **Add Endpoint**
3. Enter your endpoint URL
4. Select the webhook events to receive
5. Click **Create**
### 6. Security
For security, verify webhook signatures using the signing secret from your endpoint details page:
See the [Verify Webhooks](/introduction/platform/webhooks/verify-webhooks) guide for implementation details.
## Testing Webhooks
1. Use test card number `4242 4242 4242 4242` for successful payments
2. Use `4000 0000 0000 4954` to test payment failures
3. Monitor webhook deliveries in the [Console](https://www.crossmint.com/console/webhooks)
# Payment Error Codes
Source: https://docs.crossmint.com/payments/api-reference/error-codes
Reference of the normalized error categories and codes returned in the order's payment.failureReason
## See Also
Surface the decline reason in Embedded Checkout, including after the Apple Pay or Google Pay sheet closes
What to collect from a buyer before escalating a checkout issue to Crossmint
Order, quote, payment, and delivery statuses, which are distinct from payment error codes
The payment phase of the Headless Checkout order lifecycle, including handling declined payments
REST endpoint that returns the order, including `payment.failureReason`
Receive order updates, including declines, server-side
# Localization
Source: https://docs.crossmint.com/payments/embedded/customize/localization
Customize the language and currency displayed on checkout
## How to Adjust the Currency and Language
To customize the language use the property `locale` and for currency, set `defaultCurrency`. Here's an example:
```tsx {3,16} theme={null}
```
# Apple Pay Integration Guide
Source: https://docs.crossmint.com/payments/embedded/guides/apple-pay
Learn how to enable Apple Pay on your site for Checkout
To enable Apple Pay on your site, follow these steps using the [Crossmint Developer Console](https://www.crossmint.com/console/apple-pay-domains).
## Prerequisites
1. **HTTPS Hosting**: Your site must be hosted on HTTPS to ensure secure transactions.
2. **Crossmint Account**: You need an active Crossmint developer account.
## Setup Instructions
### Step 1: Download the Verification File
1. Go to the [Apple Pay Domains](https://www.crossmint.com/console/apple-pay-domains) page in the Crossmint Console.
2. Click **Download file** to get the Apple Developer Merchant ID Domain Association file.
### Step 2: Host the Verification File
Host the downloaded file on your server at the following path:
```
https://[your-domain]/.well-known/apple-developer-merchantid-domain-association
```
**Option 1: Using the Public Folder**
1. Create the `.well-known` folder inside your `public` directory.
2. Place the downloaded file in `public/.well-known/` with the filename `apple-developer-merchantid-domain-association` (no extension).
**Option 2: Using a Route**
1. Create a folder at `app/.well-known/apple-developer-merchantid-domain-association/`
2. Add a `route.ts` file with the following content:
```typescript route.ts theme={null}
import { NextResponse } from "next/server";
export async function GET() {
return new NextResponse("PASTE_FILE_CONTENT_HERE", {
headers: {
"Content-Type": "text/plain",
},
});
}
```
Replace `PASTE_FILE_CONTENT_HERE` with the content of the downloaded verification file.
1. Create the `.well-known` folder inside your `public` directory.
2. Place the downloaded file in `public/.well-known/` with the filename `apple-developer-merchantid-domain-association` (no extension).
1. Create the `.well-known` folder inside your `public` directory.
2. Place the downloaded file in `public/.well-known/` with the filename `apple-developer-merchantid-domain-association` (no extension).
### Step 3: Verify Your Domain
1. Return to the [Apple Pay Domains](https://www.crossmint.com/console/apple-pay-domains) page in the Crossmint Console.
2. Enter your domain (e.g., `example.com` or `checkout.example.com`) in the input field.
3. Click **Verify domain**.
If verification succeeds, your domain will appear in the list of registered domains and Apple Pay will be enabled for that domain.
If verification fails, ensure the file is publicly accessible at the correct path. You can test by visiting `https://your-domain/.well-known/apple-developer-merchantid-domain-association` in your browser.
## Local Testing
Using [ngrok](https://ngrok.com/) is recommended for local testing. Ngrok allows you to expose your local server to the internet securely.
1. Start ngrok with a fixed domain if possible for consistent testing.
2. Host the verification file on your local server.
3. Register the ngrok domain in the Crossmint Console.
4. Test Apple Pay on Safari (macOS) or any iOS device.
## Apple Pay in Mobile Apps
In native iOS and React Native apps, the checkout renders inside a WebView, and Apple Pay works there with two differences from the web setup above:
* **Domain verification is not required.** The steps on this page apply to websites that embed the checkout. Inside a mobile app, the checkout page is served from `crossmint.com`, which is already enabled for Apple Pay.
* **Test on a physical iPhone** running iOS 17 or higher, with a card added to Apple Wallet. The iOS Simulator does not render Apple Pay.
***
For further assistance, please contact our support team.
# Use Connected Wallet as Payer
Source: https://docs.crossmint.com/payments/embedded/guides/custom-payer
Learn how to configure an already connected wallet as the payer wallet
## Overview
By default, Embedded checkout includes wallet connector UI. However, if your customers have already
connected their wallet to your website, you can configure that connected wallet directly as the payer.
This is useful when:
* You're already using a wallet connection solution (like RainbowKit, Solana Wallet Adapter, Web3Modal, or ConnectKit)
* You are using embedded wallets (from Crossmint or other providers)
## Quick Integration
Here's a simple example using [wagmi](https://wagmi.sh/), [viem](https://viem.sh/) and [TanStack Query](https://tanstack.com/query/v5):
Make sure you have this environment variables on `.env.local` or directly replace it in the code `bash
NEXT_PUBLIC_CLIENT_API_KEY="__YOUR_CLIENT_KEY__" # From API Keys page NEXT_PUBLIC_RECIPIENT_WALLET_ADDRESS="" #
Address receiving the assets NEXT_PUBLIC_RECEIPT_EMAIL="user@example.com"
NEXT_PUBLIC_COLLECTION_ID="__YOUR_COLLECTION_ID__" # From Collection details page `
```tsx Checkout.tsx theme={null}
import { CrossmintEmbeddedCheckout } from "@crossmint/client-sdk-react-ui";
import { Hex, parseTransaction } from "viem";
import { useAccount, useWalletClient } from "wagmi";
import { baseSepolia, polygonAmoy } from "wagmi/chains";
export default function Checkout() {
const { address } = useAccount();
const { data: walletClient } = useWalletClient();
const collectionId = process.env.NEXT_PUBLIC_COLLECTION_ID as string;
const chainIds: Record = {
"base-sepolia": baseSepolia.id,
"polygon-amoy": polygonAmoy.id,
} as const;
// Don't render checkout if wallet is not connected
if (!address || !walletClient) {
return (
);
}
```
```tsx Home.tsx theme={null}
"use client";
import { CrossmintProvider } from "@crossmint/client-sdk-react-ui";
import { WagmiProvider, createConfig, http } from "wagmi";
import { baseSepolia, polygonAmoy } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { injected } from "wagmi/connectors";
import Checkout from "@/components/Checkout";
import WalletConnector from "@/components/WalletConnector";
export default function Home() {
const clientApiKey = process.env.NEXT_PUBLIC_CLIENT_API_KEY as string;
// Create a client
const queryClient = new QueryClient();
// Create wagmi config
const config = createConfig({
chains: [baseSepolia, polygonAmoy] as any,
connectors: [injected()] as any,
transports: {
[baseSepolia.id]: http(),
[polygonAmoy.id]: http(),
},
} as any);
return (
);
}
```
When the user clicks the hosted checkout button, our SDK will first call `handleChainSwitch` to ensure the correct
network is set, then `handleSignAndSendTransaction` to complete the purchase.
## Payer Configuration
The `payer` prop accepts the following configuration:
| Property | Type | Description |
| - | - | - |
| `address` | `string` | The wallet address of the payer |
| `initialChain` | `PayerSupportedBlockchains` | The initial blockchain (e.g. "polygon", "ethereum", "base-sepolia") |
| `supportedChains` | `PayerSupportedBlockchains[]` | Optional list of supported chains. Defaults to `initialChain` if not specified |
| `handleChainSwitch` | `(chain: PayerSupportedBlockchains) => Promise` | Function to handle chain switching |
| `handleSignAndSendTransaction` | `(serializedTx: string) => Promise` | Function to handle transaction signing and sending, where `TransactionResponse` is either `{ success: true, txId: string }` or `{ success: false, errorMessage: string }` |
## Popular Wallet Solutions
You can integrate Crossmint with popular Web3 solutions:
### RainbowKit
```tsx theme={null}
import { useConnectModal } from "@rainbow-me/rainbowkit";
// Similar implementation as above, but using RainbowKit's hooks
```
### Web3Modal
```tsx theme={null}
import { useWeb3Modal } from "@web3modal/react";
// Similar implementation as above, but using Web3Modal's hooks
```
## Best Practices
1. **Error Handling**: Always return clear error messages in `handleSignAndSendTransaction`
2. **Chain Support**: Be explicit about supported chains to avoid runtime errors
3. **User Experience**: Show loading states during chain switches and transactions
4. **Wallet Connection**: Ensure wallet is connected before showing checkout
## Related Resources
* [Payment Methods](/payments/embedded/guides/payment-methods)
* [React Hooks](/payments/embedded/guides/hooks)
* [Testing Tips](/payments/advanced/testing-tips)
# Display Payment Error Messages
Source: https://docs.crossmint.com/payments/embedded/guides/display-payment-errors
Show buyers why a card was declined after the Apple Pay or Google Pay sheet closes
When a card is declined during an Apple Pay or Google Pay purchase, the wallet sheet closes with only the wallet's generic failure state — for example, Apple Pay shows "Order failed" with no explanation. Without a visible decline reason, buyers tend to retry the same card several times, which lowers their chance of a successful purchase.
Crossmint always delivers a structured decline reason to your page. This guide shows the two ways to surface it, and how to tell a decline apart from a user cancellation.
## Prerequisites
* **Embedded Checkout**: An [Embedded Checkout](/payments/embedded/overview) integration with Apple Pay or Google Pay enabled. See the [payment methods guide](/payments/embedded/guides/payment-methods).
* **React SDK**: The code samples use `@crossmint/client-sdk-react-ui` with `CrossmintCheckoutProvider`. See [React Hooks](/payments/embedded/guides/hooks).
## Option 1: Enable the Built-In Error Banner
The checkout component includes an error banner that displays the decline reason. On the Apple Pay and Google Pay screens this banner is hidden by default. Enable it with one appearance rule — **`appearance.rules.GlobalMessage.display: "visible"`**:
```tsx theme={null}
```
With this rule set, the checkout shows the decline reason above the payment button after the wallet sheet closes.
The built-in banner dismisses automatically after 8 seconds. If your page constrains the checkout iframe's height,
the banner can render outside the visible area. For full control over placement and persistence, use Option 2.
## Option 2: Render Your Own Error Message
The checkout reports every order change to your page. After a decline, the updated order contains `payment.failureReason`. Read it with the `useCrossmintCheckout` hook and render the message anywhere on your page:
```tsx theme={null}
import { useCrossmintCheckout } from "@crossmint/client-sdk-react-ui";
function CheckoutErrorBanner() {
const { order } = useCrossmintCheckout();
const failureReason = order?.payment?.failureReason;
if (failureReason == null) {
return null;
}
return (
{failureReason.message ?? "Your payment was declined. Try a different payment method."}
);
}
```
Place the component where the buyer is guaranteed to see it — directly adjacent to the checkout, or in a sticky status region. Do not place it where it can fall outside the visible viewport.
Keep listening after the sheet closes. With Google Pay, the sheet closes as soon as the buyer authorizes, and the decline can arrive afterwards. Some declines also arrive asynchronously while the order is finalized. React to `failureReason` whenever it appears, and clear your error UI when the buyer starts a new attempt.
## Tell a Cancellation Apart from a Decline
The sheet closing is ambiguous on its own: it happens both when the buyer cancels and when the card is declined. Use the order state to distinguish the two:
| Signal | Meaning | What to show |
| - | - | - |
| Sheet closes and `order.payment.failureReason` stays `undefined` | The buyer canceled | Nothing, or a neutral "Payment not completed" message |
| An order update arrives with `order.payment.failureReason` set | The card was declined | The decline explanation (`failureReason.message`) |
A cancellation never produces a `failureReason`, and a decline always does. Gate your error UI strictly on `failureReason` — never on the sheet closing.
## The `failureReason` Object
| Field | Type | Description |
| - | - | - |
| `code` | `string` | Normalized decline code for logic, analytics, and support tooling |
| `category` | `string` (optional) | The broad reason the payment failed, such as `authorization_decline` or `risk_block` |
| `retryPolicy` | `string` (optional) | The recommended next step for the buyer, such as `retry_with_different_payment_method` |
| `message` | `string` (optional) | User-safe explanation. **The only field safe to render to buyers** |
The complete list of categories, codes, and retry policies lives in the [Payment Error Codes](/payments/api-reference/error-codes) reference. Use `retryPolicy` to decide which affordance to show — for example, prompt for a different card on `retry_with_different_payment_method`, and hide the retry button on `do_not_retry`.
A card decline returns the order to the `awaiting-payment` state regardless of the decline code, so the order can still accept a payment. `awaiting-payment` therefore means the order is open, not that retrying is a good idea — `retryPolicy` is the signal for that. With `do_not_retry` (for example `psp_security_block`, where fraud or risk screening blocked the attempt), do not offer a retry even though the order remains `awaiting-payment`. Never retry a declined payment automatically on the buyer's behalf.
`awaiting-payment` together with `retryPolicy: "do_not_retry"` is a valid, expected combination. Crossmint does not enforce `do_not_retry` server-side; it is a hint for your UI.
## Common Gotchas
The checkout iframe's height is managed dynamically by the SDK. If your page wraps the iframe in a
fixed-height container with internal scrolling, content near the bottom — including the built-in error banner —
can land below the fold. Let the SDK manage the iframe height, or render your own error message outside the
iframe (Option 2).
This is expected, particularly with Google Pay, where authorization completes before the charge is attempted.
Keep observing order updates after the sheet closes instead of checking once.
## Next Steps
# Google Pay Mobile
Source: https://docs.crossmint.com/payments/embedded/guides/google-pay
Learn how to enable Google Pay for token checkout and onramp flows using Crossmint's mobile SDKs
**This guide is for mobile apps only.** For web applications, Google Pay works automatically when enabled via the `payment.fiat.allowedMethods.googlePay` prop—no additional setup required. See [Payment Methods](/payments/embedded/guides/payment-methods) for web configuration.
This guide covers how to enable Google Pay for token checkout and onramp flows in **React Native** and **Android native (Kotlin)** applications. Mobile apps require additional native configuration that web apps don't need.
## Overview
Crossmint's mobile SDKs provide pre-built checkout components that support Google Pay out of the box. When properly configured, users see the native Android Google Pay sheet, providing seamless access to payment credentials stored in Google Wallet without leaving your app.
### Mobile vs Web
| Platform | Setup Required | Guide |
| - | - | - |
| Web (React, Next.js, etc.) | Enable via props only | [Payment Methods](/payments/embedded/guides/payment-methods) |
| React Native (Expo) | Expo plugin + rebuild | This guide |
| React Native (Bare) | Manual Android config | This guide |
| Android Native (Kotlin) | AndroidManifest config | This guide |
| Your own WebView (no SDK) | WebView + manifest config | [Mobile WebView Integration](/payments/embedded/guides/webview-integration) |
### Supported Use Cases
* Token purchases (buy tokens with fiat)
* Stablecoin onramp (USDC, etc.)
* NFT checkout
## Mobile SDK Integration
### Prerequisites
Before starting, ensure you have:
* React Native project with Expo or bare workflow
* Crossmint account with API keys (staging and/or production)
* Android device or emulator with Google Play Services
```shell theme={null}
pnpm add @crossmint/client-sdk-react-native-ui
```
The SDK includes `react-native-webview` as a dependency. If you need to install it separately, ensure version **13.15.0 or higher**:
```shell theme={null}
pnpm add react-native-webview@^13.15.0
```
Update your `app.json` to enable Google Pay:
```json theme={null}
{
"expo": {
"plugins": [
[
"@crossmint/client-sdk-react-native-ui",
{
"enableGooglePay": true
}
]
]
}
}
```
This plugin modifies native Android permissions required for Google Pay. The app must be rebuilt after adding this configuration.
Hot reload cannot apply the permission changes. You must rebuild:
```shell theme={null}
npx expo run:android
```
Or for bare React Native:
```shell theme={null}
npx react-native run-android
```
Use `CrossmintEmbeddedCheckout` for token purchases:
```javascript theme={null}
import {
CrossmintEmbeddedCheckout,
CrossmintProvider,
} from "@crossmint/client-sdk-react-native-ui";
export default function CheckoutScreen() {
return (
);
}
```
For staging/testing, use the XMEME test token:
```javascript theme={null}
tokenLocator: "solana:7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu"
```
Hide destination and email inputs for a cleaner UI:
```javascript theme={null}
```
### Prerequisites
* Minimum Android SDK version 24 (Android 7.0)
* Jetpack Compose
* Crossmint account with API keys
In your app-level `build.gradle.kts`:
```kotlin theme={null}
dependencies {
implementation("com.crossmint:crossmint-sdk-android:0.0.9")
}
```
Add the required query intents to enable Google Pay functionality:
```xml theme={null}
```
**Security requirement:** Order creation must happen on your backend to protect your API key.
```bash cURL theme={null}
curl --location 'https://staging.crossmint.com/api/2022-06-09/orders' \
--header 'x-api-key: YOUR_SERVER_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"recipient": {
"walletAddress": "YOUR_WALLET_ADDRESS"
},
"payment": {
"receiptEmail": "user@example.com",
"method": "card"
},
"lineItems": {
"tokenLocator": "solana:TOKEN_ADDRESS",
"executionParameters": {
"mode": "exact-in",
"amount": "1"
}
}
}'
```
```javascript Node.js theme={null}
const url = 'https://staging.crossmint.com/api/2022-06-09/orders';
const payload = {
recipient: {
walletAddress: "YOUR_WALLET_ADDRESS"
},
payment: {
receiptEmail: "user@example.com",
method: "card"
},
lineItems: {
tokenLocator: "solana:TOKEN_ADDRESS",
executionParameters: {
mode: "exact-in",
amount: "1"
}
}
};
const options = {
method: 'POST',
headers: {
'x-api-key': 'YOUR_SERVER_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
};
const response = await fetch(url, options);
const data = await response.json();
console.log(data);
```
```python Python theme={null}
import requests
url = "https://staging.crossmint.com/api/2022-06-09/orders"
payload = {
"recipient": {
"walletAddress": "YOUR_WALLET_ADDRESS"
},
"payment": {
"receiptEmail": "user@example.com",
"method": "card"
},
"lineItems": {
"tokenLocator": "solana:TOKEN_ADDRESS",
"executionParameters": {
"mode": "exact-in",
"amount": "1"
}
}
}
headers = {
"x-api-key": "YOUR_SERVER_API_KEY",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.json())
```
The response includes `orderId` and `clientSecret` needed for the checkout component.
```kotlin theme={null}
import com.crossmint.kotlin.checkout.CheckoutEnvironment
import com.crossmint.kotlin.checkout.CrossmintEmbeddedCheckout
import com.crossmint.kotlin.checkout.models.*
@Composable
fun CheckoutScreen(orderId: String, clientSecret: String) {
CrossmintEmbeddedCheckout(
orderId = orderId,
clientSecret = clientSecret,
payment = CheckoutPayment(
crypto = CheckoutCryptoPayment(enabled = false),
fiat = CheckoutFiatPayment(
enabled = true,
allowedMethods = CheckoutAllowedMethods(
googlePay = true,
applePay = false,
card = true
)
)
),
appearance = CheckoutAppearance(
rules = CheckoutAppearanceRules(
destinationInput = CheckoutDestinationInputRule(display = "hidden"),
receiptEmailInput = CheckoutReceiptEmailInputRule(display = "hidden")
)
),
environment = CheckoutEnvironment.STAGING,
modifier = Modifier.fillMaxSize()
)
}
```
## Google Pay Production Approval
Google Pay works immediately in **staging** environments. For **production**, Google requires explicit approval of your app.
1. Navigate to the [Google Pay & Wallet Console](https://pay.google.com/business/console)
2. Select "Merchant" as your business type
3. Complete all business profile information
4. Navigate to **Google Pay API** and click "Get Started"
5. Accept the Google Pay API Terms of Service
6. Note your **Merchant ID** (top-right corner after completion)
In the Google Pay & Wallet Console:
1. Navigate to **Google Pay API** then **Integrations** then **Integrate with your Android app**
2. Locate your Android application and click "Manage"
3. Select your integration type (typically "Gateway")
4. Upload screenshots of your TEST Google Pay integration
5. Click "Save" then "Submit for approval"
**Required Screenshots:**
* Product/item selection showing Google Pay option
* Cart or checkout view with payment options
* Google Pay payment sheet with card selection
* Confirmation or receipt screen
After Google approval (typically \~1 business day):
1. Sign your APK with a **release key** (debug keys don't work in production)
2. Update environment settings:
* Kotlin: `environment = CheckoutEnvironment.PRODUCTION`
* React Native: Use production API key
3. Publish your app to Google Play Store
## SDK Version Requirements
| Component | Minimum Version |
| - | - |
| `@crossmint/client-sdk-react-native-ui` | Latest |
| `react-native-webview` (if installed separately) | 13.15.0 |
| `com.crossmint:crossmint-sdk-android` | Latest |
| Google Play Services (user device) | 25.18.30 |
## Troubleshooting
1. **Plugin not configured:** Verify `enableGooglePay: true` in `app.json` plugins
2. **App not rebuilt:** Run `npx expo run:android` after adding the plugin
3. **Environment mismatch:** Staging works without approval; production requires it
4. **Payment method disabled:** Check `googlePay: true` in `allowedMethods`
1. **AndroidManifest missing queries:** Add all three Chromium intent queries
2. **Native rebuild required:** Hot reload cannot apply permission changes
3. **Device requirements:** User needs Google Play Services 25.18.30+ and WebView 137+
1. Check that your API key is valid for the environment (staging vs production)
2. Verify the `tokenLocator` is correct and available
3. Ensure the quote hasn't expired (quotes have time limits)
**"Token purchase is only available in production":** Some tokens only work in production. Use the XMEME test token for staging: `7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu`
## Testing Guidance
* Use staging API keys from your Crossmint console
* Google Pay functions immediately after proper configuration
* Use XMEME test token for token testing
* Test with real Google accounts and saved payment methods
* Requires completed Google approval process
* Sign APK with release key
* Use production API keys
* Real charges may occur - test with small amounts
## Useful Resources
Get started with token checkout in React Native
Manage your Google Pay merchant account
Example React Native Expo project with checkout
Example Android Kotlin project with checkout
# React Hooks
Source: https://docs.crossmint.com/payments/embedded/guides/hooks
Learn how to use React hooks to control and customize your Embedded Checkout experience
## Complete Example
Here's a full example showing how to implement a custom checkout experience:
```tsx theme={null}
import {
CrossmintProvider,
CrossmintCheckoutProvider,
CrossmintEmbeddedCheckout,
useCrossmintCheckout,
} from "@crossmint/client-sdk-react-ui";
function CheckoutPage() {
return (
);
}
function CheckoutStatus() {
const { order } = useCrossmintCheckout();
if (!order) {
return
Loading...
;
}
switch (order.phase) {
case "completed":
return
Purchase complete!
;
case "delivery":
return
Delivering your NFTs...
;
case "payment":
return
Processing payment...
;
case "quote":
return
Preparing your order...
;
}
}
```
## Migration from Previous Version
If you're migrating from the previous version, note these key differences:
* The event system has been replaced with the `useCrossmintCheckout` hook
* Order status is now accessed through the hook instead of event listeners
* Custom UI is built using React components instead of event handlers
* Components must be wrapped with required providers
The new hook-based approach provides a more React-friendly experience and better TypeScript support compared to the
previous event system.
## Related Resources
* [UI Customization](/payments/embedded/guides/ui-customization)
* [Payment Methods](/payments/embedded/guides/payment-methods)
* [Order Lifecycle](/payments/headless/guides/order-lifecycle)
* [Testing Tips](/payments/advanced/testing-tips)
# Item Selection
Source: https://docs.crossmint.com/payments/embedded/guides/item-selection
Specify which items to purchase with Embedded Checkout
The Embedded Checkout allows you to specify which items your users can purchase using the `lineItems` property. This guide explains how to configure item selection for different asset types.
## Item Selection Examples
```tsx collectionLocator theme={null}
```
```tsx collectionLocator - imported contract theme={null}
```
```tsx tokenLocator - EVM theme={null}
::
callData: {
totalPrice: "25.00", // Only totalPrice is required for token purchases
// For external EVM contracts, you might need additional parameters:
// _amount: 1, // if your contract uses _amount instead of quantity
// tokenId: "1234", // if your contract requires explicit tokenId
// metadata: "ipfs://...", // if your contract accepts metadata
},
}}
/>
```
```tsx tokenLocator - Solana theme={null}
:
callData: {
totalPrice: "25.00",
buyerCreatorRoyaltyPercent: 100, // Required for Solana
},
}}
/>
```
## Accessing Item Data in React Components
When using Embedded Checkout, you can access the current order's item data using the `useCrossmintCheckout` hook:
```tsx theme={null}
import { useCrossmintCheckout } from "@crossmint/client-sdk-react-ui";
function YourComponent() {
const { order } = useCrossmintCheckout();
// Access line items data
const lineItems = order?.lineItems || [];
return (
);
}
```
The `useCrossmintCheckout` hook must be used within components wrapped by both `CrossmintProvider` and
`CrossmintCheckoutProvider`.
# Payment Methods
Source: https://docs.crossmint.com/payments/embedded/guides/payment-methods
Enable a complete payment suite for your digital asset checkout - from traditional credit cards and digital wallets to cross-chain crypto payments, all through a single integration
## Overview
Crossmint's Embedded Checkout makes it easy to accept multiple payment methods. Users can pay:
* With credit/debit cards
* Using Apple Pay or Google Pay
* Using ETH, SOL, USDC, EURC, or other supported tokens
* Using cross-chain crypto on supported chains
## Quick Integration
Enable all payment methods with just a few lines of code:
```jsx theme={null}
```
## Fiat Payments
### Credit Cards & Digital Wallets
Embedded Checkout provides a seamless fiat payment experience supporting:
* All major credit and debit cards
* Apple Pay for iOS/Safari users
* Google Pay for Android/Chrome users
You can configure which fiat payment methods to enable:
```jsx theme={null}
payment={{
fiat: {
enabled: true,
// By default, all payment methods are enabled if you don't specify any.
allowedMethods: {
card: true, // Enable/disable credit cards
applePay: true, // Enable/disable Apple Pay
googlePay: true, // Enable/disable Google Pay
},
defaultCurrency: "usd" // Set default currency
}
}}
```
## Crypto Payments
### Native Wallet Support
Embedded Checkout works with any Web3 wallet, including:
* MetaMask
* Coinbase Wallet
* WalletConnect
* Phantom
Users can pay with ETH, SOL, stablecoins (USDC, EURC), or other supported tokens on their preferred chain. Enable crypto payments with:
```jsx theme={null}
payment={{
crypto: {
enabled: true,
defaultChain: "ethereum", // Optional: Set default blockchain
defaultCurrency: "usdc" // Optional: Set default currency (usdc, eurc, eth)
}
}}
```
## Cross-Chain Support
The checkout supports paying with funds from any supported blockchain, even if the digital asset is on a different chain. For example, a user could pay for a digital asset on Polygon using ETH from their Ethereum wallet. The checkout handles all necessary conversions automatically.
## Customizing the Default Experience
Set a default payment method to guide users:
```jsx theme={null}
payment={{
defaultMethod: "fiat", // or "crypto"
crypto: {
enabled: true
},
fiat: {
enabled: true
}
}}
```
### Test Price Limits and Test Credit Cards
When building your applications using the staging environment, you can use various test credit cards numbers to see the entire process end-to-end, without actually having to transact using a real credit card. Check out the Testing Tips page for more info on [price limits](/payments/advanced/testing-tips#limits-in-staging) and [test card numbers](/payments/advanced/testing-tips#test-credit-card-numbers).
## Best Practices
* Enable Multiple Payment Methods: Offer both fiat and crypto options to maximize conversion
* Set Default Currency: Choose a default that matches your target market
* Test All Flows: Use test cards and test wallets to verify the complete payment experience
* Handle Events: Use the `useCrossmintCheckout` hook to track payment status and handle completion
## Related Resources
* [Price Limits](/payments/advanced/testing-tips#limits-in-staging)
* [Test Credit Card Numbers](/payments/advanced/testing-tips#test-credit-card-numbers)
* [Order Lifecycle](/payments/headless/guides/order-lifecycle)
* [Customizing Appearance](/payments/embedded/guides/ui-customization)
* [React Hooks](/payments/embedded/guides/hooks)
# Production Launch
Source: https://docs.crossmint.com/payments/embedded/guides/production-launch
Follow this checklist to ensure you are ready for production launch with Embedded Checkout
## From Staging to Production with Embedded Checkout
***
### Launch Checklist
1. Change your API keys to the production versions. Ensure your production API key has the appropriate scopes enabled, such as `orders.create`.
2. Update your collection ID to the production one.
3. As needed, change your passed-in props to be production ready (e.g. make `email`, `payment`, `lineItems` programmatically filled).
```jsx {(1, 4)} Production theme={null}
```
```jsx {(1, 4)} Staging theme={null}
```
# Specify Recipient
Source: https://docs.crossmint.com/payments/embedded/guides/specify-recipient
Select where purchased items should be delivered in Embedded Checkout
## Specifying the Recipient
For Embedded Checkout, you specify the recipient by adding a `recipient` object to your checkout component. The recipient can be specified by email or wallet address.
### Recipient by Email
When specifying a recipient by email, Crossmint will automatically create a secure custodial wallet on the fly for that email address:
```jsx React {4} theme={null}
```
The recipient will be able to access their purchased items by logging into their Crossmint wallet in [staging](https://staging.crossmint.com/user/collection) or [mainnet](https://www.crossmint.com/user/collection).
### Recipient by Wallet Address
To deliver items directly to a specific blockchain wallet address:
```jsx React {4} theme={null}
```
# UI Customization
Source: https://docs.crossmint.com/payments/embedded/guides/ui-customization
Learn how to customize the Embedded Checkout UI to match your brand identity and design system
## Quick Start
The checkout UI can be customized using the `appearance` prop:
```jsx theme={null}
```
## Identity Verification Screens
## Customization Options
### Background Customization
The embedded checkout component has a **transparent background** by default. The `backgroundPrimary` color in the appearance configuration only affects internal components (inputs, cards, etc.) within the iframe, not the iframe body itself.
To customize the overall background, wrap the component in a div with your desired background styling:
#### Solid Background Colors
```jsx theme={null}
{
/* Tailwind CSS */
}
;
{
/* Custom CSS */
}
;
```
Remember that the `backgroundPrimary` color in your appearance configuration should complement your wrapper
background for the best visual result.
### Custom Fonts
Load custom fonts using the `fonts` array:
```jsx theme={null}
appearance={{
fonts: [
{ cssSrc: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" },
{ cssSrc: "https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" }
]
}}
```
### Global Variables
Control the overall look and feel with these global variables:
```jsx theme={null}
appearance={{
variables: {
// Typography
fontFamily: "Inter, system-ui, sans-serif",
fontSizeUnit: "16px",
// Spacing
spacingUnit: "1rem",
borderRadius: "8px",
// Colors
colors: {
borderPrimary: "#E0E0E0",
backgroundPrimary: "#FFFFFF",
textPrimary: "#000000",
textSecondary: "#666666",
danger: "#FF0000",
warning: "#FFA500",
accent: "#0074D9"
}
}
}}
```
### Component Rules
Control the visibility of specific inputs:
```jsx theme={null}
appearance={{
rules: {
DestinationInput: {
display: "hidden" // Hides the wallet address input
},
ReceiptEmailInput: {
display: "hidden" // Hides the email input
}
}
}}
```
When hiding inputs, make sure to provide the corresponding values via props:
* For hidden `DestinationInput`, provide `recipient.walletAddress`
* For hidden `ReceiptEmailInput`, provide `payment.receiptEmail`
Customize label styling:
```jsx theme={null}
appearance={{
rules: {
Label: {
font: {
family: "Inter",
size: "14px",
weight: "500",
},
colors: {
text: "#333333",
},
},
},
}}
```
Style input fields:
```jsx theme={null}
appearance={{
rules: {
Input: {
borderRadius: "8px",
font: {
family: "Inter",
size: "16px",
weight: "400",
},
colors: {
text: "#000000",
background: "#FFFFFF",
border: "#E0E0E0",
boxShadow: "none",
placeholder: "#999999",
},
hover: {
colors: {
border: "#0074D9",
},
},
focus: {
colors: {
border: "#0074D9",
boxShadow: "0 0 0 2px rgba(0,116,217,0.2)",
},
},
},
},
}}
```
Customize tab appearance:
```jsx theme={null}
appearance={{
rules: {
Tab: {
borderRadius: "4px",
font: {
family: "Inter",
size: "14px",
weight: "500",
},
colors: {
text: "#666666",
background: "transparent",
},
selected: {
colors: {
text: "#000000",
background: "#F5F5F5",
},
},
hover: {
colors: {
background: "#F0F0F0",
},
},
},
},
}}
```
Style primary buttons:
```jsx theme={null}
appearance={{
rules: {
PrimaryButton: {
borderRadius: "8px",
font: {
family: "Inter",
size: "16px",
weight: "600"
},
colors: {
text: "#FFFFFF",
background: "#0074D9"
},
hover: {
colors: {
background: "#0063B8"
}
},
disabled: {
colors: {
text: "#FFFFFF",
background: "#CCCCCC"
}
}
}
}
}}
```
## Common Examples
### Modern Dark Theme
```jsx theme={null}
appearance={{
variables: {
fontFamily: "Inter, system-ui, sans-serif",
colors: {
backgroundPrimary: "#1A1A1A",
textPrimary: "#FFFFFF",
textSecondary: "#A0A0A0",
borderPrimary: "#333333",
accent: "#7928CA"
}
},
rules: {
Input: {
colors: {
background: "#2D2D2D",
border: "#404040",
text: "#FFFFFF",
placeholder: "#666666"
}
},
PrimaryButton: {
colors: {
background: "#7928CA",
text: "#FFFFFF"
},
hover: {
colors: {
background: "#6B24B2"
}
}
}
}
}}
```
### Minimal Light Theme
```jsx theme={null}
appearance={{
variables: {
fontFamily: "system-ui, sans-serif",
borderRadius: "4px",
colors: {
backgroundPrimary: "#FFFFFF",
textPrimary: "#000000",
borderPrimary: "#E0E0E0",
accent: "#000000"
}
},
rules: {
Input: {
borderRadius: "4px",
colors: {
background: "#F5F5F5",
border: "transparent"
},
focus: {
colors: {
border: "#000000",
boxShadow: "none"
}
}
},
PrimaryButton: {
borderRadius: "4px",
colors: {
background: "#000000",
text: "#FFFFFF"
}
}
}
}}
```
## Related Resources
* [Identity Verification UI Customization](/identity/ui-customization)
* [Payment Methods](/payments/embedded/guides/payment-methods)
* [React Hooks](/payments/embedded/guides/hooks)
* [Order Lifecycle](/payments/headless/guides/order-lifecycle)
* [Testing Tips](/payments/advanced/testing-tips)
# Mobile WebView Integration
Source: https://docs.crossmint.com/payments/embedded/guides/webview-integration
Render the embedded checkout inside your own WebView on iOS, Android, or React Native
The embedded checkout renders from a URL, so any mobile app can display it in a WebView: create an order server-side, build the checkout URL, and load it. This guide covers the direct WebView integration for apps that do not use a Crossmint client SDK, including native iOS (Swift) and Android (Kotlin) apps.
The [React Native](/payments/embedded/quickstarts/credit-card-memecoin-react-native),
[Flutter](/payments/embedded/quickstarts/credit-card-memecoin-flutter), and
[Kotlin](/sdk-reference/checkout/kotlin/index) SDKs render and configure this WebView for you. If you build with one
of those frameworks, start from the SDK instead and return here only if you need full control over the WebView.
## Prerequisites
* **API keys**: From the [Crossmint Console](https://staging.crossmint.com/console), under **Integrate → API Keys**, create a **server key** (`sk_staging_...`) with the `orders.create` and `orders.read` scopes, and a **client key** (`ck_staging_...`) with the `orders.read` scope.
* **A recipient wallet**: purchased assets are delivered to a wallet address that you pass at order creation.
* **A backend**: order creation uses the server key, which must never ship inside the app.
## Integration
Create the order from your backend and return the `orderId` and `clientSecret` to the app. The order carries the item, the recipient, and optionally the receipt email, so the checkout never needs to ask the user for them:
```javascript theme={null}
// server.js
const CROSSMINT_SERVER_API_KEY = "YOUR_SERVER_API_KEY"; // sk_staging_...
// sk_staging_ keys pair with staging.crossmint.com,
// sk_production_ keys with www.crossmint.com
const BASE_URL = "https://staging.crossmint.com";
async function createOrder(walletAddress) {
const res = await fetch(`${BASE_URL}/api/2022-06-09/orders`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": CROSSMINT_SERVER_API_KEY,
},
body: JSON.stringify({
lineItems: {
// XMEME test token on Solana (staging only)
tokenLocator: "solana:7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu",
executionParameters: {
mode: "exact-in",
amount: "5",
maxSlippageBps: "500",
},
},
payment: { method: "card" }, // wallet payment methods ride this rail at creation time
recipient: { walletAddress },
}),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.message ?? "Order creation failed");
}
return {
orderId: data.order.orderId,
clientSecret: data.clientSecret,
};
}
```
The example uses a token line item; NFT collection line items work the same way. See the
[create order API reference](/api-reference/headless/create-order) for all line item types and options.
The checkout page accepts its configuration as URL query parameters:
| Parameter | Value |
| - | - |
| `orderId` | From order creation |
| `clientSecret` | From order creation |
| `apiKey` | Your **client** key (`ck_...`) |
| `payment` | URL-encoded JSON. Same options as the `payment` prop of the React component |
| `appearance` | URL-encoded JSON. Same options as the `appearance` prop of the React component |
```javascript theme={null}
const BASE_URL = "https://staging.crossmint.com"; // www.crossmint.com in production
const CLIENT_API_KEY = "YOUR_CLIENT_API_KEY"; // ck_staging_...
const payment = {
crypto: { enabled: false },
fiat: {
enabled: true,
allowedMethods: { applePay: true, card: false, googlePay: true },
},
};
const appearance = {
rules: {
DestinationInput: { display: "hidden" },
ReceiptEmailInput: { display: "hidden" },
GlobalMessage: { display: "visible" },
},
};
function buildCheckoutUrl(orderId, clientSecret) {
const params = new URLSearchParams({
orderId,
clientSecret,
payment: JSON.stringify(payment),
appearance: JSON.stringify(appearance),
apiKey: CLIENT_API_KEY,
});
return `${BASE_URL}/sdk/2024-03-05/embedded-checkout?${params.toString()}`;
}
```
Every configuration option from [payment methods](/payments/embedded/guides/payment-methods) and
[UI customization](/payments/embedded/guides/ui-customization) works here. The example above renders a
wallet-button-only checkout; see the
[one-tap Apple Pay quickstart](/payments/embedded/quickstarts/apple-pay-only) for that experience end to end.
A few WebView settings make the difference between a checkout that shows Apple Pay and Google Pay and one that silently does not. The tabs below contain the tested configuration per platform:
Requires react-native-webview version 13.15.0 or higher.
```tsx theme={null}
import { Platform } from "react-native";
import { WebView } from "react-native-webview";
// A standard mobile browser userAgent. The default WebView userAgent
// identifies as an app, and the checkout hides wallet buttons for it.
const userAgent = Platform.select({
ios: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
android:
"Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36",
});
export function CheckoutWebView({ checkoutUrl }: { checkoutUrl: string }) {
return (
);
}
```
For Google Pay, the app also needs the Android payment intent declaration from the
[Google Pay guide](/payments/embedded/guides/google-pay).
Add the androidx.webkit dependency to reach the Payment Request setting, which Google Pay requires and Android WebView ships disabled:
```kotlin theme={null}
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
fun configureCheckoutWebView(webView: WebView, checkoutUrl: String) {
webView.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
userAgentString =
"Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
}
if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
WebSettingsCompat.setPaymentRequestEnabled(webView.settings, true)
}
// Default clients keep every step of the checkout inside the WebView
webView.webViewClient = WebViewClient()
webView.webChromeClient = WebChromeClient()
webView.loadUrl(checkoutUrl)
}
```
Declare the payment intent in `AndroidManifest.xml` so Google Pay can respond:
```xml theme={null}
```
`WKWebView` needs inline media playback enabled; its other defaults work with the checkout as-is:
```swift theme={null}
import WebKit
func makeCheckoutWebView(checkoutUrl: URL) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
configuration.mediaTypesRequiringUserActionForPlayback = []
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.load(URLRequest(url: checkoutUrl))
return webView
}
```
If your app declares `WKAppBoundDomains` in its `Info.plist`, WebKit restricts every `WKWebView` to those
domains. Add the domains listed under "Restricting navigation" below to that list, or the checkout cannot
complete.
Keep navigation inside the WebView unrestricted, and keep iframes enabled. To complete an order, the checkout
navigates to pages from Crossmint's payment method and identity partners, and a WebView that blocks those
pages interrupts the purchase with no visible error.
If your security policy requires a navigation allowlist, include these domains, grouped by what they render
during checkout:
| Purpose | Domains |
| - | - |
| Checkout and order pages | `crossmint.com` |
| Card and wallet payment methods | `stripe.com`, `checkout.com`, `pay.google.com`, `applepay.cdn-apple.com` |
| Identity verification and risk screening | `withpersona.com`, `sardine.ai` |
Match subdomains as well (for example `*.crossmint.com`), and revisit the list when you update the SDK or
change payment methods.
The checkout page reports progress with messages. In React Native, listen with `onMessage`:
```tsx theme={null}
{
const message = JSON.parse(event.nativeEvent.data);
// message.event is one of:
// order:updated order phase changes; "delivery" means the purchase succeeded
// order:creation-failed the order could not initialize
// ui:height.changed the checkout has rendered content
// ui:express-checkout.ready the wallet button is interactive
if (message.event === "order:updated" && message.data?.order?.phase === "delivery") {
// show your success state
}
}}
/>
```
On iOS (Swift) and Android (Kotlin), track the order from your backend instead: subscribe to
[webhooks](/payments/advanced/webhooks) or poll the
[get order endpoint](/api-reference/headless/get-order) with the `orderId` you already hold.
Wallet payment methods render on real hardware: test Apple Pay on an iPhone running iOS 17 or higher with a card added to Apple Wallet, and Google Pay on an Android device with Google Play Services. In staging, wallet payments run against a test environment, so the card behind the wallet is never charged.
To exercise the full purchase from a simulator or emulator during development, enable the card form
(`allowedMethods: { card: true, ... }`) and pay with the staging test card `4242 4242 4242 4242`. More options are
listed in [testing tips](/payments/advanced/testing-tips#test-credit-card-numbers).
## Troubleshooting
* Set a standard mobile browser `userAgent` on the WebView (step 3). The checkout serves wallet buttons to
mobile browsers, and the default WebView userAgent identifies as an app.
* For Google Pay, enable the Payment Request setting and declare the payment intent in the manifest (step 3),
and test on a device with Google Play Services.
* For Apple Pay, use a physical iPhone on iOS 17 or higher: the iOS Simulator does not render Apple Pay.
* Confirm the order was created successfully and `payment` in the URL enables the wallet you expect.
A navigation restriction is usually interrupting the checkout mid-purchase. Remove custom
`WebViewClient`/`WKNavigationDelegate` rules that cancel navigations, or extend your allowlist with the
domains listed under "Restricting navigation" above.
* Enable JavaScript and DOM storage on the WebView (step 3).
* Match the base URL to the key environment: `ck_staging_`/`sk_staging_` keys pair with
`staging.crossmint.com`, production keys with `www.crossmint.com`.
* Confirm the `payment` and `appearance` parameters contain valid JSON after URL encoding.
Pass `GlobalMessage: { display: "visible" }` inside `appearance.rules` (step 2). In layouts that hide the
checkout inputs, this rule is the surface where payment errors render.
## FAQ
Yes. The [React Native](/payments/embedded/quickstarts/credit-card-memecoin-react-native),
[Flutter](/payments/embedded/quickstarts/credit-card-memecoin-flutter), and
[Kotlin](/sdk-reference/checkout/kotlin/index) SDKs apply this WebView configuration automatically and expose
the checkout as a component. Use this guide when you need your own WebView, or on platforms without an SDK,
such as native iOS (Swift).
No. [Apple Pay domain registration](/payments/embedded/guides/apple-pay) applies to websites that embed the
checkout. In this integration the checkout page is served from `crossmint.com`, which is already enabled for
Apple Pay.
Restrict the payment methods and hide the checkout inputs through the `payment` and `appearance` URL
parameters. The [one-tap Apple Pay quickstart](/payments/embedded/quickstarts/apple-pay-only) covers the
complete configuration, and every setting in it maps directly onto the URL parameters from step 2.
Yes. The checkout URL also renders in `SFSafariViewController`, Chrome Custom Tabs, or a regular browser tab.
A WebView keeps the checkout inside your own screens; a browser surface takes less configuration and suits
flows where opening a sheet over the app is acceptable.
The same ones as the embedded checkout on web: cards, Apple Pay, Google Pay, and crypto, controlled by the
`payment` URL parameter. See [payment methods](/payments/embedded/guides/payment-methods).
## Next Steps
Render only the wallet button and build the rest of the experience in your own UI
Production approval and native Android configuration for Google Pay
Theme the checkout through the appearance parameter
# Overview
Source: https://docs.crossmint.com/payments/embedded/overview
Create seamless purchases with a fully customizable checkout experience
For an overview of our checkout solution, please refer to the [introduction](/payments/introduction). This guide specifically covers embedded checkout features.
## See It in Action
The video below showcases Embedded Checkout for memecoins on fomo, highlighting a seamless mobile onboarding experience with Apple Pay.
## When is the Embedded Checkout the best fit?
Embedded Checkout is the best choice for most teams. It offers experiences that can feel fully native (see the video above), and is a more complete product than headless. Embedded handles orchestration between multiple PSPs, error and state management, and has better approval rates than headless.
## Integration Methods
Embedded Checkout renders the same checkout experience on every platform. Pick the integration that matches your stack:
| Platform | Integration | Start here |
| - | - | - |
| Web | React SDK | [Quickstart](/payments/embedded/quickstarts/credit-card-nft) |
| React Native | React Native SDK | [Quickstart](/payments/embedded/quickstarts/credit-card-memecoin-react-native) |
| Flutter | Flutter SDK | [Quickstart](/payments/embedded/quickstarts/credit-card-memecoin-flutter) |
| Android (Kotlin) | Kotlin SDK | [SDK reference](/sdk-reference/checkout/kotlin/index) |
| iOS (Swift) or any other stack | Your own WebView | [Mobile WebView Integration](/payments/embedded/guides/webview-integration) |
The SDKs are the fastest path on their platforms. The [WebView integration](/payments/embedded/guides/webview-integration) works anywhere a WebView does and gives you full control over how the checkout is hosted.
## Get Started
Start selling digital assets in 5 minutes.
Contact our sales team for advanced support.
## Advanced Topics
# One-Tap Pay - Tokens (React)
Source: https://docs.crossmint.com/payments/embedded/quickstarts/apple-pay-only
Render only the Apple Pay or Google Pay button and build the rest of the checkout experience in your own UI
## Introduction
This guide shows you how to configure Crossmint's Embedded Checkout so it renders **only the Apple Pay or Google Pay button**, and everything else on the screen is yours: your amount selector, your token UI, your success states. Rendering just the wallet button gives users a smooth one-tap purchase experience inside your own UI.
You'll learn how to:
* Restrict the embedded checkout to a single payment method
* Create the order server-side so the pay button is ready when the user sees it
* Hide the checkout's built-in inputs and surface payment errors in your layout
* Detect success and update your own UI
## The five required settings
The whole experience is this configuration. There is no single "Apple Pay only" switch: restricting the payment methods removes the tabs and forms, hiding the two inputs removes the rest, and the error rule gives payment failures a place to render.
```tsx theme={null}
```
`GlobalMessage: { display: "visible" }` is essential for pay-button-only integrations. It renders payment failures
(for example, a payment blocked by risk checks) directly above the button. Without it, this layout has no other
surface for errors, so a declined order looks like nothing happened.
To reach buyers on Android and desktop Chrome too, enable Google Pay — but keep exactly **one** wallet enabled per
visitor, or the checkout shows a method selector in browsers that support both. Detect Apple Pay in your page and
flip the flags:
```tsx theme={null}
// detect after mount: window only exists in the browser, and frameworks
// that render on the server (such as Next.js) crash on it during render
const [applePay, setApplePay] = useState(false);
useEffect(() => {
const session = (window as { ApplePaySession?: { canMakePayments(): boolean } })
.ApplePaySession;
setApplePay(Boolean(session?.canMakePayments()));
}, []);
// in the component props:
allowedMethods: applePay
? { applePay: true, card: false, googlePay: false }
: { applePay: false, card: false, googlePay: true }
```
This keeps the one-tap, single-button look everywhere: Apple Pay in Safari and on iPhone, Google Pay elsewhere.
## Prerequisites
Create everything below in the **same project**: the server key that creates the order, the client key the widget
renders with, and the Apple Pay domain registration all work together, and the payment sheet validates against the
project the order belongs to.
From the Crossmint Console, under **Integrate → API Keys**, create:
* A **client key** (`ck_staging_...`) with the `orders.read` scope, plus `users.create`, `users.read`,
`wallets.read`, `wallets.create`, and `wallets:balance.read` if you also use Crossmint Auth and Wallets as
the demo app does. Add every origin the app runs on (for example `http://localhost:3000` and your public
domain) to the key's **origins allowlist**.
* A **server key** (`sk_staging_...`) with the `orders.create` and `orders.read` scopes.
Apple Pay validates the merchant against the domain serving your page, so the domain must be publicly
reachable over HTTPS — `localhost` cannot be registered. For local development, tunnel your dev server with
ngrok (`ngrok http 3000`) and use the resulting domain.
In the console, under **Integrate → Apple Pay Domains**:
1. Download the verification file and serve it at
`https:///.well-known/apple-developer-merchantid-domain-association`. In Next.js, place it at
`public/.well-known/apple-developer-merchantid-domain-association`.
2. Enter your domain and click **Verify domain**. The status turns to `verified` immediately once the file is
reachable.
See the [Apple Pay setup guide](/payments/embedded/guides/apple-pay) for framework-specific hosting details.
The purchased tokens are delivered to a Solana wallet address. Use any address you control, or create wallets
for your users with [Crossmint Wallets](/wallets/quickstarts/client-side-wallets) as the demo app does.
## Integration
The example buys XMEME, the staging test token, on Solana. Production tokens will not work in the staging environment.
Create `.env.local` in your Next.js project root:
```sh theme={null}
NEXT_PUBLIC_CROSSMINT_CLIENT_API_KEY="_YOUR_CLIENT_API_KEY_"
SERVER_API_KEY="_YOUR_SERVER_API_KEY_"
```
Create `app/checkout/actions.ts`. The server key never reaches the browser, and the order carries the recipient and the receipt email, so the checkout never needs to ask for them:
```ts theme={null}
"use server";
const serverApiKey = process.env.SERVER_API_KEY ?? "";
const baseUrl = serverApiKey.startsWith("sk_production")
? "https://www.crossmint.com"
: "https://staging.crossmint.com";
// XMEME test token on Solana (staging only)
const TOKEN_LOCATOR = "solana:7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu";
export async function createOrder(
amountUsd: string,
email: string,
walletAddress: string,
) {
const res = await fetch(`${baseUrl}/api/2022-06-09/orders`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": serverApiKey,
},
body: JSON.stringify({
lineItems: {
tokenLocator: TOKEN_LOCATOR,
executionParameters: {
mode: "exact-in", // amount is the exact USD to spend
amount: amountUsd,
maxSlippageBps: "500",
},
},
payment: {
method: "card", // wallet payments ride the card rail at creation time
receiptEmail: email,
},
recipient: {
walletAddress,
},
}),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.message ?? "Failed to create order");
}
return {
orderId: data.order.orderId as string,
clientSecret: data.clientSecret as string,
};
}
```
The base URL must match your key's environment: `sk_staging_` keys pair with `staging.crossmint.com` and
`sk_production_` keys with `www.crossmint.com`.
Create `app/checkout/page.tsx`. Three details make the experience feel native:
* **Create the order as soon as the user picks an amount**, not on a separate confirm button. Order creation takes a moment, so starting early means the Apple Pay button is ready by the time the user looks at it.
* **Show a loader until the iframe reports content** via the `ui:height.changed` message, then reveal the button.
* **The Apple Pay button is the CTA.** Do not add your own "Continue" button in front of it; one tap should open the payment sheet.
```tsx theme={null}
"use client";
import { useEffect, useState } from "react";
import {
CrossmintProvider,
CrossmintCheckoutProvider,
CrossmintEmbeddedCheckout,
useCrossmintCheckout,
} from "@crossmint/client-sdk-react-ui";
import { createOrder } from "./actions";
const RECIPIENT_WALLET = "_YOUR_SOLANA_WALLET_ADDRESS_";
const RECEIPT_EMAIL = "_YOUR_EMAIL_";
const AMOUNTS = ["1", "5", "10"];
function Checkout() {
const [amount, setAmount] = useState("1");
const [order, setOrder] = useState<{
orderId: string;
clientSecret: string;
} | null>(null);
const [ready, setReady] = useState(false);
const { order: checkoutOrder } = useCrossmintCheckout();
// create the order as soon as the user picks an amount, so the Apple Pay
// button is already loading by the time they look at it. The cleanup flag
// discards responses from amounts the user has already moved past.
useEffect(() => {
let current = true;
setOrder(null);
setReady(false);
void createOrder(amount, RECEIPT_EMAIL, RECIPIENT_WALLET).then((created) => {
if (current) {
setOrder(created);
}
});
return () => {
current = false;
};
}, [amount]);
// reveal the button once the iframe has content (ui:height.changed)
useEffect(() => {
const onMessage = (e: MessageEvent) => {
if (!e.origin.endsWith(".crossmint.com")) return;
const event = (e.data as { event?: string } | null)?.event;
if (event === "ui:height.changed" || event === "ui:express-checkout.ready") {
setReady(true);
}
};
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, []);
// success = the order reaches the delivery phase
const succeeded =
checkoutOrder?.phase === "delivery" || checkoutOrder?.phase === "completed";
if (succeeded) {
return
Payment complete. Tokens on the way!
;
}
return (
{AMOUNTS.map((a) => (
))}
{!ready &&
Loading…
}
{order && (
)}
);
}
export default function CheckoutPage() {
return (
Buy XMEME
);
}
```
Note the `key={order.orderId}`: changing the amount creates a fresh order, and keying the component by order id remounts the widget cleanly instead of navigating the iframe in place.
Open your **registered HTTPS domain** (not `localhost`) in Safari 17+ on macOS, or on an iPhone running iOS 17+, with a card added to Apple Wallet. Use a physical device rather than the iOS Simulator, which does not render Apple Pay. In staging the payment is processed in the PSP's sandbox, so the card in your Wallet is never actually charged.
To exercise the full flow from any browser during development, temporarily flip the allowed methods to the card form and pay with the staging test card `4242 4242 4242 4242` (any future expiry, any CVC):
```tsx theme={null}
allowedMethods: { applePay: false, card: true, googlePay: false }
```
More on testing can be found [here](/payments/advanced/testing-tips#test-credit-card-numbers).
## One-Tap on Mobile
The five settings above are order and URL parameters, so the same configuration renders inside a native app unchanged. The recommended integration paths are the Swift and [Kotlin](/sdk-reference/checkout/kotlin/index) SDKs, which render and configure the checkout for you. If you prefer full control and do not want to add an SDK, create the order exactly as in step 2 and render the checkout in your own WebView following the [Mobile WebView Integration guide](/payments/embedded/guides/webview-integration):
```tsx theme={null}
// React Native: the same payment and appearance configuration, in a WebView
const checkoutUrl =
`https://staging.crossmint.com/sdk/2024-03-05/embedded-checkout?` +
new URLSearchParams({
orderId: order.orderId,
clientSecret: order.clientSecret,
payment: JSON.stringify({
receiptEmail: RECEIPT_EMAIL, // must also be here, or the email input renders
crypto: { enabled: false },
fiat: {
enabled: true,
allowedMethods: { applePay: true, card: false, googlePay: false },
},
}),
appearance: JSON.stringify({
rules: {
DestinationInput: { display: "hidden" },
ReceiptEmailInput: { display: "hidden" },
GlobalMessage: { display: "visible" },
},
}),
apiKey: CLIENT_API_KEY,
}).toString();
;
```
Two things get simpler on mobile:
* **Simpler wallet detection.** The operating system determines the wallet, so replace the `ApplePaySession` check from the tip above with a platform check: enable Apple Pay on iOS and Google Pay on Android (`Platform.OS === "ios"` in React Native), keeping one wallet enabled per device.
* **No Apple Pay domain registration.** The checkout page is served from `crossmint.com`, which is already enabled for Apple Pay, so the domain steps in the prerequisites apply to the web integration only.
The [Mobile WebView Integration guide](/payments/embedded/guides/webview-integration) covers the WebView configuration, order tracking from a native app, and mobile testing.
## Troubleshooting
Apple could not validate the domain serving your page. Check, in order:
1. **You are on a registered domain.** The page must be served from a domain that shows `verified` under
**Apple Pay Domains** in the console — `localhost` always fails validation. Use your ngrok or production
domain instead.
2. **The domain matches exactly.** The registration is per host: `app.example.com` and `example.com` are
different domains, and a new ngrok tunnel gets a new domain that needs registering again.
3. **Everything belongs to one project.** The order (server key), the widget's `apiKey` (client key), and the
Apple Pay domain registration must all come from the same console project. If the order was created with a
key from a different project, validation fails even with a verified domain.
* Serve the page from your registered HTTPS domain: even in Safari, Apple Pay is unavailable on plain
`http://localhost`, which surfaces as an "Apple Pay is not available" message. Open the app through your
ngrok or production domain instead.
* Apple Pay renders in Safari 17+ on macOS and iOS 17+; other browsers show nothing where the button would
be (enable `googlePay: true` to cover them). Test in Safari or on an iPhone.
* Use a physical device: the iOS Simulator does not render Apple Pay.
* Confirm `payment.fiat.allowedMethods` sets `applePay: true` and the order was created successfully (the
`ui:height.changed` message only fires once the iframe has content).
* Add the exact origin the app runs on (scheme + host + port) to the client key's **origins allowlist** in the
console.
* Match the base URL to the key environment: `ck_staging_`/`sk_staging_` keys pair with
`staging.crossmint.com`, production keys with `www.crossmint.com`.
The error names the missing scope. The widget's client key needs `orders.read`; if you also use Crossmint Auth
and Wallets like the demo app, add `users.create`, `users.read`, `wallets.read`, `wallets.create`, and
`wallets:balance.read`. Edit the key's scopes in the console under **Integrate → API Keys**.
Pass `GlobalMessage: { display: "visible" }` in `appearance.rules`. In the pay-button-only layout this rule is
the only surface where payment errors (such as a payment blocked by risk checks) can render.
## Next steps
Render the same experience inside your iOS, Android, or React Native app
Host the domain association file and verify your domain
Match the button and messages to your brand
Enable Google Pay or cards with the same pattern
# Pay with Card - Memecoins (React)
Source: https://docs.crossmint.com/payments/embedded/quickstarts/credit-card-memecoin
Create a fully customized embedded memecoin checkout experience that accepts credit cards
## Introduction
This guide will show you how to accept credit card payments using Crossmint's Embedded Checkout for memecoin sales. You'll learn how to:
* Set up credit card payments for Solana memecoin purchases in JavaScript
* Implement an embedded checkout UI using Crossmint's React components
* Track order status and delivery
## Embedded Memecoin Checkout
The fastest way to start selling memecoins is to use our embedded checkout solution adapted for fungible tokens.
### Important Parameters
Before implementing the checkout, note these key parameters:
* `receiptEmail`: Optional. If the buyer pays with Apple Pay or Google Pay, Crossmint obtains the email automatically. Providing it improves fraud signal quality.
* `executionParameters.mode`: Set to "exact-in" for memecoin purchases (specifies exact USD amount to spend). "exact-out" is for NFTs, while "exact-in" is for fungible tokens.
* `maxSlippageBps`: Optional slippage tolerance (default is typically 500 BPS or 5% if not specified)
**Testing in Staging?** Use the XMEME test token: `solana:7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu`. Production tokens will not work in the staging environment.
### Memecoin Embedded Integration
Next, we will set up a project file with Crossmint's embedded checkout to accept memecoin purchases.
Create `.env.local` in your project root:
```sh theme={null}
NEXT_PUBLIC_CLIENT_API_KEY="_YOUR_CLIENT_API_KEY_" # From API Keys page
NEXT_PUBLIC_TOKEN_ADDRESS="7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu" # xmeme token for staging testing
NEXT_PUBLIC_RECIPIENT_WALLET_ADDRESS="YOUR_SOLANA_WALLET_ADDRESS" # Add desired recipient wallet
NEXT_PUBLIC_RECEIPT_EMAIL="YOUR_EMAIL" # Add desired recipient email
```
Create `/src/app/page.tsx` with:
```tsx theme={null}
"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 (
);
}
```
For more details on tokenLocator formatting and other item selection options, see our [embedded item selection guide](/payments/embedded/guides/item-selection#item-selection-examples).
Test purchases in staging using test credit cards, like `4242424242424242` (Visa). More information on testing can be found [here](/payments/advanced/testing-tips#test-credit-card-numbers).
For production launch with live memecoin tokens, contact the Crossmint sales team — production Checkout requires a signed Order Form and successful KYB. See the [production launch guide](/payments/embedded/guides/production-launch) for the full sequence.
Here's how your embedded checkout will look after implementation:
Note the quote expiration timer above the checkout button.
🎉 Congratulations! You've successfully set up your embedded memecoin checkout. Check out the [Next Steps](#next-steps) section below to learn how to customize your integration.
## Next Steps
Learn how to customize the embedded checkout experience
Implement webhook handling for order updates
If the quote expires (after 1 minute), the embedded checkout will automatically refresh the quote. You can
customize this behavior using the checkout hooks.
# Pay with Card - Memecoins (Flutter)
Source: https://docs.crossmint.com/payments/embedded/quickstarts/credit-card-memecoin-flutter
Create a fully customized embedded memecoin checkout experience for Flutter that accepts credit cards
For this quickstart, you will create a minimal Flutter app. Make sure you have Flutter installed.
## Create a New Flutter Project + Crossmint SDK
```bash theme={null}
flutter create crossmint_checkout_quickstart
cd crossmint_checkout_quickstart
```
Pass your API key and configuration via Dart defines:
```bash theme={null}
flutter run \
--dart-define=CROSSMINT_API_KEY=YOUR_CLIENT_SIDE_API_KEY \
--dart-define=RECIPIENT_WALLET=YOUR_SOLANA_WALLET_ADDRESS \
--dart-define=RECEIPT_EMAIL=YOUR_EMAIL@example.com
```
The `receiptEmail` field is **optional**. If the buyer pays with Apple Pay or Google Pay, Crossmint obtains the email automatically. Providing it improves fraud signal quality.
```dart theme={null}
import 'package:flutter/material.dart';
import 'package:crossmint_flutter/crossmint_flutter_ui.dart';
const apiKey = String.fromEnvironment('CROSSMINT_API_KEY');
const recipientWallet = String.fromEnvironment('RECIPIENT_WALLET');
const receiptEmail = String.fromEnvironment('RECEIPT_EMAIL');
void main() {
runApp(const MaterialApp(home: CheckoutScreen()));
}
class CheckoutScreen extends StatelessWidget {
const CheckoutScreen({super.key});
@override
Widget build(BuildContext context) {
final checkoutController = CrossmintCheckoutController();
return Scaffold(
body: SafeArea(
child: CrossmintEmbeddedCheckout(
apiKey: apiKey,
config: CrossmintCheckoutConfig(
order: CrossmintCheckoutNewOrder.typed(
lineItems: [
CrossmintCheckoutLineItem.tokenWithExecutionParameters(
tokenLocator:
'solana:7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu', // XMEME token (staging)
executionParameters: {
'mode': 'exact-in',
'amount': '1',
'maxSlippageBps': '500',
},
),
],
recipient: CrossmintCheckoutWalletRecipient(
walletAddress: recipientWallet,
),
),
payment: CrossmintCheckoutPayment(
fiat: CrossmintCheckoutFiatPayment(enabled: true),
crypto: CrossmintCheckoutCryptoPayment(enabled: false),
receiptEmail: receiptEmail,
),
),
checkoutController: checkoutController,
onOrderUpdated: (order) =>
debugPrint('Order updated: ${order['status']}'),
onOrderCreationFailed: (error) =>
debugPrint('Error: $error'),
),
),
);
}
}
```
Run with the same `--dart-define` flags from step 3:
```bash iOS theme={null}
flutter run -d ios \
--dart-define=CROSSMINT_API_KEY=YOUR_CLIENT_SIDE_API_KEY \
--dart-define=RECIPIENT_WALLET=YOUR_SOLANA_WALLET_ADDRESS \
--dart-define=RECEIPT_EMAIL=YOUR_EMAIL@example.com
```
```bash Android theme={null}
flutter run -d android \
--dart-define=CROSSMINT_API_KEY=YOUR_CLIENT_SIDE_API_KEY \
--dart-define=RECIPIENT_WALLET=YOUR_SOLANA_WALLET_ADDRESS \
--dart-define=RECEIPT_EMAIL=YOUR_EMAIL@example.com
```
```bash Chrome theme={null}
flutter run -d chrome \
--dart-define=CROSSMINT_API_KEY=YOUR_CLIENT_SIDE_API_KEY \
--dart-define=RECIPIENT_WALLET=YOUR_SOLANA_WALLET_ADDRESS \
--dart-define=RECEIPT_EMAIL=YOUR_EMAIL@example.com
```
# Pay with Card - Memecoins (React Native)
Source: https://docs.crossmint.com/payments/embedded/quickstarts/credit-card-memecoin-react-native
Create a fully customized embedded memecoin checkout experience for React Native that accepts credit cards
# Crossmint Checkout React Native SDK Quickstart
For this quickstart we'll be using Expo. You can follow this [tutorial](https://docs.expo.dev/tutorial/create-your-first-app/) to create an expo app. **Only do steps 1 and 3**, plus the Crossmint-specific steps below.
**Prerequisites:** Before starting, ensure you have completed the [Expo prerequisites](https://docs.expo.dev/tutorial/create-your-first-app/#prerequisites) including Node.js, Git, and your development environment setup.
To integrate without the SDK, or on a platform the SDKs do not cover, render the checkout URL in your own WebView
following the [Mobile WebView Integration guide](/payments/embedded/guides/webview-integration).
## Following Expo Tutorial + Crossmint SDK
```bash theme={null}
npx create-expo-app@latest crossmint-checkout-quickstart
cd crossmint-checkout-quickstart
```
```bash theme={null}
npm run reset-project
```
```bash theme={null}
EXPO_PUBLIC_CLIENT_CROSSMINT_API_KEY="your_client_side_api_key"
EXPO_PUBLIC_RECIPIENT_WALLET_ADDRESS="your_solana_wallet_address"
EXPO_PUBLIC_RECEIPT_EMAIL="your-email@example.com"
```
The `receiptEmail` field is **optional**. If the buyer pays with Apple Pay or Google Pay, Crossmint obtains the email automatically. Providing it improves fraud signal quality.
```jsx theme={null}
import { CrossmintEmbeddedCheckout, CrossmintProvider } from "@crossmint/client-sdk-react-native-ui";
export default function Index() {
return (
);
}
```
```bash npm theme={null}
npm run ios
# For Android:
npm run android
```
```bash yarn theme={null}
yarn ios
# For Android:
yarn android
```
```bash pnpm theme={null}
pnpm ios
# For Android:
pnpm android
```
```bash bun theme={null}
bun ios
# For Android:
bun android
```
### Result
# Pay with Card - NFTs (React)
Source: https://docs.crossmint.com/payments/embedded/quickstarts/credit-card-nft
Embed a onchain checkout into a web app in under 5 minutes
## Introduction
In this guide, you will create a web app with Next.js which allows customers to buy NFTs with credit card and crypto payments, using Crossmint's embedded checkout. Crossmint also supports payments for [crypto onramp](/onramp/quickstarts/react), [memecoins](/payments/headless/quickstarts/credit-card-memecoin-cko), and other onchain assets (ERC20 tokens, ERC721 tokens, and ERC1155 tokens).
If you want to get started immediately, you can clone a repo with a functioning embedded checkout here: [https://github.com/Crossmint/crossmint-embedded-demo](https://github.com/Crossmint/crossmint-embedded-demo). If you want to get started step by step, continue following the guide below.
### Basic Integration
Perfect for getting started quickly with a simple checkout flow.
Create `.env.local` in your project root:
```sh theme={null}
NEXT_PUBLIC_CLIENT_API_KEY="_YOUR_CLIENT_API_KEY_" # From API Keys page
NEXT_PUBLIC_COLLECTION_ID="_YOUR_COLLECTION_ID_" # From Collection details page
```
Replace your home page (e.g `{root_dir}/app/page.tsx`) with:
```tsx theme={null}
"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 collectionId = process.env.NEXT_PUBLIC_COLLECTION_ID as string;
return (
);
}
```
Navigate to the directory your package.json is to run the app
```bash theme={null}
cd embedded-checkout/crossmint-embedded-checkout-demo
```
### Advanced Integration
Need purchase tracking, multiple NFT purchases at once, or more customization? Here's a complete setup with additional features:
```tsx theme={null}
"use client";
import { useEffect } from "react";
import {
CrossmintProvider,
CrossmintCheckoutProvider,
CrossmintEmbeddedCheckout,
useCrossmintCheckout,
} from "@crossmint/client-sdk-react-ui";
// Component with purchase tracking
function Checkout() {
const { order } = useCrossmintCheckout();
const collectionId = process.env.NEXT_PUBLIC_COLLECTION_ID as string;
useEffect(() => {
if (order && order.phase === "completed") {
console.log("Purchase completed!");
// Handle successful purchase
}
}, [order]);
return (
);
}
// Main component with providers
export default function Home() {
const clientApiKey = process.env.NEXT_PUBLIC_CLIENT_API_KEY as string;
return (
);
}
```
This advanced example showcases: - Multiple NFTs in one checkout - Purchase status tracking - Preferred payment
methods and currencies - Email-based NFT delivery - Language settings Learn more in our guides: - [Payment
Methods](/payments/embedded/guides/payment-methods) - [Multi-purchases](/payments/pay-button/guides/item-selection#multiple-item-orders) -
[React Hooks](/payments/embedded/guides/hooks)
### Testing Your Integration
This demo uses the staging environment: - Use [test credit
cards](/payments/advanced/testing-tips#test-credit-card-numbers) for payments - Get test USDC from our
[faucet](/payments/advanced/testing-tips#getting-test-tokens) - Check [price
limits](/payments/advanced/testing-tips#limits-in-staging) for staging
## All Set!
You've successfully integrated card and crypto payments with the Crossmint Embedded Checkout!
Remember this demo is built on staging, so the digital assets will show up on the testnets. To launch on production,
check the [production launch checklist](/payments/advanced/production-launch). You will need to contact
[Sales](https://www.crossmint.com/contact/sales) to enable the embedded checkout on production.
## Next Steps
### Customize the UI and Behavior Further
Learn how to customize the look and feel of your checkout
Configure available payment options for your users
Use React hooks to build custom checkout experiences
### Advanced Topics
# Create/Deploy an NFT Collection
Source: https://docs.crossmint.com/payments/guides/create-collection
Easily deploy a collection in the console and enable payments
The easiest way to get started with selling or airdropping NFTs is to use the developer console to deploy and manage your NFT contracts.
## Create a Crossmint NFT Collection
This guide will deploy an ERC-721 contract on the Polygon Amoy testnet. Currently, you can also deploy to the mainnets and testsnets for Polygon, Base, Optimism, and Solana directly from the console. Additional chains are supported when deploying via the [create-collection API](/api-reference/minting/collection/create-collection).
This information is displayed in the Hosted Checkout. You can edit it later.
If you want to use this collection exclusively with the [Minting Tools](/minting/introduction), select the `Airdrop NFTs` option only.
Crossmint has staging and production [environments](/introduction/platform/staging-vs-production) to facilitate working in testnets or mainnets.
When creating a new collection on production you'll need some API credits to cover the gas cost of contract deployment.
#### NFT Price
Enter the price you want to charge for your NFT. When using the staging environment, please set very low test prices to help use preserve our testnet currency. Everything works the same in staging and production, so you can test your collection with low prices in staging and then deploy to production with higher prices.
In production, there is a lower bound limit of USD $0.75, and an upper bound limit of USD $1,500 per transaction. You can find more information if you need to [increase this limit](/payments/advanced/production-launch#default-transaction-limit).
#### USDC
Several chains support USDC for the currency. This option is useful for price stability and to receive payouts in a stable token. You can find more info on [configuring USDC here](/payments/advanced/usdc-support).
### Fee Sponsorship
This option enables you to control who pays the fees of the NFT purchase including gas and credit card transaction fees. The default option is "Buyer", which means that if you set a price of 10 USDC, the final price the buyer pays will be 10 USDC plus fees.
If you want to provide a consistent price for your customers, select the "You" option to sponsor the fees. When you sponsor the fees they will be deducted from the amount the buyer pays and the remainder will be paid to the recipient wallet you set in the next step.
#### Recipient Address
Enter the wallet address where you want to receive payments. This is the address where Crossmint will send the proceeds from NFT sales.
Want to split payments across multiple addresses? Use a [splits.org](https://splits.org) address as the payout recipient.
Make sure you review the Content policy. When launching in production you'll need to submit collection verification information to ensure your collection is compliant with our Content Policy. You can find more information about everything required for your production launch in the [Production Launch](/payments/advanced/production-launch) section.
## Adding NFTs
This is **required** before you can sell or airdrop the NFTs. Without this step there is not any actual information to use for creating the NFTs yet. You can create unique NFT metadata per token or an Open Edition style NFT where all tokens share the same metadata.
#### NFT Name
The name of your NFT (max 32 characters).
#### Supply
For unique per NFT, enter 1. You will need to configure the metadata for each NFT. You can do this via the developer console individually. For projects with many unique NFTs you should use the [Create Template API](/api-reference/minting/template/create-template) with a script.
For an Open Edition NFT, enter the total quantity, or select the Unlimited checkbox. This will be the only metadata you need to configure.
#### Description
The description of your NFT (max 64 characters).
#### Image
Upload an image for your NFT.
Recommended image formats are: JPEG, PNG, WEBP, or GIF. In most cases, the medium in which NFTs are displayed don't require extremely high resolution files. Strike a balance and lean towards file sizes that do not require significant bandwidth to download (below 10 MB for example).
#### Attributes `optional`
Additional attributes of the NFT. Add as few or as many as you like. You can refer to the [OpenSea metadata standards](https://docs.opensea.io/docs/metadata-standards#attributes) page for detailed explanations of how to use these in your project.
Click the `Create NFT` button to complete.
You can download an example of how your file should be structured here: [batch-upload-example.zip](https://www.crossmint.com/assets/examples/batch-upload-example.zip).
You can upload a maximum of 1000 items at a time. For collections that require more than 1000 tokens, you can repeat the batch upload process multiple times.
The CSV should include a header row with the following fields:
* `name` - The name for the token.
* `image` - The filename of the image, which will be uploaded in the next step.
* `description` - A description for the token.
* `supply ` - How many of the token should be available.
* `animation_url` - (optional)
You can add attributes via additional columns in the spreadsheet. For example, to add an attribute named `weapon`, include an additional column with the attribute name for the header row. You can refer to the example file linked above to see this in action.
Once you have your CSV ready, upload it to move on to the next step for media files.
The image file names must match the value(s) in the `image` column of the uploaded CSV file. These files should be in a flat folder structure for upload (no sub-folders).
Click the "Upload media files" button and then select all of the images referenced in the `metadata.csv` file you uploaded in the previous step.
Click the "Upload" button to upload the CSV and media files. If any media files are missing you'll be notified which ones they are in the UI.
If you need to upload additional token metadata click the "New batch upload" button. Otherwise select "View my NFTS" to close the modal and view the metadata for your tokens.
You can also perform bulk uploading by calling the [create-template](/api-reference/minting/template/create-template) API using a script that loops through your `metadata.csv` file. You'll need to have an [API key](/introduction/platform/api-keys#how-to-get-and-use-api-keys) with the `nfts.create` scope enabled.
Below is an example script that will keep your request volume below the 120/min [rate limit](/introduction/platform/api-keys#rate-limits).
This code example is available as a repository on GitHub also:
[crossmint/bulk-uploader](https://github.com/Crossmint/bulk-uploader)
Before running this script you'll need to upload your media files to a service that can host the files, such as [Pinata](https://www.pinata.cloud/).
The `image` column of your `metadata.csv` should include the full URL to the file instead of only a file name like the batch upload option above.
```javascript bulkUploader.js theme={null}
const fs = require("fs");
const csv = require("csv-parser");
require("dotenv").config();
const collectionId = process.env.COLLECTION_ID;
const apiKey = process.env.API_KEY;
const apiUrl = `https://staging.crossmint.com/api/2022-06-09/collections/${collectionId}/templates`;
// Rate limiting setup
const rateLimit = 100; // requests per minute
const interval = 60000 / rateLimit; // interval in milliseconds
async function sendRequest(data) {
try {
const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-KEY": `${apiKey}`,
},
body: JSON.stringify(data),
});
const responseData = await response.json();
console.log("Success:", responseData);
} catch (error) {
console.error("Error:", error.message);
}
}
// Read and process the CSV file
function processFile() {
let promise = Promise.resolve();
fs.createReadStream("metadata.csv")
.pipe(csv())
.on("data", (row) => {
promise = promise.then(() => {
const postData = {
metadata: {
name: row.name,
image: row.image, // this should be a publicly accessible URL
description: row.description,
},
supply: { limit: Number(row.supply) },
reuploadLinkedFiles: false, // this is optional
};
return sendRequest(postData).then(() => new Promise((resolve) => setTimeout(resolve, interval)));
});
})
.on("end", () => {
promise.then(() => {
console.log("Finished processing file.");
});
});
}
processFile();
```
Run the script to bulk upload your template metadata.
```node theme={null}
node bulkUploader.js
```
## Send a Test NFT
Once you have added metadata you can send a test NFT to yourself or another user. The interface to mint and send from the console supports email and wallet address for the recipient.
You can mint and send a test NFT to an email address or wallet to make sure everything is working as expected.
If you enter an email address the NFT will be sent to a unique Crossmint custodial wallet associated with that email address. The owner of this email can log in to Crossmint to view their NFT. The wallet option will send directly to the wallet address.
Crossmint does ***not*** send an email to the user when you airdrop NFTs from the console.
That's all there is to it. Check below for recommended next steps.
# Register External Collection
Source: https://docs.crossmint.com/payments/guides/register-collection
Import custom and/or third party contracts
Crossmint has a pre-audited library of [smart contracts](/minting/nfts/integrate/create-collections) that serve most use cases. However, if you have custom needs, you can also bring your own.
The Checkout has been battle-tested at scale with Crossmint's collections, and may result in a more reliable
experience than using an untested, imported contract, or at least require less trouble-shooting.
* When you require very custom functionality not supported by Crossmint's contracts.
* If you're developing a marketplace.
* If you have an accesslist.
Secondary markets are platforms where NFTs are traded after their initial mint/primary sale. These are marketplaces where users can buy and sell NFTs from each other.
It requires manual support from Crossmint to import secondary contracts. See our [marketplaces and launchpads guide](/payments/advanced/marketplaces-and-launchpads).
The self-serve option is only available for importing primary contracts for EVM and Solana.
When importing contracts, Crossmint has support to fetch listings from the following secondary marketplaces:
* **Auction House**: Solana's native marketplace protocol
* **Tensor**: High-performance Solana NFT marketplace
* **Magic Eden API**: Popular multi-chain NFT marketplace
To add support for a new marketplace, please [contact sales](https://www.crossmint.com/contact/sales?utm_source=devrel\&utm_medium=docs\&utm_campaign=backlinks).
## Quick Navigation
All collection imports start with the same initial steps, then diverge based on your blockchain:
* [Common Steps (All Chains)](#common-steps-all-chains) - Steps 1-4 for all blockchains
* [EVM-Specific Steps](#evm-specific-steps) - Steps for Ethereum, Polygon, etc.
* [Solana-Specific Steps](#solana-specific-steps) - Additional steps for Solana candy machines
## Common Steps (All Chains)
These initial steps are the same whether you're importing an EVM contract or a Solana candy machine:
This information is displayed in the Hosted Checkout. You can edit it later.
Remember, Crossmint has staging and production [environments](/introduction/platform/staging-vs-production).
When working in the staging environment, choose the mainnet name. For example, if your contract is on the
Polygon Amoy testnet, select Polygon.
After completing these common steps, continue with the blockchain-specific instructions below.
## EVM-Specific Steps
Complete the [Common Steps](#common-steps-all-chains) above first, then continue with these EVM-specific steps:
### Pre-requisites
* Your contract must be ERC-721, ERC-721A, or ERC-1155 compliant.
* The minting function must allow minting directly to an address that is different from the one that invoked the contract. And it must contain at least one parameter that specifies that recipient address.
* A single address must be able to call the mint function unlimited times but does not need to be able to hold unlimited NFTs.
### Contract Registration
You may register contracts manually from the console or via API.
When you compile your smart contract there will be a corresponding abi file with an `.abi` or `.json` extension.
Inside this file, you'll see JSON property named abi, which describes the functions in your smart contract. Here's an example of a very simple abi file. Yours will likely have more function descriptions.
```json theme={null}
// Example generated abi file for smart contract
{
"abi": [
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "_to",
"type": "address"
}
],
"name": "mintTo",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
]
}
```
Copy the JSON array object that comes after the string `abi` and paste it into the `Contract ABI` text box in the developer console. The content you paste in should begin with `[` and end with `]`.
Whether your ABI was retrieved automatically or you pasted it in manually, you need to specify the:
* Mint function
* Recipient address parameter name
* \[Optional] Quantity of NFTs to mint parameter name
Crossmint will attempt to automatically select these values for you, but it's important to ensure they are set correctly. Especially if you're setting up a USDC mint function as the list of options will be longer.
Proxy contracts are an advanced feature. You should use this only if you are certain that your contracts adhere to this pattern. This is crucial because Crossmint requires the actual NFT contract address when you register a mint/buy/purchase/claim function in a sales contract or revenue splitter.
If you don't specify the NFT contract address, our system won't be able to extract token URI information or facilitate transfers. Set this up only if it isn't a transparent proxy, which is common for upgradeable contracts.
### EVM Contract Import Steps
Enter the address of your contract and the console will determine the contract type automatically.
Notice in the screenshot that the example has the same contract address that was entered in previous step. This indicates a basic setup where the purchase function is within the registered NFT contract. The only reason to change this is if purchases should be sent to a different contract address that is not the same as the NFT contract.
Most developers do not need to change this. If you are unsure, leave it as is. Transparent upgradeable contracts also do not need to use this setting and should use the beacon address in the previous step.
As long as your contract is verified on the block explorer we can query the ABI automatically. If you have not verified your contract, you must enter the ABI manually.
Unless you specifically deployed a contract that supports USDC you must leave the native currency selected. If you are unsure, leave it as is.
If you selected the native currency in the previous step, this will be a very short list. If you have multiple mint functions in your contract that you want to accept payments for, you must register each one separately.
USDC mint functions typically are NOT `payable`. Change the currency selector to USDC to populate the mint function list with valid options.
The mint function must accept an address parameter and mint to that address. If your mint function lacks this you'll need to deploy a new contract or modify your existing contract.
After the mint function has been selected, this will be a short list built from the function parameters of type `address`. If you have multiple options here, ensure that you select the correct one. Doing this incorrectly will result in the purchased NFTs being sent to the wrong address and they likely will not be recoverable.
This will be a short list built from the function parameters of type `uint256`. If you have multiple options here, ensure that you select the correct one. Doing this incorrectly will result in the NFT checkout tools not being able to properly mint the correct quantity of NFTs.
## Solana-Specific Steps
Complete the [Common Steps](#common-steps-all-chains) above first, then continue with these Solana-specific steps:
Instant support for:
* **Primaries**: Candy Machine v3, Metaplex Instant Sales, Magic Eden's Launchpad
* **Secondaries**: Auction House, Tensor, Magic Eden API
Only the integrations listed above are available at the moment. For custom integrations, please [contact
sales](https://www.crossmint.com/contact/sales?utm_source=devrel\&utm_medium=docs\&utm_campaign=backlinks).
### Solana Candy Machine Import
Crossmint supports Candy Machine v3. After completing the common steps above, continue with these Solana-specific steps:
This step replaces step 4 from the common steps above - select Solana from the blockchain dropdown.
Find the ID in the `cache.json` file in the folder where you deployed your Candy Machine.
Congratulations! 🎉 You've successfully registered your external collection in Crossmint.
## FAQs
For Xion blockchain contracts, please refer to our [Xion Contract Requirements](/minting/advanced/xion-contracts) documentation.
Currently only importing EVM and Solana primary contracts is self-serve. If you wish to import Aptos and Sui contracts, please [contact
sales](https://www.crossmint.com/contact/sales?utm_source=devrel\&utm_medium=docs\&utm_campaign=backlinks).
Yes! You can programmatically register collections using the Collections Registration API.
**API Endpoint:** `POST https://staging.crossmint.com/api/v1-alpha1/collections`
**Required Headers:**
* `Content-Type: application/json`
* `x-api-key: ` (requires `collections.create` scope)
```bash EVM Contract Example theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/v1-alpha1/collections \
--header 'content-type: application/json' \
--header 'x-api-key: ' \
--data '{
"chain": "ethereum",
"contractType": "erc-721",
"args": {
"contractAddress": "0x1234567890123456789012345678901234567890",
"abi": "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_to\",\"type\":\"address\"}],\"name\":\"mintTo\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}]",
"mintFunctionName": "mintTo(address)",
"toParamName": "_to",
"quantityParamName": "_quantity",
"contractType": "erc-721"
},
"metadata": {
"title": "My NFT Collection",
"description": "A custom NFT collection",
"imageUrl": "https://example.com/collection-image.png",
"social": {
"twitter": "https://twitter.com/mycollection",
"discord": "https://discord.gg/mycollection"
}
},
"ownership": "external",
"category": "art",
"scopes": ["payments:credit-card", "payments:cross-chain"]
}'
```
```bash Solana Candy Machine Example theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/v1-alpha1/collections \
--header 'content-type: application/json' \
--header 'x-api-key: ' \
--data '{
"chain": "solana",
"contractType": "candy-machine",
"args": {
"candyMachineId": "your-candy-machine-id-here"
},
"metadata": {
"title": "My Solana Collection",
"description": "A Solana NFT collection",
"imageUrl": "https://example.com/collection-image.png"
},
"ownership": "external",
"category": "art",
"scopes": ["payments:credit-card", "payments:cross-chain"]
}'
```
**Key Parameters:**
* **chain**: `"solana"`, `"ethereum"`, `"polygon"`, `"bsc"`
* **contractType**: `"candy-machine"`, `"erc-721"`, `"erc-1155"`
* **args**: Contract-specific arguments (see examples above)
* For EVM: `contractAddress`, `abi`, `mintFunctionName`, `toParamName`, `contractType` (required); `quantityParamName`, `tokenContractAddress` (optional)
In EVM, if you use a separate contract for minting than for token metadata, set the minting contract address as `contractAddress`, and the token contract address as `tokenContractAddress`.
* For Solana: `candyMachineId` (required)
* **metadata**: Object with `title`, `description`, `imageUrl` (all required), and optional `social` object with `twitter`/`discord` links
* **ownership**: `["self", "external"]`. Set to `"external"` for imported contracts
* **category**: `"art"`, `"gaming"`, `"music"`, `"loyalty"`, `"ticketing"`, `"charity"`, `"other"`
* **scopes**: `["payments:credit-card", "payments:cross-chain"]`. Scopes are required when `ownership` is `"external"`
**Response (201 Created):**
The API returns a collection object with identifiers you can use with Crossmint's checkout and payment systems.
```json theme={null}
{
"collectionId": "abc123-def456-ghi789...",
"clientId": "abc123-def456-ghi789..."
}
```
This API is currently in alpha (v1-alpha1) and subject to change. The returned `collectionId` and `clientId` are typically the same value and can be used interchangeably for checkout integration.
# Client or Server
Source: https://docs.crossmint.com/payments/headless/guides/client-or-server
Understand the reasons to use client-side or server-side API keys
The headless checkout supports both server-side and client-side API keys. It's important that you select the right key for your implementation.
## When to use a server-side API key
* Testing in the [API Playground](/api-reference/headless/create-order) of the documentation
* Testing with cURL requests or running scripts from your command line
* Building applications that make API calls to your own backend, which then make the actual API call to Crossmint
The key consideration here is if the API request is coming from a server environment.
### Server Side Example Code
```typescript route.ts (server-side) theme={null}
import { callCrossmintAPI } from "@/app/utils/crossmint";
import { NextRequest, NextResponse } from "next/server";
const crossmintBaseUrl = process.env.CROSSMINT_API_URL;
const crossmintApiKey = process.env.CROSSMINT_API_KEY;
const crossmintAPIHeaders = {
accept: "application/json",
"content-type": "application/json",
"x-api-key": crossmintApiKey,
};
const callCrossmintAPI = async (endpoint: string, options: { method: string; body?: any; params?: any }) => {
const url = `${crossmintBaseUrl}/${endpoint}`;
const { body, method } = options;
const response = await fetch(url, {
body: body ? JSON.stringify(body) : null,
method,
headers: crossmintAPIHeaders,
});
const json = await response.json();
return json;
};
export async function POST(req: NextRequest, res: NextResponse) {
try {
const body = await req.json();
console.log("create order: ", body);
const apiResponse = await callCrossmintAPI("/orders", {
method: "POST",
body,
});
return NextResponse.json(apiResponse, { status: 200 });
} catch (error) {
console.log("failed to create order");
return NextResponse.json({ message: "Error creating order" }, { status: 500 });
}
}
```
## When to use a client-side API key
* Your application will be making API requests to Crossmint directly from a browser
When you create client-side API keys you must add the authorized origins that can use the key. For example, in
testing you'll need to indicate `http://localhost:3000` (or similar local dev URLs) as authorized origins, or the
request will be denied.
There is one additional step when using a client-side API key in your application with headless checkout. The first call will be to create the order. The response will include a `clientSecret` property that you must persist in state and then pass as an additional header in subsequent API requests to the [update-order](/api-reference/headless/edit-order) or [get-order](/api-reference/headless/get-order) routes.
### Client Side Example Code
The sample code below shows direct client-side API calls to Crossmint using a client-side API key. The `component.tsx` file is simplified to only show the relevant logic for making requests directly from the browser to Crossmint's API.
```tsx component.tsx (create-order) theme={null}
// note the end of try block where the clientSecret is saved to local state
const { setOrder, setClientSecret } = useLocalState();
const createOrder = async (orderInput: any) => {
try {
const res = await fetch(`https://staging.crossmint.com/api/2022-06-09/orders`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.NEXT_PUBLIC_CLIENT_SIDE_KEY,
},
body: JSON.stringify(orderInput),
});
const order = await res.json();
setOrder(order.order);
setClientSecret(order.clientSecret);
} catch (e) {
console.error(e);
throw new Error("Failed to create order");
}
};
```
```tsx component.tsx (update-order) theme={null}
// note the `authorization` header, which contains previously saved clientSecret
const { order, clientSecret, setOrder } = useLocalState();
const updateOrder = async (orderInput: any) => {
try {
const res = await fetch(`https://staging.crossmint.com/api/2022-06-09/orders/${order.orderId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.NEXT_PUBLIC_CLIENT_SIDE_KEY,
authorization: clientSecret,
},
body: JSON.stringify(orderInput),
});
const updatedOrder = await res.json();
setOrder(updatedOrder);
} catch (e) {
console.error(e);
throw new Error("Failed to update order");
}
};
```
```tsx component.tsx (get-order) theme={null}
// note the `authorization` header, which contains previously saved clientSecret
const { order, clientSecret, setOrder } = useLocalState();
const getOrder = async () => {
try {
const res = await fetch(`https://staging.crossmint.com/api/2022-06-09/orders/${order.orderId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.NEXT_PUBLIC_CLIENT_SIDE_KEY,
authorization: clientSecret,
},
});
const refreshedOrder = await res.json();
setOrder(refreshedOrder);
return refreshedOrder.lineItems[0].delivery.status;
} catch (e) {
console.error(e);
throw new Error("Failed to fetch order");
}
};
```
### Mobile Applications
When using client-side API keys in mobile applications, you must include an additional `x-app-identifier` header in your requests. This header should contain the bundle identifier (iOS) or package name (Android) that you whitelisted when creating your API key.
```swift iOS (Swift) theme={null}
let headers = [
"Content-Type": "application/json",
"x-api-key": "your-client-side-api-key",
"x-app-identifier": "com.yourcompany.yourapp", // iOS Bundle ID
"authorization": clientSecret // When updating/reading orders
]
```
```kotlin Android (Kotlin) theme={null}
val headers = mapOf(
"Content-Type" to "application/json",
"x-api-key" to "your-client-side-api-key",
"x-app-identifier" to "com.yourcompany.yourapp", // Android Package Name
"authorization" to clientSecret // When updating/reading orders
)
```
```javascript React Native theme={null}
const headers = {
"Content-Type": "application/json",
"x-api-key": "your-client-side-api-key",
"x-app-identifier": "com.yourcompany.yourapp", // Bundle ID or Package Name
authorization: clientSecret, // When updating/reading orders
};
```
The `x-app-identifier` must match one of the mobile app identifiers you whitelisted when creating your client-side
API key. For more information on setting up mobile app identifiers, see the [Client-side API
Keys](/introduction/platform/api-keys/client-side#mobile-app-identifiers) documentation.
# Design Your UI
Source: https://docs.crossmint.com/payments/headless/guides/design-your-ui
How to get started with setting up your UI
This guide will walk you through the basic steps of implementing a user interface integrated with the Headless Checkout. The first step in the process will be to create an Order via API call.
You can create an order on load and add the recipient later, or wait to create the order when you have the recipient info ready. Creating the order immediately helps obtain the price quote for display. Let's proceed with that approach in mind.
## Building Your App
There are four key components you'll need to represent in the UI for your users.
1. Purchase Preview - a visual explanation of what they are buying and the price
2. Wallet Connection - cross-chain payments will require the ability to connect a wallet
3. Payment Status - keeping the user updated about the payment acceptance
4. Delivery Status - progress of delivery and information upon completion/failure
### Purchase Preview
It is common to create an order upon initial render for your user, and then update the details of that order as the adjustments are made by the user. You'll need to create a function in your application that can call the create-order API and save the response.
After you create the `order` you'll get back metadata related to the `collectionId` you pass in the [create-order](/api-reference/headless/create-order) call. You can use this metadata to build the preview highlighted in the screenshot below.
```jsx theme={null}
useEffect(() => {
if (order != null) {
return;
}
createOrder({
payment: {
method: "ethereum-sepolia",
currency: "eth",
},
locale: "en-US",
lineItems: [
{
collectionLocator: "crossmint:YOUR_CROSSMINT_COLLECTION_ID",
callData: {
totalPrice: "0.001",
},
},
],
});
}, [order]);
```
### Wallet Connection
To enable the cross-chain payments feature, you'll need to provide an easy way for users to connect their wallet. This guide will not go into detail about how to add any specific tools, since there are a variety available and the implementation of each is unique to the tool.
However, you will want to set up your app to listen for changes to the primary connected wallet and selected network so that the order is updated automatically.
Once the user has connected their wallet they might still change the primary connected account or network. You need to listen for these changes and update the order.
Note the dependency array at the end of this `useEffect` watches `primaryWallet` and `selectedNetwork`. If any of these change your app needs to refresh the order with the new values.
```jsx theme={null}
useEffect(() => {
if (order == null || primaryWallet == null) {
return;
}
const currentRecipient = order.lineItems[0].delivery.recipient?.walletAddress;
const isSameWallet = currentRecipient === primaryWallet.address;
const isSameNetwork = order.payment.method === selectedNetwork;
if (isSameWallet && isSameNetwork) {
return;
}
updateOrder({
recipient: {
walletAddress: primaryWallet.address,
},
payment: {
method: selectedNetwork,
currency: "eth",
payerAddress: primaryWallet.address,
},
});
}, [primaryWallet, selectedNetwork]);
```
# Item Selection
Source: https://docs.crossmint.com/payments/headless/guides/item-selection
Specify which items to purchase with Headless Checkout API
The Headless Checkout API allows you to specify which items your users can purchase using the `lineItems` property. This guide explains how to configure item selection for different asset types.
## JSON Item Selection Examples
```json collectionLocator theme={null}
{
"lineItems": [
{
"collectionLocator": "crossmint:_YOUR_COLLECTION_ID_", // Crossmint managed collection
// "collectionLocator": "crossmint:_YOUR_COLLECTION_ID_:_TEMPLATE_ID_", // With specific template
"callData": {
"totalPrice": "5.00",
"quantity": 1 // matches your contract's parameter name
}
}
]
}
// To use exact-in mode, add executionParameters: { maxSlippageBps: "500" } to the lineItem object. For custom contract params, add executionParameters: { customParam: "value" }.
```
```json collectionLocator - imported contract theme={null}
{
"lineItems": [
{
"collectionLocator": "ethereum:0x71c7656ec7ab88b098defb751b7401b5f6d897", // External collection registered in Crossmint Console
"callData": {
"totalPrice": "5.00",
"quantity": 1 // matches your contract's parameter name
// For external contracts, you might need additional parameters such as:
// "_amount": 1, // if your mint function uses _amount instead of quantity
// "tokenId": "123", // if your contract requires a specific tokenId
// "metadata": "ipfs://...", // if your contract accepts metadata
}
}
]
}
```
```json tokenLocator - EVM theme={null}
{
"lineItems": [
{
"tokenLocator": "ethereum:0x71c7656ec7ab88b098defb751b7401b5f6d897:1234", // ::
"callData": {
"totalPrice": "25.00" // Only totalPrice is required for token purchases
// For external EVM contracts, you might need additional parameters:
// "_amount": 1, // if your contract uses _amount instead of quantity
// "tokenId": "1234", // if your contract requires explicit tokenId
// "metadata": "ipfs://...", // if your contract accepts metadata
}
}
]
}
// To use exact-in mode, add executionParameters: { maxSlippageBps: "500" } to the lineItem object. For custom contract params, add executionParameters: { customParam: "value" }.
```
```json tokenLocator - Solana theme={null}
{
"lineItems": [
{
"tokenLocator": "solana:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", // :
"callData": {
"totalPrice": "25.00",
"buyerCreatorRoyaltyPercent": 100 // Required for Solana
}
}
]
}
// To use exact-in mode, add executionParameters: { maxSlippageBps: "500" } to the lineItem object. For custom contract params, add executionParameters: { customParam: "value" }.
```
## Complete API Request Examples
For those who want a full copy/pastable code example, extended versions of the JSON guides are provided below.
```javascript collectionLocator theme={null}
const options = {
method: "POST",
headers: { "x-api-key": "_YOUR_API_KEY_", "Content-Type": "application/json" },
body: JSON.stringify({
recipient: {
email: "buyer@example.com",
},
payment: {
method: "ethereum-sepolia",
currency: "eth",
},
lineItems: [
{
collectionLocator: "crossmint:_YOUR_COLLECTION_ID_", // Crossmint managed collection
// collectionLocator: "crossmint:_YOUR_COLLECTION_ID_:_TEMPLATE_ID_", // With specific template
callData: {
totalPrice: "5.00",
quantity: 1, // matches your contract's parameter name
},
}
],
}),
};
fetch("https://staging.crossmint.com/api/2022-06-09/orders", options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
```javascript tokenLocator - EVM theme={null}
const options = {
method: "POST",
headers: { "x-api-key": "_YOUR_API_KEY_", "Content-Type": "application/json" },
body: JSON.stringify({
recipient: {
email: "buyer@example.com",
},
payment: {
method: "ethereum-sepolia",
currency: "eth",
},
lineItems: [
{
tokenLocator: "ethereum:0x71c7656ec7ab88b098defb751b7401b5f6d897:1234", // ::
callData: {
totalPrice: "25.00", // Only totalPrice is required for token purchases
// For external EVM contracts, you might need additional parameters:
// _amount: 1, // if your contract uses _amount instead of quantity
// tokenId: "1234", // if your contract requires explicit tokenId
// metadata: "ipfs://...", // if your contract accepts metadata
},
}
],
}),
};
fetch("https://staging.crossmint.com/api/2022-06-09/orders", options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
```javascript tokenLocator - Solana theme={null}
const options = {
method: "POST",
headers: { "x-api-key": "_YOUR_API_KEY_", "Content-Type": "application/json" },
body: JSON.stringify({
recipient: {
email: "buyer@example.com",
},
payment: {
method: "solana",
currency: "sol",
},
lineItems: [
{
tokenLocator: "solana:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", // :
callData: {
totalPrice: "25.00",
buyerCreatorRoyaltyPercent: 100, // Required for Solana
},
}
],
}),
};
fetch("https://staging.crossmint.com/api/2022-06-09/orders", options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
```javascript collectionLocator - imported contract theme={null}
const options = {
method: "POST",
headers: { "x-api-key": "_YOUR_API_KEY_", "Content-Type": "application/json" },
body: JSON.stringify({
recipient: {
email: "buyer@example.com",
},
payment: {
method: "ethereum-sepolia",
currency: "eth",
},
lineItems: [
{
collectionLocator: "ethereum:0x71c7656ec7ab88b098defb751b7401b5f6d897", // External collection registered in Crossmint Console
callData: {
totalPrice: "5.00",
quantity: 1, // matches your contract's parameter name
// For external contracts, you might need additional parameters such as:
// _amount: 1, // if your mint function uses _amount instead of quantity
// tokenId: "123", // if your contract requires a specific tokenId
// metadata: "ipfs://...", // if your contract accepts metadata
},
}
],
}),
};
fetch("https://staging.crossmint.com/api/2022-06-09/orders", options)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((err) => console.error(err));
```
# Localization
Source: https://docs.crossmint.com/payments/headless/guides/localization
Customize the language of the email receipt for headless checkouts
## Supported Locales
| Variable | Possible Values | Description |
| - | - | - |
| `locale` | `en-US` `de-DE` `es-ES` `fr-FR` `it-IT` `ja-JP` `ko-KR` `pt-PT` `ru-RU` `th-TH` `tr-TR` `uk-UA` `vi-VN` `zh-CN` `zh-TW` `Klingon` | Language selected for the email receipt. |
* Only `usd` is supported for credit and debit card checkouts, so set `payment.currency` to `usd`
* Localization is not supported for stablecoins. While `payment.crypto.defaultCurrency` can be set to `usdc` or other supported tokens, localized stablecoins like `eurc` are not available.
To customize the language use the property `locale`. Here is an example:
```json {2,6} theme={null}
{
"locale": "es-ES", // Language for the receipt (e.g. Spanish customers)
"payment": {
"receiptEmail": "jsmith@example.com",
"method": "card",
"currency": "usd" // If collection is priced in another currency, the quote is returned in USD
},
"lineItems": [
{
"collectionLocator": "crossmint:76ced33d-fec8-4741-a69f-450f1ae09fa6",
"callData": {
"totalPrice": ""
}
}
]
}
```
When implementing a headless checkout, you will need to handle the display of localized content in your own UI based
on the locale you selected. The locale only affects the email receipt language, not the checkout interface built
by you.
# Multiple Line Items
Source: https://docs.crossmint.com/payments/headless/guides/multiple-line-items
How to handle orders with multiple NFTs at once
The Headless Checkout multiple line items feature is used to build more elaborate purchase experiences, which require multiple NFT purchases at once. Multiple line items is currently only supported for NFTs.
Currently, there is a maximum limit of 15 NFTs per order.
Example use cases include:
* A marketplace where a buyer can add multiple NFTs to a cart and make the purchase in a single transaction.
* Enabling the purchase of multiple distinct tokens from an ERC-1155 contract in a single transaction.
## Marketplace Sales
The following example requires that you have a custom `collectionId` that supports secondary sales provisioned for you by your Crossmint Customer Success Engineer. For more information on marketplace and launchpad support, check [this guide](/payments/advanced/marketplaces-and-launchpads).
If you need to contact the team about getting set up with secondary sales support [contact the team](https://www.crossmint.com/contact/sales).
### Multiple `lineItems` for Secondary Sales
To enable a multi item purchase, you simply need to pass an array of `lineItems` instead of a single object when creating an order.
The below examples demonstrate how to create a multiple line item order for secondary sales:
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/orders \
--header 'Content-Type: application/json' \
--header 'x-api-key: _YOUR_API_KEY_' \
--data '{
"recipient": {
"email": "testy@crossmint.com"
},
"locale": "en-US",
"payment": {
"receiptEmail": "testy@crossmint.com",
"method": "ethereum-sepolia",
"currency": "eth"
},
"lineItems": [
{
"collectionLocator": "crossmint:_YOUR_SECONDARY_SALES_COLLECTION_ID_",
"callData": {
"contractAddress": "0x___CONTRACT_ADDRESS_OF_TOKEN",
"tokenId": "234"
}
},
{
"collectionLocator": "crossmint:_YOUR_SECONDARY_SALES_COLLECTION_ID_",
"callData": {
"contractAddress": "0x___ANOTHER_TOKEN_CONTRACT",
"tokenId": "567"
}
}
]
}'
```
```javascript Node.js theme={null}
const options = {
method: 'POST',
headers: {'x-api-key': '_YOUR_API_KEY_', 'Content-Type': 'application/json'},
body: '{
"recipient": {
"email": "test@crossmint.com"
},
"locale": "en-US",
"payment": {
"receiptEmail": "test@crossmint.com",
"method": "ethereum-sepolia",
"currency": "eth",
"payerAddress": "0x1234_payer_address"
},
"lineItems": [
{
"collectionLocator": "crossmint:_YOUR_SECONDARY_SALES_COLLECTION_ID_",
"callData": {
"contractAddress": "0x___CONTRACT_ADDRESS_OF_TOKEN",
"tokenId": "234"
}
},
{
"collectionLocator": "crossmint:_YOUR_SECONDARY_SALES_COLLECTION_ID_",
"callData": {
"contractAddress": "0x___ANOTHER_TOKEN_CONTRACT",
"tokenId": "567"
}
}
]
}'
};
fetch('https://staging.crossmint.com/api/2022-06-09/orders', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
After this step you will collect payment from the buyer as outlined in the [Order Lifecycle](/payments/headless/guides/order-lifecycle/) section.
## Primary Sales of Multiple Items
If you have a custom ERC-721 contract that supports the minter being able to specify which token they want to mint
you could use the example(s) below to enable them to purchase multiple and specific tokens in the same transaction.
This is an uncommon pattern for ERC-721 contracts though.
In the following example, the use case is purchasing two unique tokens from an ERC-1155 contract. You can see that the `_id` passed in the `callData` of each `lineItem` is unique (1 and 2).
```bash cURL theme={null}
curl --request POST \
--url https://staging.crossmint.com/api/2022-06-09/orders \
--header 'Content-Type: application/json' \
--header 'x-api-key: _YOUR_API_KEY_' \
--data '{
"recipient": {
"email": "testy@crossmint.com"
},
"locale": "en-US",
"payment": {
"receiptEmail": "testy@crossmint.com",
"method": "ethereum-sepolia",
"currency": "eth"
},
"lineItems": [
{
"collectionLocator": "crossmint:_YOUR_COLLECTION_ID_",
"callData": {
"_id_": "1",
"quantity": "1"
}
},
{
"collectionLocator": "crossmint:_YOUR_COLLECTION_ID_",
"callData": {
"_id_": "2",
"quantity": "1"
}
},
]
}'
```
```javascript Node.js theme={null}
const options = {
method: 'POST',
headers: {'x-api-key': '_YOUR_API_KEY_', 'Content-Type': 'application/json'},
body: '{
"recipient":
{
"email": "testy@crossmint.com"
},
"locale": "en-US",
"payment":
{
"receiptEmail": "testy@crossmint.com",
"method": "ethereum-sepolia",
"currency": "eth",
"payerAddress": "0x1234_payer_address"
},
"lineItems": [
{
"collectionLocator": "crossmint:_YOUR_COLLECTION_ID_",
"callData": {
"_id_": "1",
"quantity": "1"
}
},
{
"collectionLocator": "crossmint:_YOUR_COLLECTION_ID_",
"callData": {
"_id_": "2",
"quantity": "1"
}
},
]
}'
};
fetch('https://staging.crossmint.com/api/2022-06-09/orders', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
```
As in the secondary sales example above, you'll need to handle the remaining steps of the order lifecycle in your UI - payment, delivery, etc.
## Error Handling
As outlined in the [Delivery Phase](/payments/headless/guides/order-lifecycle/delivery-phase#error-handling) of the order lifecycle guides, it is important to keep track of the delivery for each item passed when you created the order and report back to the user on the status of the order.
Each line item is attempted and processed independently. If one line item fails, others will still go through and be fulfilled. If any items are undeliverable, the buyer will be automatically refunded for that specific portion of their order.
For definitions of delivery and line item statuses, see the Status Codes page.
# Order Complete Phase
Source: https://docs.crossmint.com/payments/headless/guides/order-lifecycle/completed-phase
Completion phase of the order lifecycle
The final phase in the order lifecycle is order completion. This occurs once payment has been successful and the delivery phase is finished.
The `order.phase` property will be set to `completed` once all line items have reached a final state — whether each item individually succeeded or failed.
For reference on all statuses (quote, payment, delivery), see the [Status Codes](/payments/headless/guides/status-codes) page.
`order.phase === "completed"` does **not** mean every line item succeeded. The phase reaches `completed` once all
line items have reached a final state — whether `completed` or `failed`. This applies to **both** single-item and multi-item orders.
You must always check each line item's `delivery.status` individually.
### Checking Line Item Delivery Status
Once `order.phase` reaches `completed`, fetch the order via the GET order API and loop through the `lineItems` array to determine the outcome of each item. Each line item's `delivery.status` can be one of: `awaiting-payment`, `in-progress`, `completed`, or `failed`.
```tsx theme={null}
const apiUrl = "https://staging.crossmint.com/api/2022-06-09";
const res = await fetch(`${apiUrl}/orders/${orderId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.NEXT_PUBLIC_CROSSMINT_API_KEY,
authorization: clientSecret,
},
});
const order = await res.json();
if (order.phase === "completed") {
for (const item of order.lineItems) {
if (item.delivery.status === "failed") {
// Handle item failure
} else if (item.delivery.status === "completed") {
// Handle successful delivery
}
}
}
```
### Recommendations
Think through and implement the call to action that makes sense for your buyers.
#### Where can they access the purchased item?
If the token was minted to their Crossmint wallet you can provide a direct link and/or render a representation of it.
Even if they minted directly to an existing wallet you can provide a link to display the token in Crossmint. Use the following URL format to display NFTs within Crossmint:
`https://www.crossmint.com/user/collection/::`
For info on the values to use for `` refer to the [supported chains](/introduction/supported-chains) page.
#### What are the next steps?
Can they stake, trade, or utilize the purchase in some way? Tell them how!
#### Handling Order Failure
If there were any issues with delivery explain what happened and ensure they know that refunds are automatic. Anyone who purchases through Crossmint and has an issue with delivery can contact our support team for assistance.
# Pay with Card
Source: https://docs.crossmint.com/payments/headless/guides/order-lifecycle/credit-card-payment-phase
Payment acceptance phase of the order lifecycle for credit cards.
The order lifecycle can be summarized as follows:
Your application determines recipient info for the buyer. It can be an email wallet or wallet address. Create or
update an order with this info to proceed to next step.
The API response returned from create/update order call(s) will include a `clientSecret` and `order` object that your
application uses to render the Crossmint Embedded Component.
Use the `clientSecret` and `order.orderId` returned in the API response to render the Crossmint Embedded Component
for credit card checkout.
The buyer completes checkout via credit card.
Your application will poll the GET order status and update the UI as the order progresses to the next phase.
During the initial `quote` phase of the order the payment status will be `requires-quote`.
Once the quote phase is completed, the order enters the payment phase and will have the status `awaiting-payment`, which indicates that the order is ready to be paid.
For the complete, authoritative list of payment statuses and their meanings, see the [Status Codes](/payments/headless/guides/status-codes) page. |
### Render the Crossmint Embedded Component
When the order is ready to accept payment, the API response will include a `clientSecret` and `order` object. You can use these with the Crossmint Embedded Component to collect the user's credit card payment.
```json theme={null}
{
"clientSecret": "_YOUR_CLIENT_SECRET_",
"order": {
"orderId": "5ddc0090-7f63-4f6a-b68d-a91f8253b02e",
"phase": "payment",
"locale": "en-US",
"lineItems": [], // removed for brevity
"quote": {
"status": "valid",
"quotedAt": "_timestamp_",
"expiresAt": "_timestamp_",
"totalPrice": {
"amount": "0.5",
"currency": "usd"
}
},
"payment": {
"status": "awaiting-payment",
"method": "basis-theory",
"currency": "usd",
"preparation": {},
"receiptEmail": "test@example.com"
}
}
}
```
After creating an order, you'll need to render the Crossmint Embedded Component to collect payment information. This secure, embeddable UI component lets you accept payment methods, validates input, and handles errors:
```tsx theme={null}
import { CrossmintEmbeddedCheckout, CrossmintProvider } from "@crossmint/client-sdk-react-ui";
export function MyCheckoutPage({ clientSecret, order, recipientWalletAddress, receiptEmail }) {
return (
);
}
```
The `CrossmintEmbeddedCheckout` component is highly customizable. You can adjust colors, styling, layout, and behavior to match your brand. Learn more in our [UI customization guide](/payments/embedded/guides/ui-customization).
### Poll for Status Updates
### Handling Declined Payments
When a card payment is declined, the order returns to `awaiting-payment` in most cases and includes a `payment.failureReason` object with a normalized `code`, `category`, `retryPolicy`, and user-safe `message`. Render the `message` to the buyer and use `retryPolicy` to decide whether to offer a retry, prompt for a different card, or stop. See the [Payment Error Codes](/payments/api-reference/error-codes) reference for the full catalog.
### 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.
```json theme={null}
{
"order": {
"payment": {
"status": "completed",
"refunded": {
"amount": "1.00",
"currency": "usd"
}
}
}
}
```
The `payment.refunded` object includes the following fields:
* `amount`: The amount that was refunded
* `currency`: The currency of the refund
When you encounter this state, your application should:
1. Display an appropriate message to the user indicating that their payment was refunded
2. Prevent any further actions related to the order (such as delivery expectations)
3. 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.
# Pay with Crypto
Source: https://docs.crossmint.com/payments/headless/guides/order-lifecycle/crypto-payment-phase
Payment acceptance phase of the order lifecycle for pay with crypto
An overview of the process works as follows:
Your user selects the chain, token, and wallet they want to pay from via your application and you create or
update an existing order with this information.
The API response returned from create or update order will include a `serializedTransaction` that your
application uses to request payment from the user's wallet.
After your application initiates the payment request the user must confirm the transaction.
Your application will poll the GET order status and update the UI as the order progresses to the next phase.
During the initial `quote` phase of the order the payment status will be `requires-quote`.
Once the quote phase is completed, the order enters the payment phase.
For most orders the payment phase will begin with the status `awaiting-payment`, which indicates that the order is ready to be paid. It can also begin with `requires-crypto-payer-address` if the payer address is missing.
For the complete, authoritative list of payment statuses and their meanings, see the [Status Codes](/payments/headless/guides/status-codes) page. |
### Setting the Payer Address
The order must know the address that will be sending the crypto payment. This enables Crossmint's payment listeners to associate incoming transactions with the correct order. To update the order with the payer address, call the update API as demonstrated below:
`PATCH` `/api/2022-06-09/orders/`
```json theme={null}
{
"payment": {
"currency": "eth",
"method": "base-sepolia",
"payerAddress": "0x1234abcd…"
}
}
```
The `payerAddress` is the wallet the user will be sending the payment from. Note that you must send the entire payment object even if the currency and/or method values are not changing.
### Submitting the Payment
When you've fully prepared the order such that the payment status is `awaiting-payment` you'll have everything necessary to request the crypto payment from your user. The details will be returned in the `order.payment.preparation` property.
```json theme={null}
{
"orderId": "c167db0f-0cb9-4c59-80d3-aface6bcb338",
"phase": "payment",
"locale": "en-US",
"lineItems": [], // removed for brevity
"quote": // removed for brevity
"payment": {
"status": "awaiting-payment",
"method": "base-sepolia",
"currency": "eth",
"preparation": {
"chain": "base-sepolia",
"payerAddress": "0x6C3b3225759Cbda68F96378A9F0277B4374f9F06",
"serializedTransaction": "0x02f9015083014a34238489173700848917387582653d94a105c311fa72b8fb78c992ecbdb8b02ea5bd394d868644e0f88d81b9011e2d2d2d2d2d2d424547494e204d454d4f2d2d2d2d2d2d65794a68624763694f694a49557a49314e694973496e523563434936496b705856434a392e65794a756232356a5a534936496a4e6b5954673159324a6b4c546c6a4e4459744e4459774f4331694d44686a4c5746694e6d557a595746684e4468694d794973496d39795a4756795357526c626e52705a6d6c6c63694936496d4d784e6a646b596a426d4c54426a596a6b744e474d314f5330344d47517a4c57466d59574e6c4e6d4a6a596a4d7a4f434973496d6c68644349364d5463784f446b784d7a63774f48302e75574365534961563642504b6f6935724d7939394c2d4b56303256644d4d343442343934724c4352656f632d2d2d2d2d2d454e44204d454d4f2d2d2d2d2d2dc0"
}
}
}
```
The `order.payment.preparation` property contains details about the chain that Crossmint is expecting the payment to be received on, the payer address, and a `serializedTransaction` that you can use to open a payment request in the user's wallet. See examples of how to parse the response and request transaction confirmation from the user below:
```jsx theme={null}
import { parseTransaction } from "viem";
import { useSendTransaction } from "wagmi";
const { sendTransactionAsync } = useSendTransaction();
const signAndSendTransaction = async (serializedTransaction) => {
const txn = parseTransaction(serializedTransaction)
try {
const txId = await sendTransactionAsync(txn);
console.log("Transaction ID:", txId);
} catch (error) {
console.error("Error sending transaction:", error);
}
};
```
```jsx theme={null}
import bs58 from "bs58";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
const { connection } = useConnection();
const { sendTransaction } = useWallet();
const signAndSendTransaction = async (serializedTransaction) => {
const transaction = Transaction.from(bs58.decode(serializedTransaction));
try {
const txId = await sendTransaction(transaction, connection);
console.log("Transaction ID:", txId);
} catch (error) {
console.error("Error sending transaction:", error);
}
}
```
You should **never** alter the values in the parsed transaction object. Simply parse the transaction object as shown
in the example above. Changing any of these values may result in Crossmint not being able to validate the payment.
Calling the `signAndSendTransaction` function in the code snippet(s) above will open the user's wallet and enable them to confirm the crypto payment.
### Awaiting Payment Confirmation
We recommend a polling interval of about 2500ms and never below 500ms.
You can use client-side or server-side API keys to implement headless checkout. Check the code samples for the type
of API key you're using in your application.
```tsx theme={null}
const getOrderPaymentStatus = async () => {
const apiUrl = "https://staging.crossmint.com/api/2022-06-09";
try {
const res = await fetch(`${apiUrl}/orders/${order.orderId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.NEXT_PUBLIC_CROSSMINT_API_KEY,
authorization: clientSecret, // saved from response of create order call
},
});
const refreshedOrder = await res.json();
setOrder(refreshedOrder);
return refreshedOrder.payment.status;
} catch (e) {
console.error(e);
throw new Error("Failed to fetch order");
}
};
const pollPaymentStatus = async () => {
const intervalId = setInterval(async () => {
const status = await getOrderPaymentStatus();
console.log("payment status: ", status);
if (status === "completed") {
clearInterval(intervalId);
}
}, 2500);
// Set a timeout to stop polling after 60 seconds
setTimeout(() => {
clearInterval(intervalId);
console.log("Taking longer than expected...");
}, 60000);
};
```
The sample code below is from a NextJS application. The `component.tsx` file is simplified to only show the relevant
logic. The client-side component sends an API request to the application's backend, which then proxies the request
to Crossmint. This is because the example is using a server-side API key, which requires making requests from a server environment.
```tsx component.tsx (client-side) theme={null}
const getOrderPaymentStatus = async () => {
try {
const res = await fetch(`/orders/${order.orderId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const refreshedOrder = await res.json();
setOrder(refreshedOrder);
return refreshedOrder.payment.status;
} catch (e) {
console.error(e);
throw new Error("Failed to fetch order");
}
};
const pollPaymentStatus = async () => {
const intervalId = setInterval(async () => {
try {
const status = await getOrderPaymentStatus();
console.log("payment status: ", status);
if (status === "completed") {
clearInterval(intervalId);
}
} catch (e) {
clearInterval(intervalId);
console.error("Error polling payment status: ", e);
}
}, 2500);
// Set a timeout to stop polling after 60 seconds
setTimeout(() => {
clearInterval(intervalId);
console.log("Taking longer than expected...");
}, 60000);
};
```
```typescript route.ts (server-side) theme={null}
import { callCrossmintAPI } from "@/app/utils/crossmint";
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest, { params }: { params: { orderId: string } }) {
if (params.orderId) {
const order = await callCrossmintAPI(`/orders/${params.orderId}`, {
method: "GET",
});
return NextResponse.json(order, { status: 200 });
} else {
return NextResponse.json({ error: true, message: "Missing orderId" }, { status: 400 });
}
}
```
```typescript crossmint.ts theme={null}
const crossmintBaseUrl = process.env.CROSSMINT_API_URL;
const crossmintAPIHeaders = {
accept: "application/json",
"content-type": "application/json",
"x-api-key": process.env.CROSSMINT_API_KEY!,
};
const callCrossmintAPI = async (endpoint: string, options: { method: string; body?: any; params?: any }) => {
const url = `${crossmintBaseUrl}/${endpoint}`;
const { body, method } = options;
const response = await fetch(url, {
body: body ? JSON.stringify(body) : null,
method,
headers: crossmintAPIHeaders,
});
const json = await response.json();
return json;
};
export { callCrossmintAPI };
```
Once the payment is confirmed, you can move on to the delivery phase of the order lifecycle.
### 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.
```json theme={null}
{
"order": {
"payment": {
"status": "completed",
"refunded": {
"amount": "1.23",
"currency": "eth"
}
}
}
}
```
The `payment.refunded` object includes the following fields:
* `amount`: The amount that was refunded
* `currency`: The currency of the refund
When you encounter this state, your application should:
1. Display an appropriate message to the user indicating that their payment was refunded
2. Prevent any further actions related to the order (such as delivery expectations)
3. 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.
# Delivery Phase
Source: https://docs.crossmint.com/payments/headless/guides/order-lifecycle/delivery-phase
Token delivery phase of the order lifecycle
When the payment phase is completed, the user is guaranteed to receive the items ordered or an automatic refund for any unfulfilled items. Upon payment completion you'll want to display an animation in the UI indicating that the delivery is in-progress. Then poll the GET order API to check on delivery status and update your application UI when delivery status is updated.
You can use client-side or server-side API keys to implement headless checkout. Check the code samples for the type
of API key you're using in your application.
We recommend a polling interval of about 2500ms and never below 500ms.
```tsx theme={null}
const getOrderDeliveryStatus = async () => {
const apiUrl = "https://staging.crossmint.com/api/2022-06-09";
try {
const res = await fetch(`${apiUrl}/orders/${order.orderId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.NEXT_PUBLIC_CROSSMINT_API_KEY,
authorization: clientSecret, // saved from response of create order call
},
});
const refreshedOrder = await res.json();
setOrder(refreshedOrder);
return refreshedOrder.lineItems[0].delivery.status;
} catch (e) {
console.error(e);
throw new Error("Failed to fetch order");
}
};
const pollDeliveryStatus = async () => {
const intervalId = setInterval(async () => {
try {
const status = await getOrderDeliveryStatus();
console.log("delivery status: ", status);
if (status === "completed") {
clearInterval(intervalId);
}
} catch (e) {
clearInterval(intervalId);
console.error("Error polling delivery status: ", e);
}
}, 2500);
// Set a timeout to stop polling after 60 seconds
setTimeout(() => {
clearInterval(intervalId);
console.log("Taking longer than expected...");
}, 60000);
};
```
The sample code below is from a NextJS application. The `component.tsx` file is simplified to only show the relevant
logic. The client-side component sends an API request to the application's backend, which then proxies the request
to Crossmint. This is because the example is using a server-side API key, which requires making requests from a
server environment.
```tsx component.tsx (client-side) theme={null}
const getOrderDeliveryStatus = async () => {
try {
const res = await fetch(`/orders/${order.orderId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const refreshedOrder = await res.json();
setOrder(refreshedOrder);
return refreshedOrder.lineItems[0].delivery.status;
} catch (e) {
console.error(e);
throw new Error("Failed to fetch order");
}
};
const pollDeliveryStatus = async () => {
const intervalId = setInterval(async () => {
const status = await getOrderDeliveryStatus();
console.log("delivery status: ", status);
if (status === "completed") {
clearInterval(intervalId);
}
}, 2500);
// Set a timeout to stop polling after 60 seconds
setTimeout(() => {
clearInterval(intervalId);
console.log("Taking longer than expected...");
}, 60000);
};
```
```typescript route.ts (server-side) theme={null}
import { callCrossmintAPI } from "@/app/utils/crossmint";
import { NextRequest, NextResponse } from "next/server";
export async function GET(req: NextRequest, { params }: { params: { orderId: string } }) {
if (params.orderId) {
const order = await callCrossmintAPI(`/orders/${params.orderId}`, {
method: "GET",
});
return NextResponse.json(order, { status: 200 });
} else {
return NextResponse.json({ error: true, message: "Missing orderId" }, { status: 400 });
}
}
```
```typescript crossmint.ts theme={null}
const crossmintBaseUrl = process.env.CROSSMINT_API_URL;
const crossmintAPIHeaders = {
accept: "application/json",
"content-type": "application/json",
"x-api-key": process.env.CROSSMINT_API_KEY!,
};
const callCrossmintAPI = async (endpoint: string, options: { method: string; body?: any; params?: any }) => {
const url = `${crossmintBaseUrl}/${endpoint}`;
const { body, method } = options;
const response = await fetch(url, {
body: body ? JSON.stringify(body) : null,
method,
headers: crossmintAPIHeaders,
});
const json = await response.json();
return json;
};
export { callCrossmintAPI };
```
### Error Handling
Once the payment has been confirmed, delivery is likely to complete successfully. Before accepting the payment Crossmint simulates the expected transaction, which means that most delivery issues will be prevented before payment is even accepted. Given a successful payment the delivery process will be retried until it completes successfully or determines that delivery is impossible.
The most common cause for a delivery failure is that the item is no longer available. In the case of primary sales this could mean that the collection is sold out. For secondary sales the reason could be that someone else bought the item first.
Your application should be aware of the potential for delivery to fail and provide information to the buyer on what to expect. In the case of multiple items purchased and partial delivery failure you'll need to indicate which items were successfully delivered and which items failed.
Be sure to let your buyer know that in the case of a failed delivery they will be refunded automatically. If they
have any questions or issues they can reach out to Crossmint support directly for assistance.
A receipt will be sent to the buyer as long as their email was included in the order. There are two ways to pass this information. If the recipient is set to an email address, it will be used to send the receipt. In the case that the recipient is set to a `walletAddress` you can include an optional `receiptEmail` property within the payment object.
```json Crypto Payment theme={null}
{
"payment": {
"method": "base-sepolia",
"currency": "eth",
"payerAddress": "0xabcd1234...",
"receiptEmail": "user@example.com"
},
"recipient": {
"walletAddress": "0xabcd1234..."
}
}
```
```json Credit Card Payment theme={null}
{
"payment": {
"method": "card",
"receiptEmail": "user@example.com"
},
"recipient": {
"email": "user@example.com"
}
}
```
# Quote Phase
Source: https://docs.crossmint.com/payments/headless/guides/order-lifecycle/quote-phase
Initial phase of the order lifecycle
The first phase of the headless checkout process is the quote phase. During this phase, you construct an order that consists of the items being purchased, payment method, locale, and recipient. This will ultimately yield a quote to purchase it.
It's possible for an order to go from the payment phase back to the quote phase if the quote expires.
You can initialize an order such that the quote phase is completed in a single API call. Alternatively, you can create a basic order and iteratively update it with locale, recipient, and payment method information. The only information you cannot edit on a quote is the `lineItems` array, which indicate the details of what is being purchased.
If you need to alter the `lineItems` in an order you must create a new order.
## Quote status overview
During the quote phase, statuses indicate whether the order has enough information to proceed (like a recipient), and whether the quote is still valid. For the full, authoritative list of quote statuses, see the Status Codes page.
See the full list: [Status Codes](/payments/headless/guides/status-codes).
## Creating the Order
You will create the order via API call. The headless checkout offers support for both client-side or server-side API keys to enable you to build in a way that makes sense for your application.
Orders can be created using a client-side API key, but any subsequent fetches or updates will require passing the
`clientSecret` returned in the create-order API response as an `authorization` header. This guide is written with
the expectation of using a server-side API key.
For more information on using Crossmint APIs refer to [API keys](/introduction/platform/api-keys) documentation
page.
To create a minimal order, make a `POST` request to the `/api/2022-06-09/orders` endpoint (view [API reference](/api-reference/headless/create-order)). The required properties to create an order are: `payment` and `lineItems`.
```json Crypto Payment theme={null}
{
"payment": {
"method": "base-sepolia",
"currency": "eth",
"payerAddress": "0x1234abcd..." // optional to create order, but required to proceed to payment phase
},
"lineItems": [
{
"collectionLocator": "crossmint:_YOUR_COLLECTION_ID_",
"callData": {
"totalPrice": "0.0001"
}
}
]
}
```
```json Credit Card Payment theme={null}
{
"payment": {
"method": "card",
"receiptEmail": "your-email@example.com"
},
"lineItems": [
{
"collectionLocator": "crossmint:_YOUR_COLLECTION_ID_",
"callData": {
"totalPrice": "0.0001"
}
}
]
}
```
The `payment` object indicates the method and currency you intend to make the payment with. For crypto payments, this can be the same chain of the NFT or another chain that the buyer has liquidity on.
For example, buying an NFT on `BASE` chain with `BASE` `ETH`, buying an NFT on Solana with `SOL`, or any cross-chain combination of a [supported chain](/introduction/supported-chains) and currency.
The `payment.method` value for credit card checkouts should be `card` and the `payment.currency` must be `usd` (see [Supported Locales](/payments/headless/guides/localization#supported-locales)).
The `lineItems` array describes the NFTs being purchased.
#### Collection Locator
The `collectionLocator` within `lineItems` is how you specify the collection within Crossmint.
For primary sales where the NFTs are being minted for the first time, the contract will need to be registered in the Crossmint console.
You can find your `collectionId` within the Token collections tab in the developer console.
#### Token Locator
Alternatively, if this is a secondary sale, and your token is already minted and available on a supported marketplace, you can use a `tokenLocator`, instead of a `collectionLocator`.
***Using a token locator means you do not need to register the collection in the Crossmint console.***
The `tokenLocator` for EVM Chains follows the format `blockchain:contractAddress:tokenId` and for Solana follows the format `blockchain:tokenAddress`.
More information on the `tokenLocator` format can be found in the [marketplaces and launchpads guide](/payments/advanced/marketplaces-and-launchpads).
Remember, you can [view orders within the Developer
Console](/payments/advanced/testing-tips#reviewing-orders-in-the-developer-console) to help get insight into the
order process during testing and even after launch.
## Updating the Order
All orders require a recipient to progress to the payment phase. You can also optionally add a specific locale, which is used to indicate the language for the email receipt sent to the buyer.
The primary reason you'll update an existing order is when the buyer changes the payment method or currency they'd like to pay with in the UI that you build. To update the order, make a `PATCH` request to the `/api/2022-06-09/orders/` endpoint to make these changes and receive a response with updated payment details. View [API reference here](/api-reference/headless/edit-order).
You can update all of these fields at the same time or individually depending on what makes the most sense for your use case.
### Update the Recipient
First, take a look at how to add the recipient to an existing order. The `requires-recipient` state occurs when you initialize a minimal order and do not include a recipient value. Valid options for the recipient property are `email` or `walletAddress`.
When you set an email recipient, the token will be minted to a Crossmint custodial wallet that can be accessed via [www.crossmint.com](https://www.crossmint.com/) (or [staging.crossmint.com](https://staging.crossmint.com) during testing).
You can update the recipient for an existing order as follows:
`PATCH` `/api/2022-06-09/orders/`
```json theme={null}
{
"recipient": {
"email": "buyer@example.com"
}
}
```
OR
```json theme={null}
{
"recipient": {
"walletAddress": "0x1234abcd…"
}
}
```
The wallet address **must** be compatible with the chain the NFT is on. For example, a Solana NFT requires a Solana
wallet address and an NFT on an EVM chain requires a compatible EVM wallet address.
### Update Chain and/or Currency
To change details about how the buyer will pay for the NFTs you use the same API route as above and pass a new payment object.
```json Crypto Payment theme={null}
{
"payment": {
"method": "ethereum-sepolia",
"currency": "usdc",
"payerAddress": "0x1234abcd..."
}
}
```
```json Credit Card Payment theme={null}
{
"payment": {
"method": "card",
"receiptEmail": "your-email@example.com"
}
}
```
This will return a new response that can be used to prompt the buyer to complete the crypto payment.
Anytime you update the order you must ensure you use the newly returned `payment.preparation` property to proceed
with the payment process.
### Update the Locale
As mentioned above, the `locale` is used to set the language and currency for the email receipt sent to your buyer. The default is `en-US`.
Below is an example of the body you'd pass to update the locale setting:
```json theme={null}
{
"locale": "es-ES"
}
```
The available locale values are:
`en-US`, `de-DE`, `es-ES`, `fr-FR`, `it-IT`, `ja-JP`, `ko-KR`, `pt-PT`, `ru-RU` `th-TH` `tr-TR` `uk-UA` `vi-VN` `zh-CN` `zh-TW`, `Klingon`
## Quote Expiration
The time before expiration depends on the payment method chosen. Check the `quote.expiresAt` property to determine how long the quote is valid for. Additionally, the `quote.status` property will be set to `expired` if this timeframe is exceeded.
If your quote has expired you'll need to create a new order.
# Payment Methods
Source: https://docs.crossmint.com/payments/headless/guides/payment-methods
Understand the supported payment methods and how to implement them
## Cross-Chain + Crypto Payments
You can easily create or update an order to receive a price quote, enabling your user to pay with crypto they have on the same chain or even liquidity they have on other chains.
There are several considerations you need to keep in mind when building your app to support cross-chain payments. Some recommendations include the following:
* How to test the process out in staging/testnet and then move to production/mainnet
* Filtering down the list of tokens for which the connected wallet has enough balance to cover the purchase
* Ensuring the wallet is set to the same network as the selected payment chain
Refer to the [Order Lifecycle](/payments/headless/guides/order-lifecycle/quote-phase) docs for details about creating and updating orders based on user selections.
For a full list of supported crypto currencies, refer to the [supported
currencies](/payments/headless/guides/supported-currencies) page.
You'll need to build out a UI that enables your buyer to select from the supported currencies, and then update the existing order upon any changes.
Code example(s) coming soon!
## Credit Card Payments
The option of accepting credit card payments via headless checkout is also available. For detailed examples of creating and updating orders, refer to the [Order Lifecycle](/payments/headless/guides/order-lifecycle/quote-phase) docs.
### Test Price Limits and Test Credit Cards
When building your applications using the staging environment, you can use various test credit cards numbers to see the entire process end-to-end, without actually having to transact using a real credit card.
Check out the Testing Tips page for more info on [price limits](/payments/advanced/testing-tips#limits-in-staging) and [test card numbers](/payments/advanced/testing-tips#test-credit-card-numbers).
# Plan Your Solution
Source: https://docs.crossmint.com/payments/headless/guides/plan-your-solution
Detailed integration guide to build out a fully custom digital asset checkout experience
The Headless Checkout suite of APIs offers the most sophisticated tooling available to build fully custom experiences for your users. This guide will walk you through the entire integration process in a logical order. The first step is to take a few minutes to plan out your solution.
## Client or Server Approach
Depending on how your application is architected, you may want to use server-side or client-side API keys. Check out the [client or server guide](/payments/headless/guides/client-or-server) for more info.
## Decide on Payment Methods
The Headless Checkout supports cross-chain crypto payments and credit cards. Decide if you want to enable both credit card and crypto payments, and which cross-chain tokens should be available.
### Cross-Chain Payments
Enable your users to pay for digital assets directly with crypto or with liquidity they have available on other chains by leveraging the cross-chain payments support.
Check the [cross-chain payments](/payments/headless/guides/payment-methods#cross-chain-payments) section for more in-depth guidance.
### Credit Card Payments
You can also use headless checkout to initiate credit card orders for your buyers.
Check the [credit card payments](/payments/headless/guides/payment-methods#credit-card-payments) section for more in-depth guidance.
## Staging or Production
In most cases, it makes sense to start the integration process in the staging environment where all features are available and free to try. You can find more information about [Staging and Production environments here](/introduction/platform/staging-vs-production).
Headless checkout requires enablement by the Customer Success Engineering team in production. [Contact
Sales](https://www.crossmint.com/contact/sales) if your don't already have a CSE assigned.
## Create or Import Your Collection in the Console
For NFTs, you will need to have a collection that your users can purchase NFTs from. You can deploy a collection directly from the Crossmint Developer Console or import a contract you've deployed elsewhere. Check these guides for more details on creating or importing collections.
* [Create a Collection](/payments/guides/create-collection)
* [Import a Collection](/payments/guides/register-collection)
Once you have a collection created or imported, you'll have a `collectionId`, which is necessary to create orders.
## Create Your API Key
With the above steps completed, you're ready to create an API key to use for your project.
1. Login to the developer console where your collection was created in the previous step
2. If you're on the collection detail view, navigate back to the home page of the console
3. Click the "Integrate" tab and select the "API Keys" section on top
Before creating an API key, you'll need to decide on server-side vs client-side implementation. You can find some more info in the [Client or Server Guide](/payments/headless/guides/client-or-server).
## Next Steps
You're ready to move on to implementing the Headless Checkout in your application!
# Production Launch
Source: https://docs.crossmint.com/payments/headless/guides/production-launch
Follow this checklist to ensure you are ready for production launch with Headless Checkout
## From Staging to Production with Headless Checkout
***
### Launch Checklist
1. Change all environment URLs from `staging.crossmint.com/api/2022-06-09` to `www.crossmint.com/api/2022-06-09`.
2. Change your API keys to the production versions. *Ensure your production API key has the appropriate scopes enabled, such as `orders.create`, and that all authorized domains and mobile bundle identifiers are properly configured.*
3. Update your collection ID to the production one.
4. As needed, change your passed-in props to be production ready (e.g. email, `payment`, `lineItems`).
```javascript {2,6,20} Production theme={null}
// Using the REST API
fetch("https://www.crossmint.com/api/2022-06-09/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_PROD_API_KEY"
},
body: JSON.stringify({
"recipient": {
"email": "steve@gmail.com"
},
"locale": "en-US",
"payment": {
"receiptEmail": "steve@gmail.com",
"method": "ethereum",
"currency": "eth",
"payerAddress": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e"
},
"lineItems": [
{
"collectionLocator": "crossmint:YOUR_PROD_COLLECTION_ID",
"callData": {
"totalPrice": "0.25",
"quantity": 1
}
}
]
})
});
```
```javascript {2,6,20} Staging theme={null}
// Using the REST API
fetch("https://staging.crossmint.com/api/2022-06-09/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_STAGING_API_KEY"
},
body: JSON.stringify({
"recipient": {
"email": "test@crossmint.com"
},
"locale": "en-US",
"payment": {
"receiptEmail": "test@crossmint.com",
"method": "ethereum-sepolia",
"currency": "eth",
"payerAddress": "0x1234_payer_address"
},
"lineItems": [
{
"collectionLocator": "crossmint:YOUR_STAGING_COLLECTION_ID",
"callData": {
"totalPrice": "0.001",
"quantity": 1
}
}
]
})
});
```
# Specify Recipient
Source: https://docs.crossmint.com/payments/headless/guides/specify-recipient
Select where purchased items should be delivered in Headless Checkout
## Specifying the Recipient
For Headless Checkout, you specify the recipient when creating or updating an order.
### Recipient by Email
When specifying a recipient by email, Crossmint will automatically create a secure custodial wallet on the fly for that email address:
```json {4} theme={null}
POST /api/2022-06-09/orders
{
"recipient": {
"email": "user@example.com" // Email address of the recipient
},
"locale": "en-US",
"payment": {
"method": "ethereum-sepolia",
"currency": "eth",
"payerAddress": "0x1234abcd...",
"receiptEmail": "user@example.com"
},
"lineItems": [
{
"collectionLocator": "crossmint:cosdahdasda",
"callData": {
"totalPrice": "0.0001",
"quantity": 1
}
}
]
}
```
```json {4} theme={null}
PATCH /api/2022-06-09/orders/{orderId}
{
"recipient": {
"email": "user@example.com" // Email address of the recipient
}
}
```
The recipient will be able to access their purchased items by logging into their Crossmint wallet in [staging](https://staging.crossmint.com/user/collection) or [mainnet](https://www.crossmint.com/user/collection).
### Recipient by Wallet Address
To deliver items directly to a specific blockchain wallet address:
```json {4} EVM theme={null}
POST /api/2022-06-09/orders
{
"recipient": {
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678" // For EVM chains
},
"locale": "en-US",
"payment": {
"method": "ethereum-sepolia",
"currency": "eth",
"payerAddress": "0x1234abcd...",
"receiptEmail": "user@example.com" // Optional — recommended when using wallet address
},
"lineItems": [
{
"collectionLocator": "crossmint:_YOUR_EVM_COLLECTION_",
"callData": {
"totalPrice": "0.0001",
"quantity": 1
}
}
]
}
```
```json {4} Solana theme={null}
POST /api/2022-06-09/orders
{
"recipient": {
"walletAddress": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" // For Solana
},
"locale": "en-US",
"payment": {
"method": "solana",
"currency": "sol",
"payerAddress": "5FHne...",
"receiptEmail": "user@example.com" // Optional — recommended when using wallet address
},
"lineItems": [
{
"collectionLocator": "crossmint:_YOUR_SOLANA_COLLECTION_",
"callData": {
"totalPrice": "0.0001",
"quantity": 1
}
}
]
}
```
```json {4} EVM theme={null}
PATCH /api/2022-06-09/orders/{orderId}
{
"recipient": {
"walletAddress": "0x1234567890abcdef1234567890abcdef12345678" // For EVM chains
}
}
```
```json {4} Solana theme={null}
PATCH /api/2022-06-09/orders/{orderId}
{
"recipient": {
"walletAddress": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" // For Solana
}
}
```
If specifying a recipient by wallet address, ensure the address is valid for the chain your collection is on, which
may differ from the chain the payment is performed on.
# Status Codes
Source: https://docs.crossmint.com/payments/headless/guides/status-codes
Guide to understanding the status codes returned from the Headless Checkout APIs
Crossmint's headless checkout includes status codes for multiple aspects of the checkout process. Using status codes lets you build detailed user experiences and UIs that inform about the status of the payment process and delivery of their purchase.
There is the top level `order.phase` and then there are sub-statuses for `quote`, `payment`, and `delivery`.
## Quote statuses
* `item-unavailable` — No items in the order are available for purchase at the current time
* `valid` — The quote is valid
* `expired` — The quote has expired and needs to be refreshed
* `requires-recipient` — The order is missing recipient information (email or wallet address)
## Payment statuses
* `draft` — The order was created with `state: "draft"`. It is a quote preview only: not persisted, and it cannot be polled or paid
* `requires-quote` — The order is still in the quote phase
* `requires-crypto-payer-address` — A crypto wallet address needs to be provided for the payment
* `requires-email` — An email is required to proceed (typically for card payments or receipts)
* `requires-kyc` — The buyer must complete KYC verification to proceed
* `manual-kyc` — KYC requires manual review; the buyer will be notified of the outcome
* `failed-kyc` — KYC verification failed and the buyer cannot proceed
* `crypto-payer-insufficient-funds` — The specified payer address cannot cover the purchase
* `crypto-payer-insufficient-funds-for-gas` — The payer address cannot cover the required network fees
* `awaiting-payment` — Ready to submit the payment
* `in-progress` — The payment is underway
* `completed` — Payment completed; the order proceeds to delivery/completion
Payment statuses describe where the payment is in its lifecycle. When a card payment is declined, the order also
carries a `payment.failureReason` object with a normalized error code, category, and retry policy. Those are
documented separately in the [Payment Error Codes](/payments/api-reference/error-codes) reference.
## Delivery statuses
* `draft` — The order is a draft (`state: "draft"`); nothing will be delivered until the order is created for real
* `awaiting-payment` — Delivery not started because payment is not yet complete
* `in-progress` — Delivery is being processed
* `failed` — Delivery failed; affected items will be refunded automatically
* `completed` — Delivery completed successfully
# Supporting Your Customers
Source: https://docs.crossmint.com/payments/headless/guides/supporting-your-customers
Support your customers facing issues with the checkout process
As an enterprise working with Crossmint, you receive priority customer support. Consequently, customer issues can be escalated to the Crossmint team.
## Information to pass onto Crossmint
When escalating customer issues to the Crossmint team, please ensure to pass on the following details, as it makes troubleshooting easier:
* **Email Address**: The email address they are using to purchase the digital asset.
* **Website URL**: The URL of the website they are trying to purchase the digital asset from.
* **Order Id**: The Order Id of the initiated payment.
* **Collection Id**: The Collection Id of the collection on Crossmint.
* **Project Id**: The Project Id of the project on Crossmint.
* **Payment error code**: The `payment.failureReason.code` on the order, if the payment was declined. See the [Payment Error Codes](/payments/api-reference/error-codes) reference for what each code means and the recommended next step.
## Additional items to check
* You can review the **Orders tab** to check the status of the checkout.
* If you are the dev reporting the error, please share the sample code as well.
If nothing works and you are unable to troubleshoot the issue, you can redirect them to Crossmint's support
[here](https://help.crossmint.com/hc/en-us/requests/new/?utm_source=docs).
# Overview
Source: https://docs.crossmint.com/payments/headless/overview
Create fully custom checkout experiences
For general information about Crossmint's Payments product, see the [introduction](/payments/introduction). This guide will focus on the features specific to the headless checkout.
## When is the Headless Checkout the best fit?
* **You want to build the entire user experience**
* **You want to use Crossmint checkout outside of a browser environment** (e.g. native mobile app, game, VR headset, etc.)
* **You are okay spending more time developing your own UX**
## Introduction
Crossmint's headless checkout API is a set of REST APIs that allow you to integrate credit card and cross-chain crypto payments inside your app with full control of the user experience.
It is the most customizable version of the Crossmint suite of payments products, which allows anyone to buy digital assets with familiar payment methods (credit card, crypto on any chain independent on where the asset lives) with the same backend, but allowing full UI customizability and deep integration with your app's UX.
Wherever your imagination takes you...
## Get Started
Build a custom Pay with Crypto checkout experience for your digital assets
Build a custom credit-card checkout experience
Build a USDC checkout in 5 minutes (great for AI agents)
Sell memecoins quickly with a credit card checkout
Contact our sales team for advanced support.
## Advanced Topics
# Pay with Card - Memecoins
Source: https://docs.crossmint.com/payments/headless/quickstarts/credit-card-memecoin-cko
Create a fully customized memecoin checkout experience that accepts credit cards
## Introduction
This guide will show you how to accept credit card payments using Crossmint's Headless Checkout API for memecoin sales. You'll learn how to:
* Set up credit card payments for Solana memecoin purchases in JavaScript
* Implement a checkout UI
* Track order status and delivery
For a faster, embedded checkout solution with minimal setup time, see our [embedded memecoin
quickstart](/payments/embedded/quickstarts/credit-card-memecoin).
## Headless Memecoin Checkout
The headless checkout API allows complete control over your checkout experience, including:
* Custom UI components and styling
* Custom checkout 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](/api-reference/headless/create-order).
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 the Crossmint sales team — production Checkout requires a signed Order Form and successful KYB. See the [production launch guide](/payments/headless/guides/production-launch) for the full sequence.
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.
```javascript Node.js theme={null}
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: "card",
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));
```
```json JSON theme={null}
{
"lineItems": [
{
"tokenLocator": "solana:tokenAddress",
"executionParameters": {
"mode": "exact-in",
"amount": "1",
"maxSlippageBps": "500"
}
}
],
"payment": {
"method": "card",
"receiptEmail": "receiptEmail"
},
"recipient": {
"walletAddress": "deliveryAddress"
}
}
```
For more details on tokenLocator formatting and other item selection options, see the [item selection page](/payments/headless/guides/item-selection#json-item-selection-examples).
```json theme={null}
{
"clientSecret": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmRlcklkZW50aWZpZXIiOiJlZDM0YTU3OS03ZmJjLTQ1MDktYjhkOC05ZTYxOTU0Y2Q1NTUiLCJpYXQiOjE3Mzk0MDI3NjEsImV4cCI6MTczOTQ4OTE2MX0.8AU0Y31lJhnQD2-vAXEZp3ZeMSyh_Wdm9An02Z5AW0M",
"order": {
"orderId": "ed34a579-7fbc-4509-b8d8-9e61954cd555",
"phase": "payment",
"locale": "en-US",
"lineItems": [
{
"chain": "solana",
"metadata": {
"name": "xmeme",
"description": "Token xmeme from the contract: 7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu",
"imageUrl": "https://arweave.net/VQrPjACwnQRmxdKBTqNwPiyo65x7LAT773t8Kd7YBzw"
},
"quote": {
"status": "valid",
"charges": {
"unit": {
"amount": "35.73",
"currency": "usd"
}
},
"totalPrice": {
"amount": "1",
"currency": "usd"
},
"quantityRange": {
"lowerBound": "0.0265905",
"upperBound": "0.0293895"
}
},
"delivery": {
"status": "awaiting-payment",
"recipient": {
"locator": "solana:BuWmGweapdysxU5VuUdi1RGoc4ibDG7TNirWjRtqF995",
"walletAddress": "your-solana-wallet-address"
}
},
"executionMode": "exact-in",
"maxSlippageBps": "500",
"executionParams": {
"mintHash": "7EivYFyNfgGj8xbUymR7J4LuxUHLKRzpLaERHLvi7Dgu",
"mode": "exact-in",
"amount": "1",
"maxSlippageBps": "500"
}
}
],
"quote": {
"status": "valid",
"quotedAt": "2025-02-12T23:26:00.397Z",
"expiresAt": "2025-02-12T23:26:30.397Z",
"totalPrice": {
"amount": "1",
"currency": "usd"
}
},
"payment": {
"status": "awaiting-payment",
"method": "basis-theory",
"currency": "usd",
"preparation": {},
"receiptEmail": "test@example.com"
}
}
}
```
Note the following parameters in the request body:
* `maxSlippageBps`: Optional, or else default autogenerated slippage will be applied
* `receiptEmail`: Optional. If the buyer pays with Apple Pay or Google Pay, Crossmint obtains the email automatically. Providing it improves fraud signal quality.
* `executionParameters.mode`: The execution method for the order. "exact-out" is for NFTs, "exact-in" is for fungible tokens
### Render the Crossmint Embedded Component
After creating an order, you'll need to render the Crossmint Embedded Component to collect payment information. This secure, embeddable UI component lets you accept payment methods, validates input, and handles errors:
```javascript theme={null}
import { CrossmintEmbeddedCheckout, CrossmintProvider } from "@crossmint/client-sdk-react-ui";
export function MyCheckoutPage({ clientSecret, order, recipientWalletAddress, receiptEmail }) {
return (
);
}
```
The `CrossmintEmbeddedCheckout` component is highly customizable. You can adjust colors, styling, layout, and behavior to match your brand. Learn more in our [UI customization guide](/payments/embedded/guides/ui-customization).
### Poll for Status Updates
After making the payment, you'll need to poll the [Get Order API](/api-reference/headless/get-order) to check on the delivery status and present this information to your user.
**Endpoint:** `GET` `https://staging.crossmint.com/api/2022-06-09/orders/`
Refer to the complete **get order** API reference [here](/api-reference/headless/get-order).
**Example Response:**
```json theme={null}
{
"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.
```json theme={null}
{
"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](#next-steps) section below to learn how to customize your integration.
## Next Steps
Learn how to design your headless checkout experience
Implement webhook handling for order updates
## Refreshing Orders
For memecoin checkout, the order expiration is 1 minute. The Crossmint Embedded Checkout will automatically refresh the order for you when the time comes. Alternatively, you can either create a new order or use the refresh quote API:
```javascript theme={null}
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;
}
}
```
# Pay with Card - NFTs
Source: https://docs.crossmint.com/payments/headless/quickstarts/credit-card-nft
Create a fully customized checkout experience with credit cards.
## Introduction
In this quickstart you'll learn how to accept credit card payments (including Apple and Google Pay) using Crossmint's headless APIs to purchase an NFT on the `polygon-amoy` testnet
## Prerequisites
## Integration Steps
### 1. Create an Order
```json theme={null}
{
"payment": {
"method": "card",
},
"lineItems": [
{
"collectionLocator": "crossmint:", // Use the collectionId you created in the Prerequisites section
"callData": {
"totalPrice": "0.0001"
}
}
]
}
```
```json theme={null}
{
"clientSecret": "_removed_",
"order": {
"orderId": "cc40e6af-f8dc-4ac2-8e26-d91a32f271cf",
"phase": "quote",
"locale": "en-US",
"lineItems": [
{
"chain": "polygon-amoy",
"quantity": 1,
"callData": {
"quantity": 1
},
"metadata": {
"name": "Headless Demo",
"description": "NFT Checkout Demo",
"imageUrl": "https://utfs.io/f/cd8545b7-3410-4fcd-988d-cd11951ed53c-g287ok.png"
},
"quote": {
"status": "requires-recipient",
"charges": {
"unit": {
"amount": "0.000169004",
"currency": "eth"
}
},
"totalPrice": {
"amount": "0.000169004",
"currency": "eth"
}
},
"delivery": {
"status": "awaiting-payment"
}
}
],
"quote": {
"status": "requires-recipient",
"quotedAt": "_DATE_",
"expiresAt": "_DATE_",
"totalPrice": {
"amount": "0.000169004",
"currency": "eth"
}
},
"payment": {
"status": "requires-quote",
"method": "basis-theory",
"preparation": {},
}
}
}
```
### 2. Update the Order with Recipient
Legally, for card payments, **an email *must* be specified** in order to deliver a receipt to your user, either in
`recipient.email` or in `payment.receiptEmail`. If one is not specified, the API will not return the fields
necessary to collect the user's payment.
```json theme={null}
{
"recipient": {
"email": "test@example.com"
}
}
```
```json theme={null}
{
"recipient": {
"walletAddress": "0xabcd1234..."
},
"payment": {
"method": "card", // When updating the payment, you must pass the complete object
"receiptEmail": "test@example.com"
}
}
```
### 3. Render the Crossmint Embedded Component
After creating an order, you'll need to render the Crossmint Embedded Component to collect payment information. This secure, embeddable UI component lets you accept payment methods, validates input, and handles errors:
```tsx theme={null}
import { CrossmintEmbeddedCheckout, CrossmintProvider } from "@crossmint/client-sdk-react-ui";
export function MyCheckoutPage({ clientSecret, order, recipientWalletAddress, receiptEmail }) {
return (
);
}
```
The `CrossmintEmbeddedCheckout` component is highly customizable. You can adjust colors, styling, layout, and behavior to match your brand. Learn more in our [UI customization guide](/payments/embedded/guides/ui-customization).
### 4. Poll for Status Updates
## Next Steps
# Pay with Other Crypto
Source: https://docs.crossmint.com/payments/headless/quickstarts/crypto
Create a fully customized checkout experience with crypto payments.
## Introduction
In this quickstart you'll learn how to accept multiple different cryptocurrencies as payment using Crossmint's headless APIs, to purchase an NFT on the `polygon-amoy` testnet.
## Prerequisites
If you are choosing to do this quickstart in your own console or IDE, consider you will need to have a way to prompt and sign crypto payments, like using Metamask.
### External Prerequisites
* Your crypto wallet of choice - Metamask, Coinbase Wallet, etc.
* A small balance of testnet currency in whichever cryptocurrency you would like to pay with
* [Ethereum, Base, Polygon](https://www.alchemy.com/faucets) - note that some of these faucets require a minimum of 0.1 ETH on mainnet, to prevent abuse
* [Solana devnet](https://solana.com/developers/guides/getstarted/solana-token-airdrop-and-faucets)
## Integration Steps
This quickstart will primarily focus on the API calls necessary to implement headless checkout in your project. You will also need to build out the frontend experience for your users.
### 1. Create an Order
```json theme={null}
{
"payment": {
"method": "base-sepolia",
"currency": "eth",
"payerAddress": "0xabcd1234..."
},
"lineItems": [
{
"collectionLocator": "crossmint:",
"callData": {
"totalPrice": "0.0001"
}
}
]
}
```
```json theme={null}
{
"payment": {
"method": "base-sepolia",
"currency": "usdc" | "degen" | "brett" | ... ,
"payerAddress": "0xabcd1234..."
},
"lineItems": [
{
"collectionLocator": "crossmint:",
"callData": {
"totalPrice": "0.0001"
}
}
]
}
```
```json theme={null}
{
"payment": {
"method": "solana",
"currency": "sol",
"payerAddress": "7NxpS1cMZ8..."
},
"lineItems": [
{
"collectionLocator": "crossmint:",
"callData": {
"totalPrice": "0.0001"
}
}
]
}
```
```json theme={null}
{
"payment": {
"method": "solana",
"currency": "usdc" | "bonk" | ... ,
"payerAddress": "7NxpS1cMZ8..."
},
"lineItems": [
{
"collectionLocator": "crossmint:",
"callData": {
"totalPrice": "0.0001"
}
}
]
}
```
```json theme={null}
{
"payment": {
"method": "polygon-amoy",
"currency": "matic",
"payerAddress": "0xabcd1234..."
},
"lineItems": [
{
"collectionLocator": "crossmint:",
"callData": {
"totalPrice": "0.0001"
}
}
]
}
```
```json theme={null}
{
"clientSecret": "_removed_",
"order": {
"orderId": "cc40e6af-f8dc-4ac2-8e26-d91a32f271cf",
"phase": "quote",
"locale": "en-US",
"lineItems": [
{
"chain": "polygon-amoy",
"quantity": 1,
"callData": {
"quantity": 1
},
"metadata": {
"name": "Headless Demo",
"description": "NFT Checkout Demo",
"imageUrl": "https://utfs.io/f/cd8545b7-3410-4fcd-988d-cd11951ed53c-g287ok.png"
},
"quote": {
"status": "requires-recipient",
"charges": {
"unit": {
"amount": "0.000169004",
"currency": "eth"
}
},
"totalPrice": {
"amount": "0.000169004",
"currency": "eth"
}
},
"delivery": {
"status": "awaiting-payment"
}
}
],
"quote": {
"status": "requires-recipient",
"quotedAt": "_DATE_",
"expiresAt": "_DATE_",
"totalPrice": {
"amount": "0.000169004",
"currency": "eth"
}
},
"payment": {
"status": "requires-quote"
}
}
}
```
In the requests above the `payerAddress` has been included, however if your user has not yet connected their wallet
you may omit this field, allow them to connect their wallet after creating the order, and submit the `payerAddress`
at a later time.
### 2. Update the Order with Recipient
```json theme={null}
{
"recipient": {
"email": "test@example.com"
}
}
```
```json theme={null}
{
"recipient": {
"walletAddress": "0xabcd1234..."
}
}
```
Notice you only need to pass the object that your are updating, in this case, `recipient`. You can also update
multiple properties in the same call. Available properties for update include `recipient`, `locale`, and the
`payment` object.
If specifying a recipient by wallet address, ensure the address is valid for the chain your **collection** is on,
which may differ from the chain the **payment is being performed** on.
```json theme={null}
{
"orderId": "cc40e6af-f8dc-4ac2-8e26-d91a32f271cf",
"phase": "payment",
"locale": "en-US",
"lineItems": [
{
"chain": "polygon-amoy",
"quantity": 1,
"callData": {
"quantity": 1
},
"metadata": {
"name": "Headless Demo",
"description": "NFT Checkout Demo",
"imageUrl": "https://utfs.io/f/cd8545b7-3410-4fcd-988d-cd11951ed53c-g287ok.png"
},
"quote": {
"status": "valid",
"charges": {
"unit": {
"amount": "0.0001681558",
"currency": "eth"
}
},
"totalPrice": {
"amount": "0.0001681558",
"currency": "eth"
}
},
"delivery": {
"status": "awaiting-payment"
}
}
],
"payment": {
"status": "awaiting-payment",
"preparation": {
"method": "crypto",
"chain": "base-sepolia",
"payerAddress": "0x6C3b3225759Cbda68F96378A9F0277B4374f9F06",
"serializedTransaction": "0x02f9012683aa36a744848917370085080992ce6c82629c94a105c311fa72b8fb78c992ecbdb8b02ea5bd394d869a9d359ca000b8f465794a68624763694f694a49557a49314e694973496e523563434936496b705856434a392e65794a756232356a5a534936496a4d3559544e6d4d444d334c544d7a4e6d51744e4441305a5331684d6a45774c575268593249335957526b5a4456694e794973496d39795a4756795357526c626e52705a6d6c6c63694936496d4e6a4e44426c4e6d466d4c5759345a474d744e47466a4d6930345a5449324c5751354d57457a4d6d59794e7a466a5a694973496d6c68644349364d5463784e546b334f54557a4e33302e662d4949646c653375314c58634c7845765438754e336d74786370764a686f31384a5a4b76483768456659c0"
}
}
}
```
Notice at the bottom, the new `serializedTransaction` property. This is a unique identifier let's Crossmint know exactly which order you are updating.
Anytime you update an order you ***must*** use the returned `serializedTransaction` property.
### 3. Show Purchase Preview to User
Now, after you've told Crossmint systems what you want, and you have passed a valid place to send the NFT, Crossmint will return a quote with prices for the desired items
Within the returned response, you'll find the `lineItems` array which contains the metadata and a quote for each line item. Within a quote, a detailed breakdown of its charges can be found.
If you are developing in an IDE or in terminal, the returned `lineitems` may be stuck in an \[Object], or be in a
hard-to-understand object format. You can make this human-readable for yourself by adding `JSON.stringify(response)`
```jsx theme={null}
```
```json theme={null}
{
// some properties removed for brevity
"order": {
"lineItems": [
{
"chain": "polygon-amoy",
"quantity": 1,
"callData": {
"quantity": 1
},
"metadata": {
"name": "Headless Demo",
"description": "NFT Checkout Demo",
"imageUrl": "https://utfs.io/f/cd8545b7-3410-4fcd-988d-cd11951ed53c-g287ok.png"
},
"quote": {
"status": "valid",
"charges": {
"unit": {
"amount": "0.000169004",
"currency": "eth"
}
},
"totalPrice": {
"amount": "0.000169004",
"currency": "eth"
}
},
"delivery": {
"status": "awaiting-payment"
}
}
]
}
}
```
### 4. Request Payment from User
You now need to request crypto payment from the user.
You will use the `serializedTransaction` returned in the purchase preview from the previous step. The `serializedTransaction` needs to be structured into a transaction object, which can then be passed to a crypto payment prompter, like viem's `sendTransaction` or wagmi's `sendTransactionAsync`.
Whichever method you use, it should open the user's default wallet extension in the browser and allow the user to complete the cross-chain payment.
If you've followed along this far you can simply paste the `serializedTransaction` property returned in your
previous API call into the app below. This will allow you to sign and send the transaction.
Ensure your wallet is connected to the **devnet** network, otherwise your funds may be lost!
```tsx theme={null}
"use client";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { useState } from "react";
import { parseTransaction } from "viem"; // Refer to their documentation details to make viem work for your needs
import { useSendTransaction } from "wagmi"; // Refer to their documentation details to make wagmi work for your needs
export default function Home() {
const { data: hash, isPending, sendTransactionAsync } = useSendTransaction();
const signAndSendTransaction = async () => {
const serializedTxn = "0x_your_serialized_transaction_response";
const txn = parseTransaction(serializedTxn || "0x");
await sendTransactionAsync({
to: txn.to as `0x${string}`,
value: BigInt(txn.value ? txn.value.toString() : "0"),
data: txn.data as `0x${string}`,
chainId: txn.chainId,
});
};
return ;
}
```
For a deeper example, refer to the repository linked below, which is the exact code running in the App tab.
Refer to the full repo for complete code examples
### 5. Poll for Status Updates
## Next Steps
# Pay with USDC
Source: https://docs.crossmint.com/payments/headless/quickstarts/paying-usdc
Buy or sell any tokenized asset with USDC, in an API call
## Introduction
This guide will show you how to quickly buy or sell any tokenized asset (NFT) with USDC, using Crossmint's headless APIs.
## Prerequisites
## External prerequisites
Crypto wallet supporting USDC - [Metamask](https://metamask.io/), [Phantom](https://phantom.app/), [Coinbase Wallet](https://wallet.coinbase.com), etc.
A small balance of **USDC** testnet currency in your wallet on the **Base-sepolia** network.
Get testnet USDC here:
* [Crossmint USDC Faucet (Base-sepolia)](https://usdc-faucet.vercel.app/)
The above faucet provides both USDC and USDXM. This quickstart uses USDC by default, but you can also use USDXM (see contract address below).
USDXM is a test token that represents USDC. It has the same behavior as USDC. In production, you'll use real USDC instead.
**Adding USDC to MetaMask:**
* Import the token to MetaMask:
1. Open MetaMask and click "Import tokens"
2. For USDC: Use the official USDC contract address from Circle `0x036CbD53842c5426634e7929541eC2318f3dCF7e`
3. For USDXM: Use the USDXM contract address `0x14196F08a4Fa0B66B7331bC40dd6bCd8A1dEeA9F`
4. Click Import
5. Refresh your wallet
## Create an order and pay with USDC
Clone the quickstart repo:
```bash theme={null}
git clone https://github.com/Crossmint/headless-pay-usdc.git
```
Navigate to the project directory:
```bash theme={null}
cd headless-pay-usdc
```
Install the dependencies:
```bash theme={null}
pnpm i
```
Set the following keys in the `.env` file:
```bash theme={null}
CROSSMINT_API_KEY=""
COLLECTION_ID=""
PAYER_ADDRESS=""
EMAIL_ADDRESS=""
```
* `CROSSMINT_API_KEY`, `COLLECTION_ID` are found from the [Developer Console](https://staging.crossmint.com/console/). See [Prerequisites](#prerequisites)
* `PAYER_ADDRESS` is your wallet address that contains USDC.
* `EMAIL_ADDRESS` is where you'll receive your NFT in a new Crossmint wallet that you can access later.
Run the application:
```bash theme={null}
pnpm dev
```
This will:
1. Create an order that is waiting for a **USDC** payment on the **Base-sepolia** network
2. Return the **serializedTransaction**, which will be used in the next step
3. Poll the status of the order until it is complete
1. Copy the `serializedTransaction` returned in the previous step and paste it below
2. Connect to your USDC wallet that was specified in the `.env` file
3. Send the transaction
The code will poll for the status of the order until it is marked as complete.
You should see it changing from "payment" to "delivery" and finally to "completed".
You will also see the final order details, including the transaction hash, in your terminal.
## Access your NFT
Once the order is complete, you will receive an email with a link to the purchased item, which can be viewed in the Crossmint website.
You can also access your NFT by logging in to your Crossmint wallet at
[crossmint.com](https://staging.crossmint.com/user/collection) with the email address you specified in the `.env`
file.
## Understanding the code
### Creating an order
The order creation process involves sending a POST request to Crossmint's API with the necessary payment and recipient details. Let's break down the key components:
```ts src/createOrder.ts theme={null}
import { API_KEY, API_URL, COLLECTION_ID, EMAIL_ADDRESS, PAYER_ADDRESS } from "./utils";
export async function createOrder() {
const payload = {
recipient: {
email: EMAIL_ADDRESS, // Replace with actual recipient wallet address
},
payment: {
method: "base-sepolia",
currency: "usdc",
payerAddress: PAYER_ADDRESS,
},
lineItems: [
{
collectionLocator: `crossmint:${COLLECTION_ID}`
},
],
};
...
}
```
#### Key parameters explained
* **recipient**: Specifies where to deliver the tokenized asset
* `email`: The email address where the tokenized asset will be delivered in a Crossmint wallet.
Instead of an email, a `walletAddress` field can be used to deliver the asset to a specific wallet address.
* **payment**: Defines how the payment will be made
* `method`: The blockchain network (`base-sepolia` for testnet)
* `currency`: The payment currency (`usdc`)
* `payerAddress`: The wallet address that will send the USDC payment
* **lineItems**: Specifies what is being purchased
* `collectionLocator`: Identifies the collection
#### API request
```ts src/createOrder.ts theme={null}
const response = await fetch(`${API_URL}/api/2022-06-09/orders`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY,
},
body: JSON.stringify(payload),
});
```
The API request:
* Sends a POST request to Crossmint's order endpoint
* Includes your API key for authentication
* Sends the order data as JSON in the request body
#### Response handling
```ts src/createOrder.ts theme={null}
if (!response.ok) {
throw new Error(`Failed to create order: ${response.status} ${response.statusText}`);
}
const jsonResponse = await response.json();
return {
"clientSecret": jsonResponse["clientSecret"], // Used for order authentication
"order": jsonResponse["order"] // Contains the order details
}
```
The response includes:
* `clientSecret`: A unique token used for authenticating subsequent requests for this order
* `order`: The order details including the order ID and status
For more details on creating orders via the headless API, see the [Create Order API Reference](/api-reference/headless/create-order).
### Polling an order
The order status polling process involves periodically checking the order's status until it's completed. Let's examine how this works:
```ts src/getOrder.ts theme={null}
import { API_KEY, API_URL } from "./utils";
export async function getOrder(orderId: string) {
const response = await fetch(`${API_URL}/api/2022-06-09/orders/${orderId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY, // API authentication
}
});
if (!response.ok) {
throw new Error(`Failed to create order: ${response.status} ${response.statusText}`);
}
return response.json();
}
```
#### Get order function explained
* Takes `orderId` as parameter
* Makes a GET request to fetch the current state of the order
* Uses API key for authentication
* Returns the order details as JSON
```ts src/getOrder.ts theme={null}
export async function pollOrder(orderId: string) {
while (true) {
const order = await getOrder(orderId);
console.log(`Current order status: \x1b[33m${order.phase}\x1b[0m`);
if (order.phase === "completed") {
return order;
}
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
```
#### Poll order function explained
* Continuously checks the order status every 3 seconds.
* Prints the current phase of the order
* Possible order phases include:
* `payment`: Waiting for the buyer's payment
* `delivery`: NFT is being delivered
* `completed`: Order is successfully completed
* Returns the final order details when completed
The 3-second polling interval helps prevent rate limiting while still providing timely updates. Adjust this value based on your needs.
For more details on polling for order status, see the [Get Order API Reference](/api-reference/headless/get-order).
Always implement proper error handling, retry mechanisms, and asynchronous polling in production environments to
handle network issues or API interruptions.
# Overview
Source: https://docs.crossmint.com/payments/introduction
Allow your users to buy onchain assets using any payment method
Crossmint enables seamless in-app checkout experiences that minimize onboarding friction and maximize conversion.
With Checkout, users can buy memecoins, closed-loop tokens, NFTs, and other assets with debit cards, credit cards, Apple Pay, Google Pay, and stablecoins. Learn more about the types of assets that can be supported here.
Checkout works across all non-sanctioned geographies, has market-leading conversion for buyers, and includes built-in chargeback protection for the platform. This product requires business verification (KYB) during onboarding to get production access — see [account verification](/introduction/platform/account-verification).
## See It in Action
The video below showcases Embedded Checkout for memecoins on fomo, highlighting a seamless mobile onboarding experience with Apple Pay.
## Key Features
Support for any non-sanctioned geo.
Best-in-class conversion rates.
One-click mobile flows.
Crossmint handles disputed transactions.
Dedicated team of anti-fraud experts.
Tools to measure and troubleshoot faster.
## Ways to Integrate
Crossmint-hosted pop-up.
Embedded iframe with customizable UI.
APIs for agents or custom UIs
Crossmint generally recommends to use Embedded Checkout. It yields experiences that feel completely native while handling PSP orchestration and error management, and offering better approval rates.
## Get started
## FAQs
The *checkout* is used when a user is buying a specific item — like an NFT, memecoin, or product — with fiat or crypto. It's a direct purchase of a specific item.
The *onramp* is used when a user wants to fund their wallet with crypto — without buying a specific item. Think of it as topping up a balance instead of making a purchase.
Use *checkout* when selling something. Use *onramp* when your users need crypto to spend later.
Checkout is available in all non-sanctioned geographies. Effective July 1, 2026, token checkout — including memecoins, closed-loop tokens, and other fungible tokens — is not available for buyers in the European Economic Area (EEA), United Kingdom, and Brazil. NFT checkout remains available. This restriction applies to all checkout modalities.
Embedded is the best solution for most teams. It offers embeddable iframe / mobile components which feel native and allow for customizability. Integration takes minutes, as the SDK orchestrates all payment method integrations, different states, and errors.
Our team continuously monitors market activity and proactively reviews eligible memecoins across all of our partners.
You may also request reviews for specific tokens at any time. Review timelines and throughput depend on your SaaS plan and request volume, and we'll always do our best to accommodate. If requests become too frequent relative to transaction volume, we may ask you to upgrade to a higher plan to continue supporting custom review requests.
You don't have to worry about chargeback risk. Crossmint will take on the risk and manage disputes directly with the bank.
Crossmint places an authorization hold on the user's card before initiating the transaction. Funds are only captured once the transaction is successfully completed. If the transaction fails, the hold is released and the user is not charged.
Staging is self-serve — create a staging console account and start building immediately.
Production access requires a signed Order Form and completed KYB verification. Contact sales to get started. See the [account verification documentation](/introduction/platform/account-verification) for the full sequence.
The standard transaction limits are **\$2,000 per user and day**. Limits reset daily at midnight US Eastern Time (05:00 UTC during standard time, 04:00 UTC during daylight saving time). If you need higher limits, contact the sales team.
# Localization
Source: https://docs.crossmint.com/payments/pay-button/customize/localization
Customize the language and currency displayed on checkout
## How to Adjust the Currency and Language
To customize the language use the property `locale` and for currency, set `defaultCurrency`. Here's an example:
```tsx {3,16} theme={null}
```
# React Hooks
Source: https://docs.crossmint.com/payments/pay-button/guides/hooks
Learn how to use React hooks to control and customize the Hosted Checkout UI
## Complete Example
Here's a full example showing how to implement a custom checkout experience:
```tsx theme={null}
import {
CrossmintProvider,
CrossmintCheckoutProvider,
CrossmintHostedCheckout,
useCrossmintCheckout,
} from "@crossmint/client-sdk-react-ui";
function CheckoutPage() {
return (
);
}
function CheckoutStatus() {
const { order } = useCrossmintCheckout();
if (!order) {
return
Loading...
;
}
switch (order.phase) {
case "completed":
return
Purchase complete!
;
case "delivery":
return
Delivering your NFTs...
;
case "payment":
return
Processing payment...
;
case "quote":
return
Preparing your order...
;
}
}
```
## Related Resources
* [UI Customization](/payments/pay-button/guides/ui-customization)
* [Payment Methods](/payments/pay-button/guides/payment-methods)
* [Order Lifecycle](/payments/headless/guides/order-lifecycle)
* [Testing Tips](/payments/advanced/testing-tips)
# Item Selection
Source: https://docs.crossmint.com/payments/pay-button/guides/item-selection
Specify which items to purchase with Hosted Checkout
The Hosted Checkout (Pay Button) allows you to specify which items your users can purchase using the `lineItems` property. This guide explains how to configure item selection for different asset types.
## Item Selection Examples
```tsx collectionLocator theme={null}
```
```tsx collectionLocator - imported contract theme={null}
```
```tsx Solana theme={null}
```
```tsx tokenLocator - EVM theme={null}
::
callData: {
totalPrice: "25.00", // Only totalPrice is required for token purchases
// For external EVM contracts, you might need additional parameters:
// _amount: 1, // if your contract uses _amount instead of quantity
// tokenId: "1234", // if your contract requires explicit tokenId
// metadata: "ipfs://...", // if your contract accepts metadata
},
}}
/>
```
```tsx tokenLocator - Solana theme={null}
:
callData: {
totalPrice: "25.00",
buyerCreatorRoyaltyPercent: 100, // Required for Solana
},
}}
/>
```
## Order Status Tracking
The Pay Button cannot use webhooks for order status tracking because it operates in a separate window. Therefore, you'll need to implement polling to check order status through alternative methods. See the [Order Status Guide](/payments/pay-button/guides/order-status) for more information on tracking order completion in pay button.
# Order Status
Source: https://docs.crossmint.com/payments/pay-button/guides/order-status
Track order status with Hosted Checkout
Since Hosted Checkout opens in a separate window, you can't use `useCrossmintCheckout` to track order status. Instead, you have three options:
## 1. Using Callback Functions
```tsx theme={null}
{
// Handle successful order
console.log("Order completed:", orderId);
}}
onFailure={(error) => {
// Handle failed order
console.error("Order failed:", error);
}}
/>
```
## 2. Using Event Listeners
```tsx theme={null}
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.data.type === "crossmint:checkout:completed") {
console.log("Order completed:", event.data.orderId);
}
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, []);
```
## 3. Using Order Status API
For real-time order status updates, you can use the [Order Status API](/payments/pay-button/guides/order-status) to check the status of an order programmatically.
# Configure Payment Options
Source: https://docs.crossmint.com/payments/pay-button/guides/payment-methods
Customize the payment methods available to users
## Payment Methods Supported
Buyers can purchase digital assets with any cryptocurrency. For example, they can pay for an asset on Optimism chain
with mainnet ETH. This is done real time without bridging and lower gas fees.
## Configure Payment Settings
You can configure payment preferences using the `payment` prop:
```tsx theme={null}
```
### Fiat Payment Options
Configure fiat payment settings:
```tsx theme={null}
payment={{
fiat: {
enabled: true,
defaultCurrency: "usd"
}
}}
```
### Crypto Payment Options
Configure crypto payment preferences:
```tsx theme={null}
payment={{
crypto: {
enabled: true,
defaultChain: "polygon", // Set preferred blockchain
defaultCurrency: "matic" // Set preferred crypto currency
}
}}
```
You will be paid in the currency expected by your registered mint function, regardless of how the buyer pays.
# Production Launch
Source: https://docs.crossmint.com/payments/pay-button/guides/production-launch
Follow this checklist to ensure you are ready for production launch with Pay Button
## From Staging to Production with Pay Button
***
### Launch Checklist
1. Change your API keys to the production versions. Ensure your production API key has the appropriate scopes enabled, such as `orders.create`.
2. Update your collection ID to the production one.
3. As needed, change your passed-in props to be production ready (e.g. make `email`, `payment`, `lineItems` programmatically filled).
```jsx {(1, 4)} Production theme={null}
```
```jsx {(1, 4)} Staging theme={null}
```
# Specify Recipient
Source: https://docs.crossmint.com/payments/pay-button/guides/specify-recipient
Select where purchased items should be delivered in Hosted Checkout
## Specifying the Recipient
For Hosted Checkout, you specify the recipient by adding a `recipient` object to your checkout component. The recipient can be specified by email or wallet address.
### Recipient by Email
When specifying a recipient by email, Crossmint will automatically create a secure custodial wallet on the fly for that email address:
```jsx React {3-5} theme={null}
Buy with Crossmint
```
The recipient will be able to access their purchased items by logging into their Crossmint wallet in [staging](https://staging.crossmint.com/user/collection) or [mainnet](https://www.crossmint.com/user/collection).
### Recipient by Wallet Address
To deliver items directly to a specific blockchain wallet address:
```jsx React {3-6} theme={null}
```
# Customize the Button UI
Source: https://docs.crossmint.com/payments/pay-button/guides/ui-customization
Modify the payment button and checkout experience to match your website's style
You can customize both the payment button and the checkout modal appearance using the `appearance` prop.
## Button Customization
### Theme Options
The button can be styled using predefined themes:
```tsx theme={null}
```
### Custom Colors
Customize the button's accent color:
```tsx theme={null}
```
## Checkout Experience Customization
### Display Mode
Choose how the checkout appears:
```tsx theme={null}
```
### Theme
Set the checkout interface theme:
```tsx theme={null}
```
### Overlay Options
Control the modal overlay behavior:
```tsx theme={null}
```
## Complete Styling Example
Here's an example combining multiple customization options:
```tsx theme={null}
```
The appearance configuration allows you to: - Customize button and checkout themes independently - Set custom accent
colors - Control how the checkout displays (popup/tab) - Manage overlay behavior
# Overview
Source: https://docs.crossmint.com/payments/pay-button/overview
Add a button to your site which opens the checkout in a pop-up or new tab
The hosted variant of checkout is only available for React applications. If you're using a different framework like
Vite or React Native, you can use the [Headless version](/payments/headless/overview) instead.
For general information about Crossmint's Payments product, see the [introduction](/payments/introduction). This guide
will focus on the features specific to the Hosted modality.
## When is Hosted Checkout the best fit?
* **You want to launch quickly and don't need deep customization or a fully native user experience**.
## Get Started
Start selling digital assets in 5 minutes.
Contact our sales team for advanced support.
## Advanced Topics
# Quickstart ⚡
Source: https://docs.crossmint.com/payments/pay-button/quickstart
Accept payments in under 5 min by just adding a button to your frontend
## Introduction
In this guide, you will create a web app with Next.js which allows customers to buy digital assets with credit card and
crypto payments using Crossmint's hosted checkout. We will add a button that opens a checkout hosted in a pop-up or new
tab.
### Basic Integration
This quickstart guide focuses on NFT Collections/Drops (primary sales), but Crossmint also supports marketplace integrations.
Create `.env.local` in your project root:
```sh theme={null}
NEXT_PUBLIC_CLIENT_API_KEY="_YOUR_CLIENT_API_KEY_" # From API Keys page
NEXT_PUBLIC_COLLECTION_ID="_YOUR_COLLECTION_ID_" # From Collection details page
```
Create `/src/app/page.tsx` with:
```tsx theme={null}
"use client";
import { CrossmintProvider, CrossmintHostedCheckout } from "@crossmint/client-sdk-react-ui";
export default function Home() {
const clientApiKey = process.env.NEXT_PUBLIC_CLIENT_API_KEY as string;
const collectionId = process.env.NEXT_PUBLIC_COLLECTION_ID as string;
return (
);
}
```
Navigate to the directory your package.json is to run the app
```bash theme={null}
cd hosted-checkout/crossmint-hosted-checkout-demo
```
### (Optional) Advanced Integration
Need purchase tracking, multiple line items, or more customization? Here's a complete setup with additional features:
```tsx theme={null}
"use client";
import { useEffect } from "react";
import {
CrossmintProvider,
CrossmintCheckoutProvider,
CrossmintHostedCheckout,
useCrossmintCheckout,
} from "@crossmint/client-sdk-react-ui";
// Component with purchase tracking
function Checkout() {
const { order } = useCrossmintCheckout();
const collectionId = process.env.NEXT_PUBLIC_COLLECTION_ID as string;
useEffect(() => {
if (order && order.phase === "completed") {
console.log("Purchase completed!");
// Handle successful purchase
}
}, [order]);
return (
);
}
// Main component with providers
export default function Home() {
const clientApiKey = process.env.NEXT_PUBLIC_CLIENT_API_KEY as string;
return (
);
}
```
```tsx theme={null}
"use client";
import { useEffect } from "react";
import {
CrossmintProvider,
CrossmintCheckoutProvider,
CrossmintHostedCheckout,
useCrossmintCheckout,
} from "@crossmint/client-sdk-react-ui";
// Component with purchase tracking
function Checkout() {
const { order } = useCrossmintCheckout();
useEffect(() => {
if (order && order.phase === "completed") {
console.log("Purchase completed!");
// Handle successful purchase
}
}, [order]);
return (
);
}
// Main component with providers
export default function Home() {
const clientApiKey = process.env.NEXT_PUBLIC_CLIENT_API_KEY as string;
return (
);
}
```
This advanced example showcases: - Multiple NFTs in one checkout - Purchase status tracking - Preferred payment
methods and currencies - Email-based digital asset delivery - Language settings Learn more in our guides: - [Payment
Methods](/payments/pay-button/guides/payment-methods) - [Multi-purchases](/payments/pay-button/guides/item-selection#multiple-item-orders)
* [React Hooks](/payments/pay-button/guides/hooks)
### Testing Your Integration
This demo uses the staging environment:
* Use [test credit cards](/payments/advanced/testing-tips#test-credit-card-numbers) for payments
* Get test USDC from our [faucet](/payments/advanced/testing-tips#getting-test-tokens)
* Check [price limits](/payments/advanced/testing-tips#limits-in-staging) for staging
## All Set!
You've successfully integrated a hosted checkout with credit card and crypto payment methods!
Remember this demo is built on staging, so the digital assets will show up on the testnets. To launch on production,
check the [production launch checklist](/payments/advanced/production-launch). You will need to contact
[Sales](https://www.crossmint.com/contact/sales) to enable the embedded checkout on production.
## Next Steps
### Customize the UI and Behavior Further
Learn how to customize the look and feel of the checkout button
Configure available payment options for your users
Use React hooks to build custom checkout experiences
### Advanced Topics
# Plan Your Integration
Source: https://docs.crossmint.com/payments/plan-your-solution
Walk through key decisions to design your Crossmint Checkout integration
Crossmint Checkout supports multiple asset types and integration modalities. This guide helps you make key decisions for your integration.
## 1. Select the asset type you're selling
Learn more about the types of assets that can be supported here.
## 2. Choose an integration modality
Crossmint checkout can be integrated into your application in three different ways, ranging from a bare metal headless
API with maximum UI flexibility, to a low-code hosted solution.
| Modality | Description | Integration Complexity | Supported Platforms | Recommended For |
| :- | :- | :- | :- | :- |
| [Headless](/payments/headless/overview) | Buy or sell using a REST API. Puts additional work on your side. | \~500 Lines of Code (LOC) | Everywhere | Best suited for backend flows and platforms not natively supported by other modalities. |
| [Embedded (strongly recommended)](/payments/embedded/overview) | Embed a checkout natively within your app. Handles orchestration, errors, and has better approval rates. | \~100 LOC | React, WebView | E-commerce sites, token marketplaces, embedded finance |
| [Hosted](/payments/pay-button/overview) | Add a button to your app that opens a checkout in a separate tab or window. | \~10 LOC | Web | One-off integrations for an event, quick prototypes |
Embedded Checkout is generally recommended. It yields experiences that feel completely native while handling PSP orchestration and error management, and offering better approval rates.
## 3. Choose payment method(s)
You can choose multiple options from the following payment methods to include. Consider the following as you decide:
* Is your audience crypto-native or mainstream consumers familiar with fiat payments?
* What level of optionality do you want to enable? Adding many methods may add complexity during a user's checkout flow.
| Payment Method | Description | Best For |
| :- | :- | :- |
| **Debit & Credit Cards** | All major credit cards supported - Visa, Mastercard, American Express, and more. | Mainstream consumers, global accessibility |
| **Apple & Google Pay** | Check out in under 30 seconds with faster payment methods. | Mobile-first users, quick transactions |
| **Stablecoins** | Pay with USDC, USDT, and other stablecoins for lower transaction fees. | Crypto-native users, lower fees |
| **Cross-Chain Crypto** | Pay with any token, on any chain. | Multi-chain users, existing crypto holders |
Stablecoins and cross-chain crypto not supported for memecoin checkout.
## 4. (For NFTs only) Do you need to create or import an existing collection?
**Choose one:** Do you want to create a new NFT collection or import an existing one?
Use our templates in the Console
Connect your deployed smart contract
## Get Started
Launch a Crossmint-hosted checkout page in a pop-up
Import Crossmint checkout components in your application
Buy or sell via an API, best suited for agents or deep UI customization
# Components
Source: https://docs.crossmint.com/sdk-reference/agents/react/components
React components for React SDK reference for Crossmint agent payments
## CrossmintPaymentMethodManagement
Lets the user save a new card or bank account, or pick one of their saved payment methods. Rendered in a Crossmint-hosted iframe, so payment details never touch your app. Takes the user's Crossmint `jwt`. `onPaymentMethodSelected` receives the chosen method; its `paymentMethodId` is what your backend uses to pay (for example to create order intents).
### Props
Which sections the management UI renders. `["new"]` (default) shows only the "add new" section: no saved-methods fetch, no auto-open/auto-select of an existing method. Include `"existing"` to also show the saved-methods section.
Filter array of which method types the "add new" section offers (default `["card"]`). LM0 renders the first supported entry; passing more than one type does not yet render a type picker.
### Usage
```tsx theme={null}
import { CrossmintPaymentMethodManagement } from "@crossmint/client-sdk-react-ui";
function PaymentMethods({ jwt }: { jwt: string }) {
return (
{
if (paymentMethod.type === "card") {
console.log("Card selected:", paymentMethod.card.last4);
}
}}
/>
);
}
```
> **Note:** CrossmintPaymentMethodManagement renders an iframe-based UI for managing saved payment methods. Requires a valid JWT for authentication. The onPaymentMethodSelected callback receives a discriminated union — narrow on `type` before accessing variant-specific fields.
***
## OrderIntentVerification
Runs the card network's allowance verification for an order intent. Render it when the intent's `agentic-token` rail reports `status: "pending_verification"`; it renders nothing itself and opens the network's verification UI on mount.
### Props
Not applied by `OrderIntentVerification`: only `colors` (and the button / input colors under `rules`) reach the verification modal. Not the multiplier unit of `EmbeddedCheckoutV3AppearanceVariables.fontSizeUnit`, so do not share one object between the two.
Not applied by `OrderIntentVerification`; see `fontSizeUnit`.
Name of the agent shown to the user in the card network's verification UI.
Called once the user has approved the allowance. Refetch the order intent afterwards.
The order intent returned by the API, including its `verificationConfig`.
ISO 3166-1 alpha-2, upper case.
http or https URL of the merchant.
### Usage
```tsx theme={null}
import { OrderIntentVerification, type OrderIntentVerificationProps } from "@crossmint/client-sdk-react-ui";
function VerifyOrderIntent({
orderIntent,
}: {
orderIntent: OrderIntentVerificationProps["orderIntent"];
}) {
return (
{
console.log("Allowance verified");
}}
onVerificationError={(error) => {
console.error("Allowance verification failed", error);
}}
/>
);
}
```
> **Note:** After `onVerificationComplete`, fetch the order intent again and wait for the `agentic-token` rail to report `status: "active"` before creating a credential.
***
## CrossmintCvcRecollection
Asks the user to enter a saved card's CVC again. Render it when an order intent's `encrypted-card` rail reports `status: "pending_cvc_recollection"`, then refetch the order intent from `onComplete`.
### Props
Same appearance model as `CrossmintPaymentMethodManagement` and the embedded checkout, not the one of the verification modal: `variables.fontSizeUnit` / `spacingUnit` are multiplier units (defaults `"4px"` / `"3.33px"`), not base sizes. See `EmbeddedCheckoutV3AppearanceVariables`.
The user's Crossmint auth token, the same one `CrossmintPaymentMethodManagement` takes.
Called once Crossmint has stored the new CVC. Receives no CVC and no token payload.
The saved card whose CVC has to be entered again.
### Usage
```tsx theme={null}
import { CrossmintCvcRecollection } from "@crossmint/client-sdk-react-ui";
function RefreshCardCvc({ jwt, paymentMethodId }: { jwt: string; paymentMethodId: string }) {
return (
{
console.log("CVC stored, refetch the order intent");
}}
onError={(error) => {
if (!error.retriable) {
console.error("CVC recollection failed", error);
}
}}
/>
);
}
```
> **Note:** Render it when an order intent's `encrypted-card` rail reports `status: "pending_cvc_recollection"`, or when creating a credential is refused with HTTP 409 `ORDER_INTENT_CVC_RECOLLECTION_REQUIRED`. Retriable errors keep the form mounted; non-retriable ones unmount it, so show your own message.
***
## CrossmintProtectedInput
Collects the password of the buyer's account on a merchant site inside a Crossmint-hosted iframe and returns an opaque `protectedInputId` to hand to Universal Checkout. The password never reaches this component, the developer's JavaScript, or the agent. Takes the buyer's JWT as a prop, like `CrossmintPaymentMethodManagement`.
### Props
ISO 8601 datetime. Server default is 24 hours from creation; the maximum is 7 days.
The buyer's Crossmint auth token, the same one `CrossmintPaymentMethodManagement` takes.
Text shown above the field, for example the merchant name. At most 120 characters.
The merchant sign-in page the password is for. Its host becomes the input's merchant domain; Universal Checkout may only use the input on that host or a subdomain of it.
Called once with the opaque `protectedInputId` to hand to Universal Checkout.
### Usage
```tsx theme={null}
import { CrossmintProtectedInput } from "@crossmint/client-sdk-react-ui";
function MerchantPassword({ jwt, onProtectedInputId }: { jwt: string; onProtectedInputId: (id: string) => void }) {
return (
onProtectedInputId(protectedInputId)}
onError={({ code, message }) => console.error(code, message)}
/>
);
}
```
> **Note:** CrossmintProtectedInput collects the password of the buyer's account on a merchant site inside a Crossmint-hosted iframe and returns only an opaque protectedInputId. Pass that id to Universal Checkout as the `protected` input response; the password never reaches your JavaScript. Pass the buyer's JWT as `jwt`, like CrossmintPaymentMethodManagement. Lifecycle: the hosted page posts exactly one terminal event per mount, so after onCreated or onError remount the component (change its `key`) to collect again; the page posts `invalid_params` when it rejects the props.
# Get Started
Source: https://docs.crossmint.com/sdk-reference/agents/react/get-started
Installation and setup for the React SDK reference for Crossmint agent payments
### Latest React SDK version -
The Crossmint React SDK (`@crossmint/client-sdk-react-ui`) provides the user-facing pieces of agent card payments: saving a card, verifying an order intent's allowance with the card network, re-collecting a saved card's CVC, and collecting a merchant password for an agent checkout without it ever reaching your app. Agent wallets use the same wallet providers and hooks as the [Wallets React SDK](/sdk-reference/wallets/react/get-started).
## Installation
## Provider Setup
Wrap your application with the required providers:
```tsx theme={null}
import type { ReactNode } from "react";
import { CrossmintProvider } from "@crossmint/client-sdk-react-ui";
function App({ children }: { children: ReactNode }) {
return {children};
}
```
> **Note:** The agent card components take the user's JWT directly, so no additional provider is required. Add `CrossmintWalletProvider` only if your agent also uses wallets.
## Quick Example
Once the provider is set up, let the user save a card your agent can pay with:
```tsx theme={null}
import { CrossmintPaymentMethodManagement } from "@crossmint/client-sdk-react-ui";
function PaymentMethodsPage({ jwt }: { jwt: string }) {
// jwt comes from your own auth provider (for example Stytch, Auth0, Dynamic)
return (
{
// Persist this ID so your backend can register the card and create order intents.
console.log("Card saved:", paymentMethod.paymentMethodId);
}}
/>
);
}
```
> **Note:** Card data is collected directly by Crossmint and never passes through your servers. Store the returned paymentMethodId on your backend; it is the input for registering the card and creating order intents.
## Next Steps
* [Providers](/sdk-reference/agents/react/providers) — Configure providers and their options
* [Components](/sdk-reference/agents/react/components) — Drop-in UI components
# Providers
Source: https://docs.crossmint.com/sdk-reference/agents/react/providers
React context providers for React SDK reference for Crossmint agent payments
1. `CrossmintProvider` — SDK initialization (required for all Crossmint features)
## CrossmintProvider
### Props
Your Crossmint client-side API key.
Application identifier, sent as `x-app-identifier` header.
Minimum log level for console output (or "silent" to suppress all output). Logs below this level will not be written to the console. Set to "silent" to completely suppress console output. Defaults to "debug" (all logs shown) for backward compatibility.
Extension identifier, sent as `x-extension-id` header.
JWT token for authentication.
Override the base API URL.
### Usage
```tsx theme={null}
import { CrossmintProvider } from "@crossmint/client-sdk-react-ui";
function App() {
return (
{/* Your app content */}
);
}
```
# Get Started
Source: https://docs.crossmint.com/sdk-reference/auth/react-native/get-started
Installation and setup for the React Native SDK reference for Crossmint authentication
### Latest React Native SDK version -
The Crossmint React Native SDK (`@crossmint/client-sdk-react-native-ui`) provides React Native providers and hooks for integrating Crossmint authentication into your mobile application, including email OTP, social login (OAuth), and session management.
## Installation
## Provider Setup
Wrap your application with the required providers:
```tsx theme={null}
import {
CrossmintProvider,
CrossmintAuthProvider,
} from "@crossmint/client-sdk-react-native-ui";
function App() {
return (
{/* Your app components */}
);
}
```
## Quick Example
Once providers are set up, use the `useCrossmintAuth` hook to access auth state:
```tsx theme={null}
import { useCrossmintAuth } from "@crossmint/client-sdk-react-native-ui";
import { View, Text, TouchableOpacity } from "react-native";
function LoginScreen() {
const { loginWithOAuth, logout, user, status } = useCrossmintAuth();
if (status === "initializing") return Loading...;
if (user == null) {
return (
loginWithOAuth("google")}>
Sign in with Google
);
}
return (
Welcome, {user.email}Log out
);
}
```
## Next Steps
* [Providers](/sdk-reference/auth/react-native/providers) — Configure providers and their options
* [Hooks](/sdk-reference/auth/react-native/hooks) — Access SDK state via React hooks
# Hooks
Source: https://docs.crossmint.com/sdk-reference/auth/react-native/hooks
React Native hooks for React Native SDK reference for Crossmint authentication
## useCrossmintAuth()
Access Crossmint authentication state and actions: `loginWithOAuth`, `logout`, the current `user`, `jwt`, and auth `status`. Must be used within a `CrossmintAuthProvider`.
### Usage
```tsx theme={null}
import { useCrossmintAuth } from "@crossmint/client-sdk-react-native-ui";
import { View, Text, TouchableOpacity } from "react-native";
function AuthStatus() {
const { loginWithOAuth, logout, user, jwt, status } = useCrossmintAuth();
if (status === "initializing") return Loading...;
return (
{user == null ? (
loginWithOAuth("google")}>
Log in
) : (
Log out ({user.email})
)}
);
}
```
> **Note:** Must be used within a CrossmintAuthProvider.
# Providers
Source: https://docs.crossmint.com/sdk-reference/auth/react-native/providers
React Native context providers for React Native SDK reference for Crossmint authentication
1. `CrossmintProvider` — SDK initialization (required for all Crossmint features)
2. `CrossmintAuthProvider` — Authentication state, login/logout, and the auth flow
## CrossmintProvider
Root provider for the Crossmint SDK. Must wrap your entire application. Initializes the SDK with your API key and sets up logging.
### Props
Your Crossmint client-side API key.
Application identifier, sent as `x-app-identifier` header.
Minimum log level for console output (or "silent" to suppress all output). Logs below this level will not be written to the console. Set to "silent" to completely suppress console output. Defaults to "debug" (all logs shown) for backward compatibility.
Override the base API URL.
### Usage
```tsx theme={null}
import { CrossmintProvider } from "@crossmint/client-sdk-react-native-ui";
function App() {
return (
{/* Your app content */}
);
}
```
***
## CrossmintAuthProvider
Provides Crossmint authentication to your React Native app: social login (OAuth) and session management. Exposes auth state via the `useCrossmintAuth` hook and stores auth material in secure device storage. Must be nested inside `CrossmintProvider`.
### Props
Your app's deep link scheme(s) (e.g. `"myapp://"`), used for OAuth redirects back into your app.
Custom title for the auth modal.
Custom route for logging out, for server-side session management.
Callback invoked when the user successfully logs in.
Whether to prefetch OAuth provider URLs when the provider mounts.
Custom route for refreshing the auth token, for server-side session management.
Custom storage provider for auth material. Defaults to browser cookies on web and secure device storage on React Native.
### Usage
```tsx theme={null}
import {
CrossmintProvider,
CrossmintAuthProvider,
} from "@crossmint/client-sdk-react-native-ui";
function App() {
return (
{/* Your app content */}
);
}
```
> **Note:** CrossmintAuthProvider must be nested inside CrossmintProvider. Set appSchema to your app's deep link scheme so OAuth redirects return to your app.
# Components
Source: https://docs.crossmint.com/sdk-reference/auth/react/components
React components for React SDK reference for Crossmint authentication
## EmbeddedAuthForm
Renders the Crossmint auth form inline in your page instead of in a modal. Displays the same login methods configured on `CrossmintAuthProvider`, which it must be rendered inside of.
### Usage
```tsx theme={null}
import { EmbeddedAuthForm, useCrossmintAuth } from "@crossmint/client-sdk-react-ui";
function LoginPage() {
const { status } = useCrossmintAuth();
if (status === "logged-in") {
return
You are logged in!
;
}
return (
);
}
```
> **Note:** Must be rendered inside CrossmintAuthProvider. Renders the same auth form shown in the login modal, but inline in your page.
# Get Started
Source: https://docs.crossmint.com/sdk-reference/auth/react/get-started
Installation and setup for the React SDK reference for Crossmint authentication
### Latest React SDK version -
The Crossmint React SDK (`@crossmint/client-sdk-react-ui`) provides React components and hooks for integrating Crossmint authentication into your application, including email OTP, social login (OAuth), and session management.
## Installation
## Provider Setup
Wrap your application with the required providers:
```tsx theme={null}
import {
CrossmintProvider,
CrossmintAuthProvider,
} from "@crossmint/client-sdk-react-ui";
function App() {
return (
{/* Your app content */}
);
}
```
> **Note:** CrossmintAuthProvider must be nested inside CrossmintProvider.
## Quick Example
Once providers are set up, use the `useCrossmintAuth` hook to access auth state:
```tsx theme={null}
import { useCrossmintAuth } from "@crossmint/client-sdk-react-ui";
function AuthButton() {
const { login, logout, user, status } = useCrossmintAuth();
if (status === "logged-in") {
return (
Welcome, {user?.email}
);
}
return ;
}
```
## Next Steps
* [Providers](/sdk-reference/auth/react/providers) — Configure providers and their options
* [Hooks](/sdk-reference/auth/react/hooks) — Access SDK state via React hooks
* [Components](/sdk-reference/auth/react/components) — Drop-in UI components
# Hooks
Source: https://docs.crossmint.com/sdk-reference/auth/react/hooks
React hooks for React SDK reference for Crossmint authentication
## useCrossmintAuth()
Access Crossmint authentication state and actions: `login`, `logout`, the current `user`, `jwt`, and auth `status`. Must be used within a `CrossmintAuthProvider`.
### Returns
The underlying Crossmint auth client instance.
Fetch and refresh the current authenticated user.
Current JWT authentication token, if authenticated.
Trigger the login flow. Optionally pass a default email.
Configured login methods.
Initiate a login flow with the specified OAuth provider (e.g. "google", "twitter").
Log the user out and clear the session.
Authentication status: "logged-in" | "logged-out" | "in-progress" | "initializing".
The currently authenticated user object.
### Usage
```tsx theme={null}
import { useCrossmintAuth } from "@crossmint/client-sdk-react-ui";
function LoginButton() {
const { login, logout, user, status } = useCrossmintAuth();
if (status === "logged-in") {
return (
Welcome, {user?.email}
);
}
return ;
}
```
# Providers
Source: https://docs.crossmint.com/sdk-reference/auth/react/providers
React context providers for React SDK reference for Crossmint authentication
1. `CrossmintProvider` — SDK initialization (required for all Crossmint features)
2. `CrossmintAuthProvider` — Authentication state, login/logout, and the auth modal
## CrossmintProvider
### Props
Your Crossmint client-side API key.
Application identifier, sent as `x-app-identifier` header.
Minimum log level for console output (or "silent" to suppress all output). Logs below this level will not be written to the console. Set to "silent" to completely suppress console output. Defaults to "debug" (all logs shown) for backward compatibility.
Extension identifier, sent as `x-extension-id` header.
JWT token for authentication.
Override the base API URL.
### Usage
```tsx theme={null}
import { CrossmintProvider } from "@crossmint/client-sdk-react-ui";
function App() {
return (
{/* Your app content */}
);
}
```
***
## CrossmintAuthProvider
Provides Crossmint authentication to your app: email OTP, social login (OAuth), and session management. Exposes auth state via the `useCrossmintAuth` hook and renders the login modal when `login()` is called. Must be nested inside `CrossmintProvider`.
### Props
Custom UI configuration for the auth modal (colors, fonts, border radius).
Custom title for the auth modal.
Login methods to enable (e.g. `["email", "google"]`). Defaults to `["email", "google"]`.
Custom route for logging out, for server-side session management.
Callback invoked when the user successfully logs in.
Whether to prefetch OAuth provider URLs when the provider mounts. Defaults to `true`.
Custom route for refreshing the auth token, for server-side session management.
Custom storage provider for auth material. Defaults to browser cookies on web and secure device storage on React Native.
Custom terms of service text shown in the auth modal.
### Usage
```tsx theme={null}
import { CrossmintAuthProvider } from "@crossmint/client-sdk-react-ui";
function App() {
return (
console.log("Logged in!")}
authModalTitle="Sign in to My App"
>
{/* Your app content */}
);
}
```
# CrossmintAuth
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/classes/CrossmintAuth
Defined in: [server/src/auth/CrossmintAuthServer.ts:38](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/CrossmintAuthServer.ts#L38)
Server-side Crossmint authentication client: validate sessions, verify JWTs, refresh tokens,
fetch users, and manage auth cookies. Works with both Node.js (`IncomingMessage`/`ServerResponse`)
and Fetch API (`Request`/`Response`) request/response objects.
Create an instance with [CrossmintAuthServer.from](./#from).
## Extends
* `CrossmintAuth`
## Methods
### getJwksUri()
> **getJwksUri**(): `string`
Defined in: common/auth/dist/CrossmintAuth.d.ts:14
#### Returns
`string`
#### Inherited from
`CrossmintAuth.getJwksUri`
***
### getSession()
> **getSession**(`options`, `response`?): `Promise`\<`AuthSession`>
Defined in: [server/src/auth/CrossmintAuthServer.ts:59](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/CrossmintAuthServer.ts#L59)
Validate the user's session and return it, refreshing it if expired.
Accepts either a request object (auth material is read from cookies) or the auth material
itself (`{ jwt, refreshToken }`). If a response object is provided, refreshed auth material
is stored in its cookies.
#### Parameters
| Parameter | Type |
| - | - |
| `options` | [`GenericRequest`](../type-aliases/GenericRequest) \| `AuthMaterialBasic` |
| `response`? | [`GenericResponse`](../type-aliases/GenericResponse) |
#### Returns
`Promise`\<`AuthSession`>
#### Throws
CrossmintAuthenticationError if no valid session can be established.
***
### getUser()
> **getUser**(`externalUserId`): `Promise`\<`any`>
Defined in: [server/src/auth/CrossmintAuthServer.ts:86](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/CrossmintAuthServer.ts#L86)
Fetch the Crossmint user associated with the given external user ID.
#### Parameters
| Parameter | Type |
| - | - |
| `externalUserId` | `string` |
#### Returns
`Promise`\<`any`>
***
### handleCustomRefresh()
> **handleCustomRefresh**(`request`, `response`?): `Promise`\<[`GenericResponse`](../type-aliases/GenericResponse)>
Defined in: [server/src/auth/CrossmintAuthServer.ts:102](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/CrossmintAuthServer.ts#L102)
Handle a token refresh request on a custom refresh route. Reads the refresh token from the
request body or cookies, refreshes the session, and returns a response with the new auth
material stored in cookies. Returns a 401 response if the refresh fails.
#### Parameters
| Parameter | Type |
| - | - |
| `request` | [`GenericRequest`](../type-aliases/GenericRequest) |
| `response`? | [`GenericResponse`](../type-aliases/GenericResponse) |
#### Returns
`Promise`\<[`GenericResponse`](../type-aliases/GenericResponse)>
***
### logout()
> **logout**(`request`?, `response`?): `Promise`\<[`GenericResponse`](../type-aliases/GenericResponse)>
Defined in: [server/src/auth/CrossmintAuthServer.ts:167](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/CrossmintAuthServer.ts#L167)
Log the user out: invalidate the refresh token with Crossmint and clear the auth cookies on the response.
#### Parameters
| Parameter | Type |
| - | - |
| `request`? | [`GenericRequest`](../type-aliases/GenericRequest) |
| `response`? | [`GenericResponse`](../type-aliases/GenericResponse) |
#### Returns
`Promise`\<[`GenericResponse`](../type-aliases/GenericResponse)>
***
### storeAuthMaterial()
> **storeAuthMaterial**(`response`, `authMaterial`): `void`
Defined in: [server/src/auth/CrossmintAuthServer.ts:162](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/CrossmintAuthServer.ts#L162)
Store auth material (JWT and refresh token) in the response's cookies.
#### Parameters
| Parameter | Type |
| - | - |
| `response` | [`GenericResponse`](../type-aliases/GenericResponse) |
| `authMaterial` | `AuthMaterial` |
#### Returns
`void`
***
### verifyCrossmintJwt()
> **verifyCrossmintJwt**(`token`, `jwks`?): `Promise`\<`JWTPayload`>
Defined in: [server/src/auth/CrossmintAuthServer.ts:154](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/CrossmintAuthServer.ts#L154)
Verify a Crossmint-issued JWT and return its decoded payload. Uses Crossmint's JWKS endpoint unless a key set is provided.
#### Parameters
| Parameter | Type |
| - | - |
| `token` | `string` |
| `jwks`? | `JSONWebKeySet` |
#### Returns
`Promise`\<`JWTPayload`>
***
### defaultApiClient()
> `static` **defaultApiClient**(`crossmint`): `CrossmintApiClient`
Defined in: common/auth/dist/CrossmintAuth.d.ts:19
#### Parameters
| Parameter | Type |
| - | - |
| `crossmint` | `Crossmint` |
#### Returns
`CrossmintApiClient`
#### Inherited from
`CrossmintAuth.defaultApiClient`
***
### from()
> `static` **from**(`crossmint`, `options`): `CrossmintAuthServer`
Defined in: [server/src/auth/CrossmintAuthServer.ts:47](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/CrossmintAuthServer.ts#L47)
Create a `CrossmintAuth` instance from a `Crossmint` object (created with `createCrossmint`).
#### Parameters
| Parameter | Type |
| - | - |
| `crossmint` | `Crossmint` |
| `options` | [`CrossmintAuthServerOptions`](../type-aliases/CrossmintAuthServerOptions) |
#### Returns
`CrossmintAuthServer`
#### Overrides
`CrossmintAuth.from`
# createCrossmint
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/functions/createCrossmint
> **createCrossmint**(`config`, `apiKeyExpectations`?): `Crossmint`
Defined in: common/base/dist/types/Crossmint.d.ts:19
## Parameters
| Parameter | Type |
| - | - |
| `config` | `CrossmintConfig` |
| `apiKeyExpectations`? | `ValidateAPIKeyPrefixExpectations` |
## Returns
`Crossmint`
# verifyCrossmintJwt
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/functions/verifyCrossmintJwt
> **verifyCrossmintJwt**(`token`, `jwks`): `Promise`\<`JWTPayload`>
Defined in: [server/src/auth/utils/jwt.ts:9](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/utils/jwt.ts#L9)
Verify a Crossmint-issued JWT against the given JWKS (endpoint URI or key set) and return its decoded payload.
## Parameters
| Parameter | Type |
| - | - |
| `token` | `string` |
| `jwks` | [`JWKSInput`](../type-aliases/JWKSInput) |
## Returns
`Promise`\<`JWTPayload`>
# Getting Started
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/overview
### Latest Node.js SDK version -
The Crossmint server SDK (`@crossmint/server-sdk`) provides a set of tools for authenticating users in a Crossmint-powered application using server-side rendering (SSR). It simplifies the process of handling authentication tokens and managing user sessions, making it easier to integrate authentication into your Next.js applications.
## Installation
To install the SDK, you can use npm or yarn:
```bash theme={null}
npm install @crossmint/server-sdk
```
## Usage
To use the SDK in your application, follow these steps:
1. Import the SDK into your project:
```ts theme={null}
import { createCrossmint, CrossmintAuth } from "@crossmint/server-sdk";
const crossmint = createCrossmint({
apiKey: process.env.SERVER_CROSSMINT_API_KEY || "",
});
const crossmintAuth = CrossmintAuth.from(crossmint);
```
2. Use the SDK to authenticate users:
With most frameworks, pass the request object:
```ts theme={null}
const { jwt, userId } = await crossmintAuth.getSession(request);
```
With Next.js, fetch the cookies and pass them to the `getSession` method:
```ts theme={null}
import { cookies } from "next/headers";
const cookieStore = cookies();
const jwtCookie = cookieStore.get("crossmint-jwt")?.value;
const refreshCookie = cookieStore.get("crossmint-refresh-token")?.value;
const { jwt, userId } = await crossmintAuth.getSession({
jwt: jwtCookie,
refreshToken: refreshCookie,
});
```
3. Store the authentication material in cookies
If you are using a framework with access to the response object, you can store the authentication material in cookies by passing the response object to the `getSession` method:
```ts theme={null}
const { jwt, userId } = await crossmintAuth.getSession(request, response);
```
4. Logout
```ts theme={null}
await crossmintAuth.logout(request);
```
## Security
The SDK allows you to set the `httpOnly`, `secure`, `domain` and `sameSite` options for the cookies. This way, you can control how the cookies are stored and transmitted. Putting this together with a custom refresh route, you can store the authentication material in HttpOnly cookies that are tied to the domain of the provided route.
Configure CrossmintAuth to do so when creating the custom refresh route:
```ts theme={null}
const crossmintAuth = CrossmintAuth.from(crossmint, {
cookieOptions: {
httpOnly: true,
sameSite: "Strict",
secure: true,
domain: ".example.com",
},
});
```
`httpOnly` only applies to the refresh token. The session token will not be HttpOnly as it is used in the client for API calls.
## Set up a custom refresh route
To set up a custom refresh route, you can use the `handleCustomRefresh` method. This method will refresh the token and return the new authentication material. This way, the authentication material can be stored in cookies that are tied to the domain of the provided route.
In environments that use the Fetch API for `Request` and `Response` objects, `handleCustomRefresh` will return the response object:
```ts theme={null}
return await crossmintAuth.handleCustomRefresh(request);
```
In environments that use Node.js API, you also need to provide the response object and end the response:
```ts theme={null}
await crossmintAuth.handleCustomRefresh(req, res);
res.end();
```
### Using a custom refresh route
You can also provide a custom refresh route:
```typescript theme={null}
const crossmintAuth = CrossmintAuth.from(crossmint, {
refreshRoute: "/api/refresh",
});
```
This way, the SDK will use the provided route to refresh the token instead of the default one and the authentication material can be stored in HttpOnly cookies that are tied to the domain of the provided route.
## Set up a custom logout route
When using `HttpOnly` cookies, logout can't happen client-side as it doesn't have access to the cookies. You can set up a custom logout route to handle the logout process.
In environments that use the Fetch API, `logout` will return the response object:
```ts theme={null}
return await crossmintAuth.logout(request);
```
In environments that use Node.js API, you also need to provide the response object and end the response:
```ts theme={null}
await crossmintAuth.logout(req, res);
res.end();
```
# Reference
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/reference
## Classes
* [CrossmintAuth](./classes/CrossmintAuth)
## Type Aliases
* [CrossmintAuthServerOptions](./type-aliases/CrossmintAuthServerOptions)
* [GenericRequest](./type-aliases/GenericRequest)
* [GenericResponse](./type-aliases/GenericResponse)
* [JWKSInput](./type-aliases/JWKSInput)
## Variables
* [CROSSMINT\_API\_VERSION](./variables/CROSSMINT_API_VERSION)
* [SDK\_NAME](./variables/SDK_NAME)
* [SDK\_VERSION](./variables/SDK_VERSION)
## Functions
* [createCrossmint](./functions/createCrossmint)
* [verifyCrossmintJwt](./functions/verifyCrossmintJwt)
# CrossmintAuthServerOptions
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/type-aliases/CrossmintAuthServerOptions
> **CrossmintAuthServerOptions** = `CrossmintAuthOptions` & `object`
Defined in: [server/src/auth/CrossmintAuthServer.ts:26](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/CrossmintAuthServer.ts#L26)
## Type Declaration
### cookieOptions?
> `optional` **cookieOptions**: `CookieOptions`
Options applied to the auth cookies set by the SDK (e.g. `domain`).
# GenericRequest
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/type-aliases/GenericRequest
> **GenericRequest** = `IncomingMessage` | `Request`
Defined in: [server/src/auth/types/request.ts:4](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/types/request.ts#L4)
# GenericResponse
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/type-aliases/GenericResponse
> **GenericResponse** = `ServerResponse` | `Response`
Defined in: [server/src/auth/types/request.ts:5](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/types/request.ts#L5)
# JWKSInput
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/type-aliases/JWKSInput
> **JWKSInput** = `string` | `JSONWebKeySet`
Defined in: [server/src/auth/utils/jwt.ts:6](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/utils/jwt.ts#L6)
A JWKS endpoint URI or an inline JSON Web Key Set.
# CROSSMINT_API_VERSION
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/variables/CROSSMINT_API_VERSION
> `const` **CROSSMINT\_API\_VERSION**: `"2024-09-26"` = `"2024-09-26"`
Defined in: [server/src/auth/utils/constants.ts:5](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/utils/constants.ts#L5)
# SDK_NAME
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/variables/SDK_NAME
> `const` **SDK\_NAME**: `string` = `name`
Defined in: [server/src/auth/utils/constants.ts:4](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/utils/constants.ts#L4)
# SDK_VERSION
Source: https://docs.crossmint.com/sdk-reference/auth/typescript/variables/SDK_VERSION
> `const` **SDK\_VERSION**: `string` = `version`
Defined in: [server/src/auth/utils/constants.ts:3](https://github.com/Crossmint/crossmint-sdk/blob/main/packages/server/src/auth/utils/constants.ts#L3)
# CrossmintCheckoutAppearance
Source: https://docs.crossmint.com/sdk-reference/checkout/flutter/classes/crossmint-checkout-appearance
Flutter Final Class
**Final Class**
UI customization for the hosted checkout page.
```dart theme={null}
final class CrossmintCheckoutAppearance
```
Supports three levels of customization:
* `variables` — CSS custom properties (e.g. `colorPrimary`, `borderRadius`)
* `rules` — CSS rules targeting specific elements (e.g. `.Input`, `.Tab`)
* `fonts` — custom web fonts via `cssSrc`
## Constructors
### CrossmintCheckoutAppearance
```dart theme={null}
const CrossmintCheckoutAppearance({this.variables, this.rules, this.fonts})
```
## Properties
### variables
```dart theme={null}
final Map? variables
```
CSS custom properties for theming (e.g. `{'colorPrimary': '#0F172A'}`).
### rules
```dart theme={null}
final Map? rules
```
CSS rules for specific checkout elements (e.g. `{'.Input': {'borderColor': '#CBD5E1'}}`).
### fonts
```dart theme={null}
final List