Signer: The signing wallet must be registered as a signer on the wallet. You can do this at wallet creation by passing signers, or afterward by adding a signer.
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.
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.
The next steps depend on which signer type you specified in the previous step.
Server Signer
Device Signer
External Wallet
Email and Phone
Passkey
Server signers derive a private key from your signer secret using HKDF-SHA256 and sign transactions locally.When using the SDK, signing and approval are handled automatically — no additional steps are needed.When using the REST API directly, you must:
Derive the signing key from your signer secret (scoped to your project, environment, and chain type). See the derive-server-signer helper for a reference implementation.
Sign the approval message returned from the transaction creation step.
Device signers generate a P256 keypair inside the device’s secure hardware (Secure Enclave, Android Keystore, or browser Web Crypto API). Because the private key never leaves the device, device signers are client-side only and cannot be used directly with the REST API.
Crossmint recommends using the React, React Native, or Swift SDK for device signer flows. The SDK handles device key generation, storage, and signing automatically.
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
Sign the message using your external wallet. The message is a raw hex string and must be signed exactly as returned. Here’s an example using an EVM wallet with Viem:
import { privateKeyToAccount } from "viem/accounts";// The message from tx.approvals.pending[0].messageconst 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, you 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.
3
Submit the approval
Skip this step if using the SDK with a server signer — the SDK handles signing and approval automatically.
Call the approve transaction endpoint with the signature from Step 2 and the transaction ID from Step 1. Set approvals[].signer to the locator of the signer or recovery method that produced the signature.
Add an x-idempotency-key header when you create a transfer from your server. If you retry the request with the same key, Crossmint returns the original transaction instead of creating a second one. The key identifies your request. It is not the transaction ID.
1
Create a key
Create a new UUID for each transfer and store it with the transfer in your system. Send it with the request.
A retry does not restart a transaction. You get the transaction in its current status. A failed transaction stays failed, and an awaiting-approval transaction still needs approval. To try again, use a new key.
The request body is not compared. If you reuse a key with a different amount or recipient, Crossmint returns the original transaction.
Keys apply to the whole project and do not expire. A key covers all wallets in the project, on both the Transfer Token and Create Transaction endpoints.
Responses do not include the key. You cannot look up a transaction by key, so save the transaction id.
On Stellar, a transaction expires if it is not approved before the ledger in onChain.expiration, about 8 minutes after it is created. An expired transaction keeps the awaiting-approval status. If you submit an approval for it, Crossmint returns a 422 error and sets the status to failed.