Creates a transfer from a Coinbase exchange account to an external wallet address. This lets users withdraw cryptocurrency from their Coinbase holdings to a wallet they control.Before calling this function, use getCoinbaseAccounts to get the accountId and check the available balance.Coinbase transfers can involve two extra steps beyond a normal API call, covered in detail below:
Two-step verification (2FA): a one-time code Coinbase sends to the account holder.
Travel Rule information: some recipient details Coinbase is legally required to collect for certain transfers.
import { createCoinbaseExchangeTransfer, getCoinbaseAccounts,} from '@dynamic-labs-sdk/client';// Get the user's Coinbase accountsconst accounts = await getCoinbaseAccounts();// Create a transfer from the first account. Include any recipient info you// already have as travelRuleData. See below for why.const transfer = await createCoinbaseExchangeTransfer({ accountId: accounts[0].id, to: '0x742d35Cc6634C0532925a3b844Bc9e7595f7ABCD', amount: 0.5, currency: 'ETH', travelRuleData: { beneficiaryName: 'Jane Doe', beneficiaryCountry: 'US' },});console.log('Transfer ID:', transfer.id);console.log('Status:', transfer.status);
The Coinbase account ID to transfer from. Get this from getCoinbaseAccounts().
to
string
The destination wallet address.
amount
number
The amount to transfer.
currency
string
The cryptocurrency to transfer (e.g., 'ETH', 'BTC', 'USDC').
description
string (optional)
A description for the transfer.
network
string (optional)
Network name (e.g., 'ethereum', 'polygon').
networkObject
object (optional)
Network details with chainName and networkId. Use this for multi-network tokens.
networkObject.chainName
string
The chain name (e.g., 'EVM').
networkObject.networkId
string
The network ID (e.g., '1' for Ethereum mainnet, '137' for Polygon).
destinationTag
string (optional)
Destination tag/memo some chains require to route funds to the right sub-account (e.g. XRP, Stellar).
mfaCode
string (optional)
The two-step verification code Coinbase sent the account holder. See Two-step verification below.
travelRuleData
object (optional)
Recipient information Coinbase may require by law. See Travel Rule below. Send this on every transfer whenever you have it, not just after being asked for it.
id
string (optional)
Idempotency key to prevent duplicate transfers on retry.
client
DynamicClient (optional)
The Dynamic client instance. Only required when using multiple clients.
Promise<ExchangeTransferResponse> - A promise that resolves to the transfer details:
type ExchangeTransferResponse = { id: string; // Unique transfer ID exchangeAccountId?: string; // The Coinbase account ID status?: string; // Transfer status: 'pending', 'completed', 'failed', etc. amount: number; // Amount transferred currency: string; // Currency code createdAt?: Date; // When the transfer was created};
Coinbase requires its own two-step verification for most transfers, separate from any authentication Dynamic handles. The first createCoinbaseExchangeTransfer call for a given transfer makes Coinbase automatically send the account holder a short-lived code (SMS, or a push to an authenticator app like Authy). The call throws CoinbaseTransferMfaRequiredError; retry the same transfer with mfaCode set to that code.
Error
Meaning
What to do
CoinbaseTransferMfaRequiredError
A code was just sent to the account holder.
Prompt the user for the code, then retry with mfaCode set.
CoinbaseTransferMfaFailedError
The submitted mfaCode was wrong, or expired before it was submitted.
Retry without mfaCode. This makes Coinbase send a fresh code, triggering CoinbaseTransferMfaRequiredError again.
import { createCoinbaseExchangeTransfer, CoinbaseTransferMfaRequiredError,} from '@dynamic-labs-sdk/client';const transferParams = { accountId, amount: 0.5, currency: 'USDC', to, // Send known recipient info upfront. See Travel Rule information below. travelRuleData: { beneficiaryName: 'Jane Doe', beneficiaryCountry: 'US' },};try { await createCoinbaseExchangeTransfer(transferParams);} catch (error) { if (error instanceof CoinbaseTransferMfaRequiredError) { // Coinbase already sent a code to the account holder. Prompt for it. const mfaCode = await promptUserForCoinbase2faCode(); await createCoinbaseExchangeTransfer({ ...transferParams, mfaCode }); }}
The “Travel Rule” is a law that requires exchanges like Coinbase to know who is sending money to whom for larger or cross-border transfers, not just wallet addresses but basic recipient identity. Whether it applies, and which fields it needs, depends on the sender’s and recipient’s countries and the transfer amount, so it can’t be predicted ahead of time. If Coinbase needs this information and it’s missing, the call throws CoinbaseTravelRuleRequiredError listing exactly which fields are required, each with a description you can show the user.
type CoinbaseTravelRuleData = { beneficiaryWalletType?: 'WALLET_TYPE_SELF_HOSTED' | 'WALLET_TYPE_EXCHANGE'; // Is the destination wallet self-custody, or held at another exchange? isSelfTransfer?: 'IS_SELF_TRUE' | 'IS_SELF_FALSE'; // Is the destination wallet also the sender's own? beneficiaryName?: string; // Recipient's full legal name beneficiaryCountry?: string; // Recipient's country, as an ISO 3166-1 alpha-2 code (e.g. "US") beneficiaryFinancialInstitution?: string; // Recipient exchange's VASP ID (only needed if beneficiaryWalletType is 'WALLET_TYPE_EXCHANGE') beneficiaryAddress?: { address1?: string; city?: string; country?: string; postalCode?: string; state?: string; }; transferPurpose?: string; // Free-text reason for the transfer (e.g. "Rent")};
Coinbase checks 2FA before Travel Rule on every request. If a transfer needs both and travelRuleData is missing on the attempt that finally includes a correct mfaCode, that attempt fails on the Travel Rule check instead of succeeding, and the mfaCode you just used is now spent, forcing the user through 2FA a second time. Send travelRuleData upfront whenever you already have it (e.g. a saved recipient), rather than waiting to be asked, so a transfer needing both only ever costs one 2FA prompt.
import { createCoinbaseExchangeTransfer, CoinbaseTravelRuleRequiredError,} from '@dynamic-labs-sdk/client';// Good: include any known recipient info from the start.const transferParams = { accountId, amount: 0.5, currency: 'USDC', to, travelRuleData: { beneficiaryName: 'Jane Doe', beneficiaryCountry: 'US' },};try { await createCoinbaseExchangeTransfer(transferParams);} catch (error) { if (error instanceof CoinbaseTravelRuleRequiredError) { // e.g. [{ name: 'BENEFICIARY_WALLET_TYPE', description: '...' }, ...] const travelRuleData = await promptUserForTravelRuleFields(error.missingFields); await createCoinbaseExchangeTransfer({ ...transferParams, travelRuleData: { ...transferParams.travelRuleData, ...travelRuleData }, }); }}
The Coinbase OAuth provider is missing wallet:transactions:send in the dashboard’s Additional Scopes field. The request fails with a 422 and the message “The access token scope is invalid.”
APIError
Any other error returned by the transfer request (e.g. insufficient balance).
Use an idempotency key to safely retry failed requests without creating duplicate transfers:
import { createCoinbaseExchangeTransfer } from '@dynamic-labs-sdk/client';import { v4 as uuidv4 } from 'uuid';const createSafeTransfer = async (params) => { const transfer = await createCoinbaseExchangeTransfer({ ...params, id: uuidv4(), // Idempotency key }); return transfer;};// If the network fails and you retry, the same transfer is returned// instead of creating a duplicateconst transfer = await createSafeTransfer({ accountId: 'acc_123', to: '0x742d35Cc6634C0532925a3b844Bc9e7595f7ABCD', amount: 0.5, currency: 'ETH', travelRuleData: { beneficiaryName: 'Jane Doe', beneficiaryCountry: 'US' },});
import { createCoinbaseExchangeTransfer, CoinbaseTransferMfaRequiredError, CoinbaseTransferMfaFailedError, CoinbaseTravelRuleRequiredError,} from '@dynamic-labs-sdk/client';const executeTransfer = async (baseParams) => { let params = { ...baseParams }; // Up to 3 attempts: the original request, plus one retry per extra // requirement Coinbase reports back. for (let attempt = 0; attempt < 3; attempt++) { try { return await createCoinbaseExchangeTransfer(params); } catch (error) { if (error instanceof CoinbaseTransferMfaRequiredError) { const mfaCode = await promptUserForCoinbase2faCode(); params = { ...params, mfaCode }; } else if (error instanceof CoinbaseTransferMfaFailedError) { // Wrong or expired code. Drop it so Coinbase sends a fresh one. const { mfaCode, ...rest } = params; params = rest; } else if (error instanceof CoinbaseTravelRuleRequiredError) { const travelRuleData = await promptUserForTravelRuleFields( error.missingFields ); // Body changed. Coinbase invalidates any mfaCode issued for the // previous version of this request, so drop it too. const { mfaCode, ...rest } = params; params = { ...rest, travelRuleData: { ...rest.travelRuleData, ...travelRuleData }, }; } else { throw error; } } } throw new Error('Transfer did not complete after 3 attempts');};// Usageconst transfer = await executeTransfer({ accountId: accounts[0].id, amount: 0.5, currency: 'ETH', to: '0x742d35Cc6634C0532925a3b844Bc9e7595f7ABCD', // Sending known recipient info upfront skips a Travel Rule round trip // entirely when it's the only extra thing this transfer needs. travelRuleData: { beneficiaryName: 'Jane Doe', beneficiaryCountry: 'US' },});
User must have connected their Coinbase account through Dynamic
The Coinbase account must have sufficient balance for the transfer
Add wallet:transactions:send to the Coinbase OAuth provider’s Additional Scopes field in the Dynamic dashboard. Reading Coinbase accounts and balances only needs the default read-only scopes, but sending funds requires this additional scope.
Transfers are processed by Coinbase and may take time to complete depending on network conditions
The status field indicates the current state of the transfer
Use idempotency keys (id parameter) in production to prevent duplicate transfers from network retries
For tokens on multiple networks (like USDC), always specify the networkObject to ensure the transfer goes to the correct chain
Always send travelRuleData upfront when you already have the recipient’s information. It’s optional, but supplying it late costs users an extra two-step verification prompt