πŸ’Έ Melio Pay Flow β€” Integration Guide

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:

  1. Backend: send Melio the bill(s) you want to pay (one REST call), using your own IDs. Melio returns the Melio bill IDs.
  2. Frontend: initialize the SDK and open the Pay Flow with those Melio bill IDs.

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.

%%{init:{"theme":"dark"}}%% 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', ...)
%%{init:{"theme":"default"}}%% 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', ...)
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:

  • Your backend β†’ 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.
  • Your frontend β†’ initializes the SDK with that auth code and opens the Pay Flow iframe, where the user picks their funding source and the vendor's delivery method, then schedules and pays.

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:

  • New ID β†’ Melio creates it.
  • Existing ID, vendor details changed β†’ Melio updates the vendor.
  • Existing ID, unchanged β†’ Melio does nothing.

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:

  • Omit it and the user adds or picks it in the Pay Flow.
  • Send it as 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:

  • An API key (used server-to-server only β€” never in the browser).
  • Your partner name.

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 β†’ the API-Key request header.

Both are server-to-server only β€” never expose your API key in the browser.

On your side:

  • A backend that can mint a short-lived authorization code per user (see Β§7).
  • An HTTPS page to host the flow (required for the iframe).
  • A modern browser (ES2020+).
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>:"). (With curl, 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.organizationId and data.account.userId β€” you pass these to POST /v2/auth/code in 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 billsSyncFailed error 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-Key request 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-Key header). Never use a production key in the browser.

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:

  1. Backend: POST /v2/auth/code { organizationId, userId } β†’ { code } (both IDs come from the prepare response).
  2. Pass the code to your frontend.
  3. Frontend: melioSDK.init(code, { partnerName, environment }).
  4. The SDK exchanges it for a session and fires authenticationSucceeded.
  5. On 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"));

entryPoint does not route the user. The completed event 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.


  • [ ] Received API key + partner name from Melio; using a non-production environment.
  • [ ] Backend mints a fresh, short-lived auth code per user.
  • [ ] POST /pay-flow/prepare sends the full vendor + bill state with an Idempotency-Key, using your own IDs.
  • [ ] You use the Melio bill IDs returned by prepare when opening the flow.
  • [ ] Bills are USD, not paid/draft, amount 0 < x <= 1,000,000.
  • [ ] Changed/repeat payments use a new bill (no editing of existing bills).
  • [ ] Container <div> exists before calling openPayFlow.
  • [ ] init succeeds (authenticationSucceeded) before opening a flow.
  • [ ] Handling 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.