Skip to main content
A customer links an account they hold at an exchange, then sends crypto from it to their own wallet. Coinbase is the provider today. Your backend creates the order and runs each step through Meld, and settlement arrives by webhook. There is no quote and no payment method. The only provider screen the customer sees is its sign-in and consent page, when they link their account. Every other screen is your UI.

Scope


Before you begin

  1. Ask your Meld representative to enable headless and transfers on your account, in sandbox first. Each environment is enabled separately
  2. Create the customer
  3. Configure webhooks. Transfers use the same transaction events as purchases
  4. Choose a redirectUrl. See Redirect URL
Run every call on this page from your backend. Your API key and each order’s action token are secrets, so the device never calls Meld’s API.

Flow overview


1. Find what can be transferred

Endpoint: GET /network-partner/supported/currencies
Each row is an asset the provider can send for your account. Use the row’s 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 https URL, 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, where ASWebAuthenticationSession cannot return to an https URL
Anything else is refused with 400 INVALID_REDIRECT_URL.

The response

Order creation returns 201:
Fields with no value are left out.

Retries and idempotency

Send a new X-Idempotency-Key for each new order.
  • A retry with the same key and the same body returns 200 with the order as first created and a new actionToken. Read the order for its current next
  • A retry while the first request is still running returns 425 with Retry-After: 2 and 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.endpoint on 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 Authorization headers is refused
  • Send version: 1, the operation, and that operation’s field, if it has one. Any other field is refused with 400 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’s next field has the same shape.

Steps

Branch on nextStep.
  1. 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.
  2. Open verification.url in a system browser session: ASWebAuthenticationSession on 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.
  3. The customer signs in to the provider and approves access. Meld then sends the browser to your redirectUrl with three query parameters added:
    If the customer declines, transferAuthorization is DECLINED, there is no handle, and the order’s next is AUTHORIZE_ACCOUNT with ACCOUNT_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, send PREPARE_ACCOUNT_AUTHORIZATION with a new key to try again.
  4. Your app passes handle to your backend. Your backend sends it as authorizationHandle:
    The handle works only with this order’s action token. Success returns SELECT_SOURCE_ACCOUNT with linkedAccount.displayName.

Choose the source account

Send LIST_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-Key and one sourceAccountId for SUBMIT_TRANSFER on 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 with 409 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_ACCOUNTS did not return is refused with 400 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 is COLLECT_SECOND_FACTOR:
Ask the customer for the code, then send it with a new key for each code:
  • The customer has up to 5 attempts, and 5 minutes to enter each code. secondFactor.expiresAt shows the current deadline. After that the order ends with SECOND_FACTOR_EXPIRED or SECOND_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.
After a 503 with status: UNKNOWN from SUBMIT_TRANSFER or SUBMIT_SECOND_FACTOR, the send may have gone through. Never create a new order for the same transfer. Poll READ_SUBMISSION until next moves on.

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 is CRYPTO_TRANSFER, and its id is the order’s transactionId. The standard webhook events then follow:
  1. TRANSACTION_CRYPTO_PENDING: the provider accepted the send
  2. TRANSACTION_CRYPTO_TRANSFERRING: the crypto is on its way
  3. TRANSACTION_CRYPTO_COMPLETE when it arrives, or TRANSACTION_CRYPTO_FAILED
Correlate each event on 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.
A link belongs to the customer, not to an order. While it works, the customer’s later orders start at SELECT_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
Retry once the send resolves, or after a few minutes. 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.url points at Meld’s sandbox. Opening it redirects straight to your redirectUrl with 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 of amount 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 return providerMode: 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.