Meld.ensureReady works through them in your app or web page, for every provider you offer at once.
Your backend makes one call to start a session. Meld plans each step and makes each call. The SDK runs the steps, shows the hosted pages, and asks your UI only for what the customer has to type or agree to. Providers are data in the session, so your code doesn’t branch on them, and your API key never leaves your backend.
Requirements sessions are enabled per account. In sandbox they are on for every account. For production, ask your Meld representative. Until they are on, creating a session answers
403 REQUIREMENTS_SESSIONS_NOT_ENABLED.How it works
- Your backend creates a session for the customer and the providers you offer, and returns it to your app unchanged.
- Your app runs it with
Meld.ensureReady, and handlers for what only your UI can do. On the web, the SDK’s calls go through a proxy on your backend. - You act on the result: create the order on
READY, or show the customer what is left.
1. Create a session on your backend
Endpoint:POST /crypto/onramp/requirements-sessions
Header: Authorization: BASIC <your API key>, from an API key that can create orders. The endpoint is unversioned: every Meld-Version answers the same.
Response
201:
token is a bearer for this customer, these providers and this order, valid for 30 minutes (expiresAt). Never log or store it. Create a new session each time the customer starts again.
Create sessions only for your signed-in customer: never expose an endpoint that creates one for whatever customerId its caller names.
2. Run it in your app
Configure the SDK for the environment your backend created the session in. The session names only a path, so the SDK sends its calls to the environment you configured, or on the web to your proxy, and can’t tell a session created in another environment: Meld refuses it with401, so the run ends EXPIRED, after one call to renewSession when you pass it.
Then pass the session to Meld.ensureReady with your handlers:
io.meld.sdk.requirements.
Handlers
On the web, a handler returns a Promise. On iOS and Android, it calls the completion it is given, once. On React Native, it returns a value or a Promise, and a
collect, confirmAgreement or renewSession that leaves a request unanswered for 10 minutes ends the run CANCELLED. presentUrl has no such limit, since a customer can take longer on a hosted page.
A step whose handler you leave out isn’t run: the run ends ACTION_REQUIRED, with the step in pending. The SDK never accepts terms on its own.
What collect receives
collect gets { stepId, type, code, serviceProviders, fields, reason?, fieldErrors? }. Each field is { key, label?, fieldType?, jsonSchema?, allowedValues? }. Collect every field of a request in one pass, and answer with exactly those keys. Meld checks each value before it calls anyone:
A provider’s questionnaire, such as Uphold’s due-diligence and tax questions, comes one part at a time. Each field is one questionnaire: its
key names it, and its jsonSchema is the JSON Schema of the one part to answer now, as text, with that part as its only property, for example {"type":"object","maxProperties":1,"properties":{"employment":{…}},"required":["employment"]}. Answer with the text of a JSON object that holds only that part and meets the schema, for example {"employment":{"status":"employed"}}. Never send a part again once it is answered. Meld checks only that the value is one JSON object, so check it against the schema yourself before you answer. The next read offers the next part, and collect is called again until the questionnaire is done. A questionnaire the provider gives no schema for, such as Uphold’s proof of address, is never a field: see What the SDK runs.
When Meld refuses a value, collect is called again for the same request, with fieldErrors: [{ key, reason }], where reason is REQUIRED for a key left out, INVALID for a value Meld refuses, or UNSUPPORTED for a key the step doesn’t take. It never carries the value. INVALID is also the answer when Meld can’t save a value the customer typed, so collect asks again rather than the run failing. The same goes for a contact or a code the customer typed. After the fourth refused answer to the same request, the run ends ACTION_REQUIRED with that step.
Verifying a contact
AVERIFY_CONTACT step has Meld send the customer a code, and asks for it back. Take a customer with ada@example.com on file and no phone, and Coinbase, which needs both verified. collect is called four times:
The run then ends
READY.
Meld sends the code to the contact on file. It takes a contact the customer types only while the customer has none for that channel, and saves it on the customer once the customer confirms its code. It never replaces a contact on file: if a different one is put on file while the customer types, Meld refuses the typed one with UNSUPPORTED. A code that can no longer be confirmed, because it expired, was used, or a newer code replaced it, isn’t asked for again: the SDK reads again, and the run ends ACTION_REQUIRED with the step. Run ensureReady again, and it sends a new code.
What the SDK runs
Each step comes from a checklist step of one or more providers, and itskind says what running it takes:
A step the providers share runs once: one email for all of them, and Unified KYC once, at the highest level among them. A step that belongs to a provider, such as its terms or its onboarding, runs for that provider. Meld never renders a form: your UI asks every question, including a provider’s questionnaire, from the fields in
collect.
Meld adds the customer and the order to every call, from the session. It records a terms acceptance itself, with its own clock and the IP address the call came from: on the web, your proxy’s (see On the web, proxy the calls). A contact the customer types is saved on the customer only once they confirm its code: see Verifying a contact.
The SDK doesn’t run:
WAITsteps, such as a review. It reads again until they clear: see Waiting on a review.UNKNOWNsteps. Meld answers them on a later read.- Steps no app can run: a detail only the provider’s own page collects, a provider’s questions that include a document, such as Banxa’s proof of address (a field whose
fieldTypeisOBJECT), for which the SDK has no upload, or a questionnaire the provider gives no schema for, such as Uphold’s proof of address.providers[].openlists them. When nothing else is left to run or wait for, the run endsACTION_REQUIREDat once, with an emptypending. - Steps that run inside the order (
ORDER_CONTINUATION). The result lists them incontinuation: see READY, then the order.
Hosted pages
Unified KYC, a provider’s hosted onboarding or terms, and a provider’s form page are pages the customer completes in a browser. Closing a page never counts as done: the next read decides.- A provider’s onboarding or terms link that expires within a minute is refreshed before the page opens. That includes a link that aged while the web or iOS prompt was open: the SDK refreshes it at the tap and opens the new one, and never asks the customer twice.
- When the customer closes such a page, or it fails to load, the SDK refreshes the step once, which also tells Meld what the customer did there, then reads. A
409on that refresh isn’t an error: the SDK reads as usual. A401on it callsrenewSessionand sends the refresh once more on the new session, without opening the page again; a second401ends the runEXPIRED. - When the SDK closes the page itself, because Meld reports the step done, it only reads.
- Unified KYC’s page and a provider’s form page come from running a step, and are never refreshed: the SDK reads once they close.
- A page closed with its step unfinished isn’t opened again while the checklist stays the same apart from its link: if the step is still left 30 seconds later, the run ends
ACTION_REQUIREDwith that step.
On every platform,
presentUrl replaces the SDK’s presenter: show url your way, and answer once the page is closed. On the web, without onPrompt, host or presentUrl, the SDK can’t open a page, Unified KYC’s included, so those steps end the run ACTION_REQUIRED.
3. Act on the result
On the web and React Native,handle.result is a Promise that never rejects. On iOS and Android, onResult is called once, on the main thread.
The result has the same members on every platform:
status, kind and the step types are open sets. Never log pending: a step’s url opens the customer’s own page.
READY, then the order
READY means every step before the order is done. Create the order with POST /crypto/order/headless/onramp, with the values of the session’s order, then mount it. For a virtual account or an offramp, see Virtual accounts and offramps.
READY doesn’t mean nothing is left. continuation lists the steps a provider declares to run inside the order: today only Stripe’s identity check (nextStep: SDK_VERIFY_IDENTITY). Other providers’ own pages, such as Mercuryo’s widget or Coinbase’s payment page, aren’t listed: they open when you mount the order. The SDK runs all of them once you mount the order, and they need nothing from you before you create it.
If order creation still refuses, see When order creation refuses: create a new session and run ensureReady again.
Virtual accounts and offramps
A session also gets a customer ready for a virtual account (deposit instructions) or an offramp, for example with Noah or Due Network: their hosted onboarding and terms areOPEN_URL steps the SDK runs. Create the session without order: it describes a headless onramp order, and with it Meld also checks that the order can be created, which deposit instructions and a sell order don’t need. On READY, fetch the deposit instructions, or create the sell order.
Noah and Due Network also take headless onramp orders. To get a customer ready for one, create the session with order, as for any provider, and create the order on READY.
Waiting on a review
While only reviews are left, such as Unified KYC’s or a provider’s, the SDK reads again as often as Meld asks, every 2 to 30 seconds. Whatever Meld answers, it reads at most once a second and at least once a minute, and every 4 seconds when Meld doesn’t say. It waits forwaitTimeoutMs, 10 minutes by default, then ends PENDING. Set it in the options: { waitTimeoutMs } on the web and React Native, MeldReadyOptions(waitTimeoutMs: …) on iOS, MeldReadyOptions(waitTimeoutMs = …) on Android.
Your backend learns a review’s outcome from the CUSTOMER_KYC_STATUS_CHANGE webhook. If you relay it to the app, for example through a push notification or your own socket, call handle.refresh(): the SDK reads at once instead of at the next poll. It ends a wait, never the delay before a retry, and on Android it does nothing while a page is open. After PENDING, run ensureReady again on the next webhook, with a new session once this one has expired.
handle.cancel() stops the run and ends it CANCELLED, even while a handler waits for an answer or a page is open. No handler is called after the result, even when onProgress itself calls cancel().
Several providers in one session
Name up to five providers. The steps they share run once, Unified KYC at the highest level among them, so the customer verifies once for all of them.- With
readyWhen: ALL, the result isREADYwhen every provider is, andBLOCKEDas soon as one is.providerssays which: create the next session without it. - With
readyWhen: ANY, the result isREADYwhen one provider is, andBLOCKEDonly when all are.providerssays which are ready: create the order with one of them.
Errors
Before it gives up, the SDK retries the same way on every platform:
On the web and iOS, the SDK reads every
pollAfterMs while a page is open, only to decide when to close it. A read that fails there is ignored: it isn’t retried, doesn’t call renewSession and doesn’t count toward any limit, and the next read comes at the next poll. The limits above count from the refresh or read after the page closes. Android doesn’t read while a page is open.
{ kind: 'protocol' } means the SDK couldn’t use the session or read Meld’s answer. On React Native it also means the app binary predates the native support, or on Android that no ComponentActivity is on screen.
{ kind: 'http' } carries the HTTP status and, when Meld sent one, the code:
On the web, proxy the calls
The SDK posts each read and each step to the session’srequirementsActions.endpoint, with the session token as the bearer. The Meld API accepts these calls from a browser only on Meld’s own origins, so a page on your origin sends them through a proxy on your backend. Configure its path once:
location.origin + actionProxyUrl + endpoint, for example https://your.app/api/meld/crypto/onramp/requirements-sessions/Ny9YeB5gTh8WaQc3DrUf7k/actions. actionProxyUrl is a path on your page’s origin. Native apps call Meld directly, and need no proxy.
The proxy must:
- Accept
POST {actionProxyUrl}/crypto/onramp/requirements-sessions/{sessionId}/actions, wheresessionIdis 1 to 64 base58 characters (^[1-9A-HJ-NP-Za-km-z]{1,64}$), and forward it to the same path on the Meld API:https://api-sb.meld.ioin sandbox,https://api.meld.ioin production. Forward no other path, so the proxy can’t become an open relay. - Forward only the
Authorization,Content-TypeandX-Idempotency-Keyheaders. Don’t add your API key: the session token is the credential. Don’t forwardOrigin,CookieorReferer. - Cap the request body at 64 KiB, follow no redirects, and return Meld’s status and body unchanged, with
Cache-Control: no-store. An answer can be up to 256 KiB. - Never log a body in either direction. They carry links to the customer’s hosted pages and the details the customer typed.
X-Meld-End-User-Ip header with the customer’s IP address. Meld uses it only as evidence for terms the customer accepts, recorded as MELD_SDK_PROXY, and only once Meld enables it for your account: ask your Meld representative. Until then, or when the header isn’t one IP address, Meld records the address the call came from, which is your proxy’s.
A hosted verification on a refused order
Some refusals ofPOST /crypto/order/headless/onramp come with a page where the customer can finish verifying, in serviceProviderDetails.hostedVerificationUrl. Mercuryo’s 403 KYC_NOT_COMPLETED for a customer new to Mercuryo in the US is one. Open it with Meld.openHostedVerification from the customer’s tap, then create the order again with a new externalOrderId. The refused order stays refused.
Closing the page doesn’t mean the customer passed: the next order creation decides. Never log or store the URL.
Reference
The SDKs never log the session token, a hosted page’s link, or anything
collect returns.