Prerequisites
- Wallet: Create a wallet to transfer from.
- Operational Signer: The signing wallet must be registered as an operational signer on the wallet. You can do this at wallet creation by passing
delegatedSigners, or afterward by registering a signer. - Test Tokens: Fund your wallet with USDXM testnet tokens before transferring.
- API Key: Get an API key with the
wallets:transactions.createscope. In staging, all scopes are included by default.
What is a token transfer?
A token transfer moves a token from one wallet address to another onchain. This creates an onchain transaction that costs gas, which Crossmint handles for you. Once the blockchain confirms the transaction, the transfer is final and cannot be reversed.Sending tokens
- React
- Node.js
- React Native
- Swift
- REST
Report incorrect code
Copy
Ask AI
import { useWallet } from '@crossmint/client-sdk-react-ui';
const { wallet } = useWallet();
const { hash, explorerLink } = await wallet.send("0x0D282906CDD8F6934d60E4dCAa79fa5B1c7a1925", "usdxm", "3.14");
Report incorrect code
Copy
Ask AI
import { CrossmintWallets, WalletsApiClient, createCrossmint } from "@crossmint/wallets-sdk";
import { privateKeyToAccount } from "viem/accounts";
const crossmint = createCrossmint({
apiKey: "<your-server-api-key>",
});
const crossmintWallets = CrossmintWallets.from(crossmint);
const apiClient = new WalletsApiClient(crossmint);
const account = privateKeyToAccount(
process.env.EXTERNAL_WALLET_PRIVATE_KEY as `0x${string}`
);
const wallet = await crossmintWallets.getWallet(
"<wallet-address>",
{ chain: "base-sepolia", signer: { type: "external-wallet" } }
);
// 1. Create the transaction in prepare-only mode
const { transactionId } = await wallet.send(
"0x0D282906CDD8F6934d60E4dCAa79fa5B1c7a1925",
"usdxm",
"3.14",
{
experimental_prepareOnly: true,
experimental_signer: `external-wallet:${account.address}`,
}
);
// 2. Fetch the pending transaction to get the message that needs signing
const txResponse = await apiClient.getTransaction(
wallet.address,
transactionId
);
if ("error" in txResponse) {
throw new Error(`Failed to get transaction: ${JSON.stringify(txResponse)}`);
}
const pendingApproval = txResponse.approvals?.pending[0];
if (!pendingApproval) {
throw new Error("No pending approval found");
}
// 3. Sign the pending approval message with your viem private key
const signature = await account.signMessage({
message: { raw: pendingApproval.message as `0x${string}` },
});
// 4. Submit the approval
const result = await wallet.approve({
transactionId,
options: {
experimental_approval: {
signature,
signer: `external-wallet:${account.address}`,
},
},
});
console.log("Transaction approved:", result);
Report incorrect code
Copy
Ask AI
import { useWallet } from '@crossmint/client-sdk-react-native-ui';
const { wallet } = useWallet();
const { hash, explorerLink } = await wallet.send("0x0D282906CDD8F6934d60E4dCAa79fa5B1c7a1925", "usdxm", "3.14");
Report incorrect code
Copy
Ask AI
import CrossmintClient
import Wallet
let sdk = CrossmintSDK.shared
let wallet = try await sdk.crossmintWallets.getOrCreateWallet(
chain: .baseSepolia,
signer: .email("user@example.com")
)
let result = try await wallet.send("0x0D282906CDD8F6934d60E4dCAa79fa5B1c7a1925", "usdxm", 3.14)
Transfers must be approved by one of the wallet’s signers.
The SDK handles this automatically, but with the REST API you may need to craft, sign, and submit the approval manually.Call the approve transaction endpoint with the signature from Step 2 and the transaction ID from Step 1.See the API reference for more details.
Create the transaction
Call the transfer token endpoint.See the API reference for more details.
Report incorrect code
Copy
Ask AI
curl --request POST \
--url https://staging.crossmint.com/api/2025-06-09/wallets/<walletAddress>/tokens/base-sepolia:usdc/transfers \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: <x-api-key>' \
--data '{
"recipient": "<recipientAddress>",
"amount": "3.14",
"signer": "external-wallet:<externalWalletAddress>"
}'
Choose your signer type
The next steps depend on which signer type you specified in the previous step.API Key signers require no additional steps. Crossmint approves transactions automatically when using API key authentication, so the remaining steps in this guide do not apply.
- API Key
- External Wallet
- Email & Phone
- Passkey
Contact us for access to API key signers.
For External Wallet signers, you must manually sign the approval message and submit it via the API. The response from Step 1 includes a pending approval with a
message field that must be signed exactly as returned.From the previous step’s response, extract:id- The transaction ID (used in the next step)approvals.pending[0].message- The hex message to sign
Report incorrect code
Copy
Ask AI
import { privateKeyToAccount } from "viem/accounts";
// The message from tx.approvals.pending[0].message
const messageToSign = "<messageFromResponse>";
// Sign the message exactly as returned (raw hex)
const account = privateKeyToAccount(`0x${"<privateKey>"}`);
const signature = await account.signMessage({
message: { raw: messageToSign },
});
Email and phone signers require client-side OTP verification and key derivation, which the Crossmint SDK handles automatically. While REST API signing is technically possible, Crossmint does not recommend it because you would still need client-side SDK integration for the signing step.
Crossmint recommends using the React, React Native, Swift, or Node.js SDK examples instead. The SDK handles the full signing flow for email and phone signers.
Passkey signers use WebAuthn for biometric or password manager authentication, which requires browser interaction. While REST API signing is technically possible, Crossmint does not recommend it because you would still need client-side SDK integration for the WebAuthn signing step.
Crossmint recommends using the React, React Native, Swift, or Node.js SDK examples instead. The SDK handles the full passkey signing flow automatically.
Submit the approval
Skip this step if using an
api-key signer.Report incorrect code
Copy
Ask AI
curl --request POST \
--url https://staging.crossmint.com/api/2025-06-09/wallets/<walletAddress>/transactions/<txId>/approvals \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: <x-api-key>' \
--data '{
"approvals": [{
"signer": "external-wallet:<externalWalletAddress>",
"signature": "<signature>"
}]
}'
Complete example
Here’s a complete, copy-pastable example for theexternal-wallet signer flow using :Report incorrect code
Copy
Ask AI
import { privateKeyToAccount } from "viem/accounts";
// ============================
// Config (replace placeholders)
// ============================
const API_BASE_URL = "https://staging.crossmint.com";
const API_VERSION = "2025-06-09";
const API_KEY = "<x-api-key>";
const WALLET_ADDRESS = "<walletAddress>";
const CHAIN = "base-sepolia";
const TOKEN = "usdc";
// The externally-owned address that will sign the approval message:
const EXTERNAL_WALLET_ADDRESS = "<externalWalletAddress>";
const RECIPIENT_ADDRESS = "<recipientAddress>";
const AMOUNT = "0.69";
const headers = {
"X-API-KEY": API_KEY,
"Content-Type": "application/json",
};
// ============================
// STEP 1: Create the transaction
// ============================
const createTransferUrl = `${API_BASE_URL}/api/${API_VERSION}/wallets/${WALLET_ADDRESS}/tokens/${CHAIN}:${TOKEN}/transfers`;
const createTransferPayload = {
recipient: RECIPIENT_ADDRESS,
amount: AMOUNT,
signer: `external-wallet:${EXTERNAL_WALLET_ADDRESS}`,
};
const createTransferRes = await fetch(createTransferUrl, {
method: "POST",
headers,
body: JSON.stringify(createTransferPayload),
});
if (!createTransferRes.ok) {
throw new Error(
`Failed to create transfer: ${createTransferRes.status} ${await createTransferRes.text()}`
);
}
const tx = await createTransferRes.json();
const txId = tx.id;
const messageToSign = tx?.approvals?.pending?.[0]?.message;
if (!messageToSign) {
throw new Error(
"No approval message found. Ensure signer is external-wallet and that an approval is pending."
);
}
console.log("txId:", txId);
console.log("messageToSign:", messageToSign);
// ============================
// STEP 2: Sign the approval message (Viem)
// ============================
// IMPORTANT: sign the message EXACTLY as returned (a raw hex string).
const account = privateKeyToAccount(`0x${"<privateKey>"}`);
const signature = await account.signMessage({
message: { raw: messageToSign },
});
console.log("signature:", signature);
// ============================
// STEP 3: Submit the signature
// ============================
const submitApprovalUrl = `${API_BASE_URL}/api/${API_VERSION}/wallets/${WALLET_ADDRESS}/transactions/${txId}/approvals`;
const submitApprovalPayload = {
approvals: [
{
signer: `external-wallet:${EXTERNAL_WALLET_ADDRESS}`,
signature,
},
],
};
const submitApprovalRes = await fetch(submitApprovalUrl, {
method: "POST",
headers,
body: JSON.stringify(submitApprovalPayload),
});
if (!submitApprovalRes.ok) {
throw new Error(
`Failed to submit approval: ${submitApprovalRes.status} ${await submitApprovalRes.text()}`
);
}
const approvalResult = await submitApprovalRes.json();
console.log("approvalResult:", approvalResult);
// ============================
// STEP 4 (optional): Check transaction status
// ============================
const getTxUrl = `${API_BASE_URL}/api/${API_VERSION}/wallets/${WALLET_ADDRESS}/transactions/${txId}`;
const getTxRes = await fetch(getTxUrl, { method: "GET", headers });
if (!getTxRes.ok) {
throw new Error(
`Failed to fetch transaction: ${getTxRes.status} ${await getTxRes.text()}`
);
}
const txStatus = await getTxRes.json();
console.log("txStatus:", txStatus);
Verify your transfer
Once the transfer completes, you can verify it in two ways:- View the onchain transaction using the
explorerLinkreturned by thesendmethod (available in the React and React Native SDKs):
Report incorrect code
Copy
Ask AI
console.log("View transaction:", explorerLink);
// Example: https://sepolia.basescan.org/tx/0xe5844116732d6cd21127bfc100ba29aee02b82cc4ab51e134d44e719ca8d0b48
When using the Node.js SDK with
experimental_prepareOnly mode, explorerLink is not returned. You can check the transaction status using the transactionId via the API reference.- Check the recipient’s balance programmatically using the check balances API.

