For: Partner developers embedding Melio payments.
Companion SDK: @melio-eng/web-sdk β full SDK reference: https://melio.github.io/web-sdk/
Melio Pay Flow lets you embed a fully managed payment experience inside your own product. Your users stay in your UI; when it's time to pay a bill, Melio renders a secure, embedded flow that handles everything money-related β selecting a payment method, choosing how the vendor gets paid, scheduling, compliance, and execution.
You keep owning your bills and vendors. You never rebuild payments. The integration is two simple steps:
That's the whole loop. The user picks their payment method (funding source) inside the Pay Flow. The vendor's delivery method is picked there too β unless you already have the vendor's bank details, in which case you can send them with the vendor (see Β§3).
You can also reuse Melio's Payments Dashboard, Settings, and Onboarding flows the same way (see Β§10).
The integration spans your backend, your frontend, and Melio. Your backend talks to Melio over REST (with your API key); your frontend talks to Melio through the SDK's secure iframe.
sequenceDiagram
autonumber
participant BE as Your Backend
participant FE as Your Frontend (browser)
participant M as Melio
Note over BE,M: REST over HTTPS (API key) - server-to-server
BE->>M: POST /pay-flow/prepare (account, vendor, bills - your own IDs)
M-->>BE: 200 OK - Melio bill IDs + organizationId, userId
BE->>M: POST /v2/auth/code (organizationId, userId)
M-->>BE: 200 OK - auth code
BE-->>FE: Pass auth code + Melio bill IDs to the browser
Note over FE,M: Secure iframe - in the browser
FE->>M: sdk.init(authCode, partnerName, ...)
M-->>FE: authenticationSucceeded
FE->>M: sdk.openPayFlow(billIds, containerId)
M-->>FE: Pay Flow iframe renders - user picks funding source and delivery method, schedules and pays
M-->>FE: flow.on('completed', ...)In words:
POST /pay-flow/prepare creates/updates your data in Melio (keyed by your IDs) and returns the Melio bill IDs, plus the organizationId / userId you use to mint an auth code via POST /v2/auth/code.Every entity you send β account, vendor, bill β is referenced by your own ID, so you don't have to track Melio's IDs in your system. In return, prepare gives you the Melio bill IDs, which you pass to the SDK when opening the flow. Using the returned Melio bill IDs confirms the bill was actually created and synced before you launch the payment.
On each payment, send the complete current details of the vendor and bill. You don't need to check whether they already exist in Melio:
This keeps Melio's copy of your data correct at the moment that matters β right before the payment β and makes your integration stateless and safe to retry.
Melio does not patch an existing bill. If a bill's amount or details change, or the bill was already paid, create a new bill (a new ID) and open the Pay Flow with it. Re-sending an existing bill ID with different values will not change the bill.
You do not send the user's bank accounts or cards through the API β inside the embedded Pay Flow, the user selects their funding source (how they pay).
The vendor's delivery method (how the vendor receives funds) is optional:
vendor.deliveryMethod if you already hold the vendor's bank details. Currently only bank-account (ACH) is supported; more types may be added later, each identified by its type.A vendor keeps one delivery method per type. Re-sending the same bank details leaves it unchanged; sending different details replaces the vendor's existing bank account.
You'll need from Melio:
Authentication note. The two backend endpoints authenticate differently:
POST /v1/pay-flow/prepareβ HTTP Basic auth:Authorization: Basic base64("<YOUR_API_KEY>:")(API key as username, empty password).POST /v2/auth/codeβ theAPI-Keyrequest header.Both are server-to-server only β never expose your API key in the browser.
On your side:
| Environment | SDK environment |
API base URL |
|---|---|---|
| Production | production (default) |
https://partnerships.melioservices.com |
| Staging | staging01 |
https://partnerships.staging01.melioservices.com |
| Public QA | public-qa |
https://partnerships.public-qa.melioservices.com |
| Certification | certification |
https://partnerships.certification.melioservices.com |
| Localhost | localhost |
http://localhost:3005 |
Send the account (optional), vendor, and bill(s) in one call, using your own IDs. Melio creates or updates them.
POST /v1/pay-flow/prepare
Authorization: Basic <base64 of "<YOUR_API_KEY>:">
Idempotency-Key: <unique-key-per-attempt>
Content-Type: application/json
Auth. The prepare endpoint uses HTTP Basic auth with your API key as the username and an empty password β i.e.
Authorization: Basic base64("<YOUR_API_KEY>:"). (Withcurl, that's simply-u "<YOUR_API_KEY>:".)
{
"account": { // optional β include to create/resolve the org + user
"id": "partner-org-001",
"company": {
"name": "Acme Plumbing LLC",
"phoneNumber": "+14155550123",
"address": { "line1": "123 Market St", "city": "San Francisco", "state": "CA", "postalCode": "94103" }
},
"user": {
"firstName": "Dana",
"lastName": "Rivera",
"email": "dana@acmeplumbing.example",
"phoneNumber": "+14155550124"
}
},
"vendor": {
"id": "partner-vendor-7781",
"name": "City Power & Light",
"contact": {
"name": "Billing Dept",
"email": "ap@citypower.example",
"phoneNumber": "+18005551212"
},
"address": { "line1": "500 Grid Ave", "city": "San Francisco", "state": "CA", "postalCode": "94110" },
"deliveryMethod": { // optional β omit to let the user pick it in the Pay Flow
"type": "bank-account",
"details": { "accountType": "checking", "routingNumber": "011000015", "accountNumber": "000123456789" }
}
},
"bills": [ // one or more bills to pay
{
"id": "partner-bill-55012",
"vendorId": "partner-vendor-7781",
"amount": 482.75,
"currency": "USD",
"dueDate": "2026-07-15",
"invoice": { "number": "INV-3391", "date": "2026-06-20" },
"note": "June electricity"
}
]
}
Response 200 β returns the Melio IDs for each entity (alongside your own IDs) and a per-entity status.
{
"data": {
"account": { "id": "acc_9f8cβ¦", "externalId": "partner-org-001", "organizationId": "org_1234", "userId": "usr_5678", "status": "created" },
"vendor": { "id": "vnd_β¦", "externalId": "partner-vendor-7781", "status": "updated" },
"deliveryMethod": { "id": "dmtd_β¦", "type": "bank-account", "status": "created" }, // only when sent
"bills": [ { "id": "bill_abc123", "externalId": "partner-bill-55012", "status": "created" } ]
}
}
Take
data.bills[].idβ these are the Melio bill IDs you pass to the SDK in Step 4. Using them ensures the bill was created and synced before launching the flow.Also take
data.account.organizationIdanddata.account.userIdβ you pass these toPOST /v2/auth/codein Step 2 to mint the SDK auth code.Bill rules. Each bill must be USD, not already paid, not a draft, and have an amount greater than 0 and at most $1,000,000. Bills that violate these are rejected here (and would otherwise surface as a
billsSyncFailederror in the SDK).
Using the organizationId and userId returned by prepare, mint a short-lived, user-scoped auth code. Pass the returned code (with the Melio bill IDs) to your frontend.
POST /v2/auth/code
API-Key: <YOUR_API_KEY>
Content-Type: application/json
{ "organizationId": "org_1234", "userId": "usr_5678" }
Auth. The auth-code endpoint uses the
API-Keyrequest header (not Basic auth).
{ "data": { "code": "eyJhbGciβ¦" } }
The code identifies the user to the SDK. It's single-use and short-lived β generate a fresh one per session and never expose your API key to the browser.
npm install @melio-eng/web-sdk
import { MelioSDK } from "@melio-eng/web-sdk";
const melioSDK = new MelioSDK();
const init = melioSDK.init("AUTH_CODE_FROM_YOUR_BACKEND", {
partnerName: "your-partner-name", // required
environment: "production", // optional β defaults to 'production'
keepAlive: true, // optional β keeps the session warm
});
init.on("authenticationSucceeded", () => { /* ready */ });
init.on("authenticationFailed", () => { /* re-mint the auth code and retry */ });
Make sure the container element exists on the page first:
<div id="melio-payflow-container" style="width:100%;height:720px"></div>
const payFlow = melioSDK.openPayFlow({
containerId: "melio-payflow-container", // DOM element id
billIds: ["bill_abc123"], // Melio bill IDs returned by /pay-flow/prepare
billIdType: "melio", // these are Melio bill IDs (opens the payment screen directly)
});
payFlow.on("loaded", () => console.log("Flow rendered"));
payFlow.on("completed", (data) => console.log("Payment done:", data)); // data.flowName === "payment"
payFlow.on("error", (err) => console.warn("Flow error:", err.errorCode));
payFlow.on("exit", () => console.log("User closed the flow"));
That's the full integration. Repeat Step 1 on every payment with the latest details.
π§ͺ Try it live. The interactive API Reference & Sandbox lets you inspect the full schema and send real requests against the Public QA and Certification environments (enter your scoped test API key in the
API-Keyheader). Never use a production key in the browser.
POST /pay-flow/prepare| Aspect | Detail |
|---|---|
| Auth | Authorization: Basic base64("<YOUR_API_KEY>:") β API key as the Basic-auth username, empty password (server-to-server only) |
| Idempotency | Send Idempotency-Key; retries return the same result |
| Body | account?, vendor, bills[] β all referenced by your own IDs |
| Returns | 200 with per-entity id (Melio) + externalId β’ status. The account result also includes organizationId and userId for POST /v2/auth/code. A deliveryMethod result (id, type, status) is returned when you sent one. |
Field notes
| Field | Notes |
|---|---|
account |
Optional. Include to create or resolve the organization + user. |
account.id |
Your ID for the org/user. If omitted on first creation, Melio generates one internally. |
vendor |
id, name, contact, address, and optionally deliveryMethod. |
vendor.deliveryMethod |
Optional. { "type": "bank-account", "details": { accountType, routingNumber, accountNumber } }. accountType is checking or savings; routingNumber is 9 digits; accountNumber is 4β17 digits. Only bank-account is supported today. |
bills[].id |
Your bill ID. Melio returns the corresponding Melio bill ID in the response, which you pass to the SDK. |
bills[].vendorId |
Your vendor ID (matches vendor.id). |
bills[].amount |
Number. Must be > 0 and <= 1000000. |
bills[].currency |
Must be "USD". |
bills[].dueDate |
YYYY-MM-DD. |
Reconciliation behavior
| Situation | Result |
|---|---|
| Vendor ID not seen before | Vendor created |
| Vendor ID exists, details changed | Vendor updated |
| Delivery method sent, vendor has none of that type | Delivery method created (also for an existing vendor) |
| Same delivery method details re-sent | Delivery method unchanged (same Melio ID) |
| Different delivery method details sent | Vendor's existing delivery method of that type is replaced (updated, new Melio ID) |
| Delivery method omitted | Vendor's existing delivery methods are left as-is |
| Bill ID not seen before | Bill created |
| Bill ID exists | Not modified β create a new bill (new ID) to pay changed/again |
Completeness: send the full current state of the vendor on every call. Omitted fields are treated as "no change," not "clear."
Errors
{ "error": { "code": "ValidationError", "message": "β¦", "requestId": "β¦" } }
| Status | Meaning |
|---|---|
400 |
Validation error β bill issues (non-USD, draft/paid, amount out of range, missing required fields), vendor issues (missing name, invalid contact email/phone, invalid address) or delivery method issues (unsupported type, invalid routing/account number) |
403 |
API key not permitted for this operation/org |
404 |
Referenced entity not found |
409 |
Conflict β your ID already maps to a different entity |
500 |
Server error β safe to retry with the same Idempotency-Key |
The error response identifies which entity failed (e.g. vendor/bill) and why, so you can correct the payload.
The SDK's init(authCode, β¦) needs a short-lived code that identifies the end user. Mint it on your backend with POST /v2/auth/code, using the organizationId and userId returned by prepare:
POST /v2/auth/code { organizationId, userId } β { code } (both IDs come from the prepare response).code to your frontend.melioSDK.init(code, { partnerName, environment }).authenticationSucceeded.authenticationFailed, mint a new code and retry.Generate a fresh code per session. Never cache or share it across users, and never expose your API key in the browser.
melioSDK.init(authenticationCode: string, options: InitOptions): InitFlowInstance
InitOptions
| Field | Type | Required | Notes |
|---|---|---|---|
partnerName |
string |
β | Your Melio partner name |
environment |
Environment |
β | production (default), staging01, public-qa, certification, localhost |
keepAlive |
boolean |
β | Keeps the session alive via a hidden iframe (avoids repeat auth handshakes) |
branchOverride |
string |
β | Advanced/testing only |
InitFlowInstance: on("authenticationSucceeded" | "authenticationFailed"), off(...), close().
melioSDK.openPayFlow(config: PayFlowConfig): FlowInstance
| Field | Type | Required | Notes |
|---|---|---|---|
containerId |
string |
β | Id of a DOM element to render the iframe into |
billIds |
string[] |
β | Melio bill IDs returned by /pay-flow/prepare |
billIdType |
"melio" | "accounting" |
β | For this prepare-based flow use "melio" β the bills already exist in Melio, so the payment screen opens directly. Defaults to "accounting" (bills imported from a connected accounting platform). |
authCode |
string |
β | Per-flow auth code override |
entryPoint |
ExternalEntryPoint |
β | Which page of your product the user came from. Shapes the return-button wording β see Entry point. |
PayFlowConfig, JustPayFlowConfig and OnboardingConfig accept an optional entryPoint β which
page of your product the user came from. Melio uses it to word the button that ends the flow.
entryPoint |
return button reads |
|---|---|
"bills" |
"Back to bills" |
"contacts" |
"Back to contacts" |
"quickpayment" |
"Back to quick payment" |
"managepayments" |
"Back to manage payments" |
"home" |
"Back to homepage" |
"generic" |
"Finish and return" |
| omitted | today's default wording |
Use "generic" when your page has no single destination.
const payFlow = melioSDK.openPayFlow({
containerId: "melio-payflow-container",
billIds: ["ext-bill-1"],
entryPoint: "bills",
});
// Wording only β you still navigate:
payFlow.on("completed", () => router.push("/bills"));
entryPointdoes not route the user. Thecompletedevent carries no destination, so your application decides where the click lands.
On the Pay Flow, entryPoint has no effect when billIdType is "melio".
Every flow returns a FlowInstance with on(event, cb), off(event, cb), close().
| Event | Payload | Meaning |
|---|---|---|
loaded |
β | Iframe rendered |
completed |
{ flowName: "payment" | "onboarding", β¦ } |
Flow finished successfully |
navigated |
{ target, targetAction } |
Internal navigation occurred |
error |
{ errorCode: "billsSyncFailed" } |
A flow error occurred |
exit |
β | User closed the iframe |
authenticationSucceeded |
β | Session established |
authenticationFailed |
β | Auth/token exchange failed |
billsSyncFailed means a bill couldn't be loaded into the flow β it was non-USD, already paid, a draft, or had an unsupported amount (0 or over $1,000,000). Enforcing the Β§5 bill rules when you build the payload prevents this.
Call flow.close() to tear down the iframe and listeners when you remove the flow from your UI.
Because you own the data, Melio's copy is only as current as your last prepare call β and that's all you need, because you call prepare right before every payment.
| Scenario | What happens |
|---|---|
| Vendor name / contact / address changed | Picked up automatically on the next prepare. |
| Vendor bank details changed | Send the new vendor.deliveryMethod on the next prepare β it replaces the old bank account. |
| Bill amount / details changed before paying | Create a new bill (new ID) and open that β existing bills are not edited. |
| Bill already paid | Create a new bill for the next payment. |
| Same vendor used by several bills | Sending the vendor updates it for all pending payments to that vendor. |
You don't manage the user's payment methods through the API β the user sets those in the flow. Vendor delivery methods are managed in the flow too, unless you choose to send them with vendor.deliveryMethod.
Beyond Pay Flow, the same SDK + initialization gives you Melio's other embedded experiences. These are launched exactly like Pay Flow (initialize once, then open⦠into a container). Full configuration details are in the SDK reference: https://melio.github.io/web-sdk/
| Flow | Method | Use it for |
|---|---|---|
| Payments Dashboard | melioSDK.openPaymentsDashboard({ containerId, paymentId? }) |
Let users view and manage their payments (status, history, a specific payment) inside your product. |
| Settings | melioSDK.openSettings({ containerId }) |
Host Melio account settings (the "manage payments" entry point) within your product. |
| Onboarding | melioSDK.openOnboarding({ containerId, userDetails?, organizationDetails? }) |
Collect any missing account/KYC information directly in Melio. |
Onboarding options. If you already collect the required KYC data, you can provision the account via the prepare / account API and skip the UI. If some details are missing, use the Onboarding flow so the user can complete them in Melio.
Support / chat. A support entry point can be made available within the Settings/Dashboard area for your partner. Availability is configured per partner β confirm with your Melio partnerships representative.
environment.POST /pay-flow/prepare sends the full vendor + bill state with an Idempotency-Key, using your own IDs.prepare when opening the flow.0 < x <= 1,000,000.<div> exists before calling openPayFlow.init succeeds (authenticationSucceeded) before opening a flow.completed, error (billsSyncFailed), exit, and authenticationFailed.class MelioSDK {
init(authenticationCode: string, options: InitOptions): InitFlowInstance;
openPayFlow(config: PayFlowConfig): FlowInstance;
openPaymentsDashboard(config: PaymentsDashboardConfig): FlowInstance;
openSettings(config: SettingsConfig): FlowInstance;
openOnboarding(config: OnboardingConfig): FlowInstance;
}
interface InitOptions {
partnerName: string;
environment?: "production" | "staging01" | "public-qa" | "certification" | "localhost";
keepAlive?: boolean;
branchOverride?: string;
}
type ExternalEntryPoint =
| "bills" | "contacts" | "quickpayment"
| "managepayments" | "home" | "generic";
interface PayFlowConfig {
containerId: string;
billIds: string[]; // Melio bill IDs from /pay-flow/prepare
billIdType?: "accounting" | "melio"; // use "melio" for the prepare-based flow; default "accounting"
authCode?: string;
entryPoint?: ExternalEntryPoint; // page the user came from; see Β§8 "Entry point"
}
type FlowEventType =
| "completed" | "exit" | "navigated"
| "authenticationSucceeded" | "authenticationFailed"
| "error" | "loaded";
interface FlowInstance {
on(event: FlowEventType, cb: (data?: any) => void): void;
off(event: FlowEventType, cb: (data?: any) => void): void;
close(): void;
}
Full SDK reference (all flows, configs, and events): https://melio.github.io/web-sdk/
Need an API key, partner name, or help? Contact your Melio partnerships representative.