Scope
Before you begin
- Ask your Meld representative to enable headless and transfers on your account, in sandbox first. Each environment is enabled separately
- Create the customer
- Configure webhooks. Transfers use the same transaction events as purchases
- Choose a
redirectUrl. See Redirect URL
Flow overview
1. Find what can be transferred
Endpoint:GET /network-partner/supported/currencies
currencyCode as the order’s currencyCode, and its chainCode as the order’s networkCode, exactly as returned. For example, USDC on Base is currencyCode: USDC_BASE with networkCode: BASE.
Read the list rather than hardcoding it. An order for an asset that is not on it is refused.
2. Create the order
Endpoint:POST /crypto/order/headless/transfer
Headers: Authorization: BASIC {apiKey}, X-Idempotency-Key: <uuid>
Redirect URL
redirectUrl must be at most 2,048 characters, carry no user name or password, and be one of these:
- An absolute
httpsURL, such as a universal link on iOS or an App Link on Android - A URL on your app’s custom scheme, such as
myapp://coinbase/return. Contact Meld to use one. You need it to support iOS before 17.4, whereASWebAuthenticationSessioncannot return to anhttpsURL
400 INVALID_REDIRECT_URL.
The response
Order creation returns201:
Fields with no value are left out.
Retries and idempotency
Send a newX-Idempotency-Key for each new order.
- A retry with the same key and the same body returns
200with the order as first created and a newactionToken. Read the order for its currentnext - A retry while the first request is still running returns
425withRetry-After: 2and no body. Retry with the same key - The same key with a different body is refused with
409 IDEMPOTENCY_KEY_CONFLICT
Errors on order creation
Errors use the standard envelope.3. Run the order’s steps
Call the action endpoint
Endpoint:POST {paymentActions.endpoint}
Headers: Authorization: Bearer {actionToken}, Content-Type: application/json, and X-Idempotency-Key: <uuid> where the operation needs it
- Call
paymentActions.endpointon the same base URL as your other calls - Authenticate with the action token only. Do not also send your API key: a request with two
Authorizationheaders is refused - Send
version: 1, theoperation, and that operation’s field, if it has one. Any other field is refused with400 INVALID_REQUEST
Keys are UUIDs. A repeated key never acts twice, so after a timeout, retry with the same key. A missing key where one is required, or a key where none is allowed, is refused with
400 INVALID_REQUEST.
Read the order
Endpoint:GET /crypto/order/headless/transfer/{orderId}, with your API key
It returns the order as the create response does, with the current next and a new actionToken. The action token lasts 30 minutes, and the one it replaces works for 2 more minutes. An unknown order returns 404.
The action response
Every action returns the same envelope. The order’snext field has the same shape.
Steps
Branch onnextStep.
Link the account
-
Send
PREPARE_ACCOUNT_AUTHORIZATION. The answer carries the provider’s page:The attempt lasts 15 minutes. A retry with the same key returns the same URL while it is still valid. -
Open
verification.urlin a system browser session:ASWebAuthenticationSessionon iOS, Custom Tabs on Android, or a browser tab on the web. Prefer an ephemeral session. Do not open it in an embedded web view. -
The customer signs in to the provider and approves access. Meld then sends the browser to your
redirectUrlwith three query parameters added:If the customer declines,transferAuthorizationisDECLINED, there is nohandle, and the order’snextisAUTHORIZE_ACCOUNTwithACCOUNT_AUTHORIZATION_DECLINED. If the customer comes back after the attempt expired or was already used, Meld shows a “Link expired” page instead of redirecting. After a decline, or a session that ends without a redirect, sendPREPARE_ACCOUNT_AUTHORIZATIONwith a new key to try again. -
Your app passes
handleto your backend. Your backend sends it asauthorizationHandle:The handle works only with this order’s action token. Success returnsSELECT_SOURCE_ACCOUNTwithlinkedAccount.displayName.
Choose the source account
SendLIST_SOURCE_ACCOUNTS, without an idempotency key. It returns the customer’s accounts that hold the order’s asset:
eligible is true when availableAmount covers amount. Let the customer pick only an eligible account: Meld does not refuse an ineligible one, and the provider’s decline ends the order. The provider can still decline an eligible account whose balance does not also cover the network fee. An empty list means the customer holds none of this asset there.
Then send SUBMIT_TRANSFER with the chosen account’s id:
- An order makes one send. Use one
X-Idempotency-Keyand onesourceAccountIdforSUBMIT_TRANSFERon the order, and resend both on every retry, including after the customer links again. Once the order has a send, a different key or account is refused with409 REQUEST_CONFLICT - A customer can have one unresolved send at a time. A send on another of their orders meanwhile is refused with
409 CONCURRENT_STATE_CHANGE - An account id that
LIST_SOURCE_ACCOUNTSdid not return is refused with400 INVALID_REQUEST
SUBMIT_TRANSFER answers with one of these:
If
COMPLETE_ACCOUNT_AUTHORIZATION keeps returning 425 while the customer links again, they still have a send that is not resolved, possibly this order’s own. Read the order and follow next. A send from this order that never reached the provider ends with TRANSFER_NOT_SENT within about 15 minutes.
Second factor
The provider can ask the customer for their two-step verification code before it sends. The answer isCOLLECT_SECOND_FACTOR:
- The customer has up to 5 attempts, and 5 minutes to enter each code.
secondFactor.expiresAtshows the current deadline. After that the order ends withSECOND_FACTOR_EXPIREDorSECOND_FACTOR_EXHAUSTED - Six wrong codes in a row on one link within 24 hours lock it, across orders. The order ends with
SECOND_FACTOR_LOCKED, and the customer links the account again on the next order
Wait for the outcome
WAIT_FOR_PROVIDER means the provider is working on the send. Rely on webhooks for the outcome. To show progress, poll READ_SUBMISSION every few seconds. It does not call the provider or replace the action token.
Action errors
Errors from the action endpoint have their own small body, not the standard envelope:
Treat a code you do not recognise as “read the order and follow
next”.
4. Settlement and webhooks
Meld creates a transaction once the provider accepts the send. Its type isCRYPTO_TRANSFER, and its id is the order’s transactionId. The standard webhook events then follow:
TRANSACTION_CRYPTO_PENDING: the provider accepted the sendTRANSACTION_CRYPTO_TRANSFERRING: the crypto is on its wayTRANSACTION_CRYPTO_COMPLETEwhen it arrives, orTRANSACTION_CRYPTO_FAILED
paymentTransactionId. Show the transfer as complete only after TRANSACTION_CRYPTO_COMPLETE.
An order that ends before the provider accepts a send, for example a declined send or an expired code, has no transaction and sends no webhook. Read the order to see how it ended.
If the provider reports delivering an amount other than amount, the order stays at WAIT_FOR_PROVIDER until Meld resolves it.
Order status
Failure codes
failureCode tells you why an order went back a step or ended. With AUTHORIZE_ACCOUNT, the customer links again: send PREPARE_ACCOUNT_AUTHORIZATION with a new key. With START_NEW_ORDER, this order is over; create a new one. The list can grow, so show generic copy for a code you do not know.
Account links
A link belongs to the customer, not to an order. While it works, the customer’s later orders start atSELECT_SOURCE_ACCOUNT and the provider’s page does not open again, so to switch the customer to another account, unlink first.
An account can be linked to only one of your customers at a time. When a customer links an account that another of your customers already linked, the link moves to the customer who just approved access. The other customer links again if they need it.
The link stays put, and linking returns ACCOUNT_ALREADY_LINKED, while either of these holds:
- The other customer has a send on it that is not resolved yet
- Meld is still removing that link at the provider
Unlink
Endpoint:DELETE /crypto/order/headless/transfer/account-links/COINBASEPAY?customerId={customerId}
Call it with your API key and Meld’s customerId. It does not accept externalCustomerId. Meld removes its access at the provider first.
Limits
- Coinbase send limit. Meld asks Coinbase to cap sends on each link at 1,000 USD per day, and Coinbase refuses sends over it
- Order creation. A customer can create up to 10 transfer orders per hour. More return
429 TRANSFER_RATE_LIMITED
Test in sandbox
In sandbox, Meld simulates the provider:- Orders return
providerMode: SIMULATED. No real account or funds are involved verification.urlpoints at Meld’s sandbox. Opening it redirects straight to yourredirectUrlwith a handle, and no provider page appears- Each customer gets their own simulated user, so you cannot test a link moving between customers
- Sends stay pending until you settle them
LIST_SOURCE_ACCOUNTS returns two simulated USDC accounts: USDC Wallet (250.00, eligible for amounts up to 250) and USDC Vault (0.00, never eligible). Sandbox does not decline a send from USDC Vault. To test a decline, use an amount ending in 4.
Pick a scenario with the amount
The last digit ofamount picks the scenario. Trailing zeros after the decimal point are ignored, so 1.50 counts as 1.5 and ends in 5.
Scenarios 5 and 6 happen while linking, so they need a customer with no link. Unlink first, or use a new customer. Running scenario 7 on two orders for one customer within 24 hours locks the link with
SECOND_FACTOR_LOCKED.
Spread scenario runs across customers: the order limit applies in sandbox too.
Settle a simulated transfer
Endpoint:POST /payments/mock/headless/simulate
This endpoint exists only in sandbox.
Go live
In production, orders returnproviderMode: LIVE: they act on the customer’s real account and move real funds. The provider has no test environment for transfers, so test failure paths in sandbox.