# Add a Deposit Instruction to an Account Source: https://docs.mnai.com/api-reference/accounts/add-a-deposit-instruction-to-an-account /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/accounts/{accountId}/deposit-instructions Starts provisioning a new, named deposit instruction on an existing account. Funds received on it are swept into the account automatically. Responds `202 Accepted`; the instruction appears in `GET /v1/accounts/{accountId}` once provisioned. A retry that reuses the same `idempotencyKey` with a different `name` does not rename the instruction: the name from the first request wins. For stablecoin accounts, the new address also appears on your other stablecoin accounts for the assets and networks it supports. # Get Account by ID Source: https://docs.mnai.com/api-reference/accounts/get-account-by-id /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/accounts/{accountId} Returns the account details for the authenticated Meridian account. # Get Account Statement Source: https://docs.mnai.com/api-reference/accounts/get-account-statement /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/accounts/{accountId}/statement Returns a statement of the account's activity over a period, formatted as JSON, CSV, or PDF. JSON is returned inline; CSV and PDF are rendered and uploaded, and this endpoint responds with a time-limited download link instead of the file itself. Statements are available from 2026-01-01 onwards, the date balance history began being recorded; a period starting before that floor is rejected. # List Accounts Source: https://docs.mnai.com/api-reference/accounts/list-accounts /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/accounts Returns the accounts associated with the authenticated Meridian account. # Create a Meridian user for a partner Source: https://docs.mnai.com/api-reference/auth/create-a-meridian-user-for-a-partner /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/auth/users Creates a person or business user for the calling partner on the supplied program. Only MULTI_USER integrations may call this endpoint; SINGLE_USER keys are rejected. # Issue a Meridian Accounts access token Source: https://docs.mnai.com/api-reference/auth/issue-a-meridian-accounts-access-token /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/auth/token Mints a JWT for a Meridian user. For `client_credentials` grants, the user is identified by the `X-Meridian-User-Id` header: it must be provided for MULTI_USER integrations and omitted for SINGLE_USER integrations (the single configured user is used). For `refresh_token` grants, the user is derived from the supplied refresh token. # Add an Instrument to a Beneficiary Source: https://docs.mnai.com/api-reference/beneficiaries/add-an-instrument-to-a-beneficiary /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/beneficiaries/{beneficiaryId}/instruments Attaches an additional payout instrument to an existing beneficiary for the authenticated Meridian account. A bank account (`BANK_ACCOUNT`) or a stablecoin wallet address (`BLOCKCHAIN_WALLET`). One wallet address may be registered once per supported currency, each as its own instrument. # Create a Beneficiary Source: https://docs.mnai.com/api-reference/beneficiaries/create-a-beneficiary /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/beneficiaries Creates a beneficiary together with its first payout instrument for the authenticated Meridian account. The instrument is either a bank account (`BANK_ACCOUNT`) or a stablecoin wallet address (`BLOCKCHAIN_WALLET`); further instruments are attached with `POST /v1/beneficiaries/{beneficiaryId}/instruments`. # Get Beneficiary by ID Source: https://docs.mnai.com/api-reference/beneficiaries/get-beneficiary-by-id /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/beneficiaries/{beneficiaryId} Returns the beneficiary details for the authenticated Meridian account. # List Beneficiaries Source: https://docs.mnai.com/api-reference/beneficiaries/list-beneficiaries /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/beneficiaries Returns the beneficiaries associated with the authenticated Meridian account. # List Beneficiary Instruments Source: https://docs.mnai.com/api-reference/beneficiaries/list-beneficiary-instruments /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/beneficiaries/{beneficiaryId}/instruments Returns the payout instruments for a beneficiary belonging to the authenticated Meridian account, bank accounts and stablecoin wallets alike, discriminated by `type`. # Activate a pending enrollment Source: https://docs.mnai.com/api-reference/enrollment/activate-a-pending-enrollment /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/enrollment/activate Creates or resumes the enrollment for the authenticated Meridian account and returns the current enrollment state. # Get current enrollment status Source: https://docs.mnai.com/api-reference/enrollment/get-current-enrollment-status /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/enrollment Returns the enrollment for the authenticated Meridian account, creating one on the first call if none exists yet. # Create or update Information Request requirement entry data Source: https://docs.mnai.com/api-reference/information-requests/create-or-update-information-request-requirement-entry-data /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/information-requests/{informationRequestId}/data Writes structured data for one or more requirement entries. # Get an Information Request by ID Source: https://docs.mnai.com/api-reference/information-requests/get-an-information-request-by-id /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/information-requests/{informationRequestId} Returns the Information Request details for the authenticated Meridian account. # List all Information Requests Source: https://docs.mnai.com/api-reference/information-requests/list-all-information-requests /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/information-requests Returns the Information Requests associated with the authenticated Meridian account. # Submit an Information Request for review Source: https://docs.mnai.com/api-reference/information-requests/submit-an-information-request-for-review /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/information-requests/{informationRequestId}/submit Submits the Information Request for the authenticated Meridian account. A `validationStatus` of `VALID` covers every rule checkable from the Information Request itself; submission additionally checks it against existing data (for example whether the business name is already registered) and reports any such rejection in `validationErrors`. # Upload and attach an Information Request document Source: https://docs.mnai.com/api-reference/information-requests/upload-and-attach-an-information-request-document /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/information-requests/{informationRequestId}/documents Uploads a document for a requirement entry and stores it. # Submit a payout request Source: https://docs.mnai.com/api-reference/payouts/submit-a-payout-request /products/payout-origination/api-reference/payout-origination.json post /v1/payout-origination/{format} Accepts a Base64-encoded payout request in the format specified by the `format` path parameter. Supported values: - `pacs008` for ISO 20022 `pacs.008.*` XML messages - `json` for a JSON payload where `entityId`, `beneficiaryId`, `amount`, `currency`, and `reference` are all required Authentication uses Meridian HMAC headers: - X-Meridian-Api-Key - X-Meridian-Timestamp - X-Meridian-Signature Use Idempotency-Key as a separate header for duplicate protection. The endpoint returns 202 Accepted once the encoded payload has been safely accepted for processing. Webhooks will be sent for each status change until a finality status. The transaction APIs may be used to list resulting transactions and retrieve the latest status on demand. # Send a Mock Deposit Source: https://docs.mnai.com/api-reference/sandbox/send-a-mock-deposit /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/accounts/{accountId}/simulate-deposit Simulates an inbound deposit into a balance account through a given deposit instruction, so you can exercise downstream processing — balances, transactions, and webhooks — without moving real money. This is fire-and-acknowledge: it responds `202` with an empty body and settles out of band, so observe the result by polling the account balance or transactions. > **Sandbox only.** This endpoint exists exclusively in the sandbox environment for testing. Simulating a deposit is not possible in production. # Commit a transaction intent Source: https://docs.mnai.com/api-reference/transaction-intents/commit-a-transaction-intent /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/transaction-intents/{transactionIntentId}/commit Commits a created transaction intent. Commit is idempotent and creates at most one underlying Meridian transaction. # Create a transaction intent Source: https://docs.mnai.com/api-reference/transaction-intents/create-a-transaction-intent /products/meridian-accounts/api-reference/meridian-accounts.json post /v1/transaction-intents Creates a transaction-shaped preview from debit and credit payment instruments. The credit instrument may be a beneficiary bank account or a beneficiary blockchain wallet; a stablecoin payout must debit and credit the same currency. No funds movement occurs until the intent is committed. # Get a payout transaction Source: https://docs.mnai.com/api-reference/transactions/get-a-payout-transaction /products/payout-origination/api-reference/payout-origination.json get /v1/transactions/{id} Returns a transaction with its latest processing status by Meridian transaction ID. Authentication uses Meridian HMAC headers: - X-Meridian-Api-Key - X-Meridian-Timestamp - X-Meridian-Signature # Get Transaction by ID Source: https://docs.mnai.com/api-reference/transactions/get-transaction-by-id /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/transactions/{transactionId} Returns the transaction details for the authenticated Meridian account. # List payout transactions Source: https://docs.mnai.com/api-reference/transactions/list-payout-transactions /products/payout-origination/api-reference/payout-origination.json get /v1/transactions Returns transactions with their latest processing status. You can optionally filter the results by the Meridian `messageId` returned from the message submission endpoint. Authentication uses Meridian HMAC headers: - X-Meridian-Api-Key - X-Meridian-Timestamp - X-Meridian-Signature # List Transactions Source: https://docs.mnai.com/api-reference/transactions/list-transactions /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/transactions Returns the transactions associated with the authenticated Meridian account. # List the users enrolled in a program Source: https://docs.mnai.com/api-reference/users/list-the-users-enrolled-in-a-program /products/meridian-accounts/api-reference/meridian-accounts.json get /v1/users Returns the users that have an enrollment on the program identified by the `X-Meridian-Program-Id` header, newest first. A user created through `POST /v1/auth/users` appears here once enrollment has started. Both SINGLE_USER and MULTI_USER integrations may call this endpoint; no `X-Meridian-User-Id` header is required. # Postman collection Source: https://docs.mnai.com/postman-collection Import the official Meridian Postman collection. HMAC signing is handled for you — set your credentials and start making requests. Meridian publishes a Postman collection that covers Meridian Accounts, Payout Origination, and Retail Virtual Accounts. Signing is handled for you, so once your credentials are set you can call any endpoint immediately. ## Import **1. Open Postman's import dialog** In Postman, select **Import** in the top-left and select the **Link** tab. **2. Paste the collection URL** Paste this URL and select **Continue**, then **Import**: ``` https://download.mnai.com/postman/meridian.postman_collection.json ``` The collection appears in your sidebar as **Meridian API**, with one folder per product. **3. Store your credentials in a Postman Environment** Create a new **Environment** (left sidebar → Environments → **+**) called something like `Meridian sandbox`. Add the variables listed below and fill in your credentials there. Make the environment active using the dropdown in the top-right. Storing credentials in an Environment (rather than on the collection) means re-importing later won't wipe them. > [!NOTE] > > Prefer a file? Download > [`meridian.postman_collection.json`](https://download.mnai.com/postman/meridian.postman_collection.json) > and use **Import → File** instead. ## Variables | Variable | Description | | - | - | | `baseUrl` | API base URL. Defaults to the sandbox host; override per environment when you're ready for production. | | `meridianApiKey` | Your Meridian API key. | | `meridianApiSecret` | Your Meridian API secret. Used only to compute the request signature — it is never sent on the wire. | | `meridianProgramId` | Your Meridian Program ID. Required for Meridian Accounts and Payout Origination. | | `meridianUserId` | The Meridian user the call targets. Required for Meridian Accounts MULTI\_USER integrations; leave blank for SINGLE\_USER. | | `meridianAccessToken` | Populated automatically after a successful call to `POST /v1/auth/token`. Use `{{meridianAccessToken}}` in any request that needs a bearer token. | | `meridianRefreshToken` | Populated automatically after a successful call to `POST /v1/auth/token`. Picked up automatically by the Refresh Token request. | > [!NOTE] > > Sandbox and production credentials are different. Do not reuse them across environments. ## Troubleshooting If a request fails with `401 Unauthorized`, the most common causes are: * An incorrect or missing `meridianApiSecret` * Clock skew greater than 60 seconds between your machine and Meridian * Using sandbox credentials against production (or vice versa) * A `meridianProgramId` mismatch for the targeted product ## Updates The collection is updated automatically when the API changes. To pull the latest version, re-run the import from the same URL — Postman will offer to **Replace** the existing collection. > [!WARNING] > > Re-importing replaces the collection and resets any values you stored directly on it. Keep your credentials in a Postman Environment to avoid having to re-enter them. # Credit API Source: https://docs.mnai.com/products/bank-transfers/credit-api Push funds from Meridian's sponsored account to a customer's account at the home bank. The Credit API is the bank partner's endpoint for receiving and processing payout instructions from Meridian. Meridian calls it each time a product event requires funds to be pushed to a customer — for example, when a customer initiates a withdrawal from a Retail Virtual Account. Meridian's Implementation team integrates with the bank partner's Credit API during onboarding. The exact specification is partner-dependent, and Meridian adapts to what the bank provides. > [!NOTE] > > If the bank partner does not have an existing Credit API, Meridian can provide consultation to help scope the integration and define requirements before development begins. See [Reference implementation](#reference-implementation) below for a recommended starting point. ## How it works Meridian sends a credit instruction to the bank partner's Credit API with the transfer amount and relevant customer details. The bank executes an intra-bank transfer from Meridian's sponsored account to the customer's account at the home bank. The bank then returns a confirmation, and Meridian uses it to update the transaction status. ## Integration requirements **1. Authentication** Meridian supports multiple authentication methods and aligns with the bank partner's Credit API requirements during onboarding. **2. Request payload** Meridian formats the request body according to the bank partner's specification, including any required encryption or signing. Meridian passes transaction details and sender/recipient KYC data with every credit instruction. Meridian confirms exact fields during onboarding. **3. Account validation** If the bank exposes a validation endpoint, Meridian can confirm recipient account eligibility before executing a credit. **4. Commit credit** Meridian submits the credit instruction to the bank partner's API to initiate the actual money movement from Meridian's sponsored account to the customer's account. **5. Transaction status** Meridian requires a mechanism to retrieve final transaction status after you submit a credit instruction. ## Planning considerations If you are building a Credit API for the first time, account for the following before development begins. **Idempotency** — Meridian includes a unique transaction ID with every credit instruction. The Credit API must use this ID to detect and safely reject duplicate requests, ensuring a customer is never credited twice for the same transaction. **Response model** — `commitCredit` should return a final result synchronously. This allows Meridian to update the transaction status immediately and deliver the best customer experience. If the bank's core system cannot support a synchronous response, an asynchronous model is possible — Meridian will poll `getStatus` to confirm the outcome. Treat this as a fallback and align it with Meridian during scoping. **Sandbox environment** — A non-production environment is required before go-live. Meridian will use it to validate the integration end-to-end before any real money movement occurs. ## Reference implementation If the bank partner does not have an existing Credit API, the following endpoints provide a recommended baseline. Requests are ISO 20022 pacs.008 credit transfers and responses are ISO 20022 pacs.002 status reports, sent as JSON. This is the format Meridian's integration layer uses internally, so an API built to it needs no custom mapping. See the [Credit API reference specification](/products/bank-transfers/credit-api-example/index) for the full contract. | Endpoint | Purpose | | :- | :- | | `createCredit` | Registers the pacs.008 before any processing, returning `RCVD`. | | `validateCredit` | Confirms recipient account eligibility before a credit is attempted, returning `ACCP` or `RJCT`. | | `commitCredit` | Initiates the actual money movement from Meridian's sponsored account to the customer's account. | | `getStatus` | Returns the current status as a pacs.002, with an ISO 20022 reason code on every rejection. Align on retry strategy with Meridian during onboarding. | # Authentication Source: https://docs.mnai.com/products/bank-transfers/credit-api-example/authentication The recommended authentication scheme for a partner-hosted Credit API, and the alternatives Meridian supports. Because the bank partner hosts the Credit API, the bank owns the authentication scheme. Meridian is the calling client and adapts to what the bank provides. ## Recommended: OAuth 2.0 client credentials Issue Meridian OAuth 2.0 client credentials and accept the resulting token as a bearer token on every request: ```http theme={null} POST /credits/commit HTTP/1.1 Host: api.examplebank.com Authorization: Bearer Content-Type: application/json ``` This is the reference default in the specification. It is well understood, requires no custom tooling on either side, and plugs into Meridian's integration layer with no bespoke work. ## Also supported If OAuth 2.0 is not viable for your institution, Meridian supports these alternatives. The concrete scheme is agreed during onboarding. | Scheme | Notes | | :- | :- | | mTLS | Mutual TLS with certificates exchanged during onboarding | | HMAC request signing | Shared-secret signature over the canonical request | | IP allowlisting | Meridian provides egress IP ranges; typically layered with another scheme | | Asymmetric payload signatures (RSA) | Signed request payloads using an exchanged public key | These can be combined. IP allowlisting in particular is usually applied alongside OAuth or mTLS rather than on its own. ## What live integrations tend to look like The bearer token above is the cleanest starting point, but bank Credit APIs in production rarely stop there. When scoping your build, expect that one or more of the following may apply, and raise them with Meridian early because each one adds integration time: | Pattern | What it means in practice | | :- | :- | | Request signing | A signature over the canonical request body, typically RSA or HMAC SHA-256, sent alongside the payload rather than as an `Authorization` header | | Field-level encryption | Account numbers, account names, and other PII encrypted individually inside the JSON body, using a public key exchanged during onboarding | | Credentials in the body | Partner codes, passwords, or client secrets carried inside the request payload instead of in headers | | Per-request token minting | A fresh token for every call, where the institution invalidates the previous one and caching is therefore not possible | | Layered schemes | Several of the above applied at once, for example a token plus a static API key header plus a request hash | None of these are problems, and Meridian implements them today for existing partners. They matter for planning. An integration using signing and field-level encryption takes meaningfully longer to certify than one using a bearer token alone, so decide early which of these your institution requires. > [!TIP] > > If you are free to choose, OAuth 2.0 client credentials with a bearer token is the fastest path to production. ## Requirements regardless of scheme > [!WARNING] > > Credentials must be **environment-scoped**. Sandbox and production must use separate credentials, and a sandbox credential must never be accepted by production. * All traffic is HTTPS with TLS 1.2 or higher. * Authentication failures return `401` with an `ApiError` body. Do not return `200` with a failure status for an authentication problem. See [business failures vs transport failures](/products/bank-transfers/credit-api-example/index#business-failures-vs-transport-failures). * Token or credential rotation should be possible without downtime. Tell Meridian your rotation cadence during onboarding. # Commit a credit Source: https://docs.mnai.com/products/bank-transfers/credit-api-example/commit-credit products/bank-transfers/credit-api-example/credit-api-example.yaml POST /credits/commit Execute the transfer from Meridian's sponsored account to the recipient. # Create a credit Source: https://docs.mnai.com/products/bank-transfers/credit-api-example/create-credit products/bank-transfers/credit-api-example/credit-api-example.yaml POST /credits Register a credit instruction with the bank ahead of validation and execution. # Get transaction status Source: https://docs.mnai.com/products/bank-transfers/credit-api-example/get-transaction-status products/bank-transfers/credit-api-example/credit-api-example.yaml GET /credits/{endToEndId} Retrieve the current status of a credit, polled by Meridian until the transaction is terminal. # Overview Source: https://docs.mnai.com/products/bank-transfers/credit-api-example/index A reference specification for bank partners building a Credit API from scratch, based on ISO 20022 pacs.008 and pacs.002. This section is Meridian's **reference specification** for a bank partner Credit API. It describes the API the **bank partner hosts** and **Meridian calls**. Meridian calls it whenever a product event requires pushing funds from Meridian's sponsored account to a customer's account at the home bank, such as a customer withdrawal from a Retail Virtual Account. > [!NOTE] > > Partners are not required to implement this exact contract. Meridian adapts to existing partner APIs during onboarding. See [Credit API](/products/bank-transfers/credit-api) for how that integration works. If you are building a Credit API from scratch, start here. Every request is an ISO 20022 **pacs.008** customer credit transfer and every response is an ISO 20022 **pacs.002** payment status report, both sent as JSON. Meridian's integration layer already uses this message internally, so an API matching this specification plugs in with no custom mapping. This shortens the integration timeline considerably. ## Message format Messages use the ISO 20022 XML tag names as JSON keys, so the structure maps one to one onto the ISO 20022 message definitions: ```json theme={null} { "FIToFICstmrCdtTrf": { "GrpHdr": { "MsgId": "CR7Q2M9X4KZ8-withdrawal-request", "NbOfTxs": "1", "...": "..." }, "CdtTrfTxInf": [ { "PmtId": { "InstrId": "CR7Q2M9X4KZ8", "EndToEndId": "mrdn-9f2c1e4a-71bd-4c5e-a2f3-8d6b0c9e1a27" }, "IntrBkSttlmAmt": { "Ccy": "PHP", "Amt": 1500.00 }, "Cdtr": { "Nm": "Maria Santos" }, "CdtrAcct": { "Id": { "Othr": { "Id": "001234567890" } } }, "...": "..." } ] } } ``` Every message carries exactly one transaction. See [Message reference](/products/bank-transfers/credit-api-example/message-reference) for what Meridian puts in each field, and the endpoint pages for the full schemas and examples. ## Lifecycle Meridian drives every credit through the same sequence: **1. Prepare Credit: create (optional)** `POST /credits` registers the pacs.008 ahead of any processing. The bank persists it and returns `RCVD`. The bank does not check the recipient account, and no money moves. **2. Prepare Credit: validate (optional but recommended)** `POST /credits/validate` confirms the recipient account can receive the credit, returning `ACCP` or `RJCT`. No funds move, and it is safe to call repeatedly. **3. Execute Credit: commit** `POST /credits/commit` executes the transfer. It is idempotent on `EndToEndId`. **4. Confirm Credit: get transaction status** `GET /credits/{endToEndId}` returns the current credit status. Meridian's workflow engine polls this until the credit reports `ACSC` or `RJCT`, and may also call it **before** commit as a pre-check. Until the bank receives a commit, the endpoint must return `404` rather than a status, even for a credit that was created or validated. Meridian treats the `404` as "not yet committed" rather than a failure. **5. Confirm Credit: status webhook (optional)** Proactively push the final pacs.002 to Meridian instead of waiting for polling. > [!NOTE] > > Create and validate are independent. Meridian may call validate without a prior create, and commit without either. Every request carries the full pacs.008, so no step depends on bank-side state from an earlier one. ## Status codes Every response reports a status in `TxInfAndSts[0].TxSts`, using ISO 20022 codes. Create and validate report the outcome of that call only. The **credit status** starts at commit. ### Create and validate responses These do not set the credit status, and the status endpoint does not report them. | Endpoint | `TxSts` | Meaning | | :- | :- | :- | | Create | `RCVD` | Registered. Nothing checked and nothing moved. | | Validate | `ACCP` | The recipient account passed eligibility checks. | | Validate | `RJCT` | The credit would be rejected, with a reason code. Meridian does not commit it. | Validate reports the eligibility check as of the call, so repeated calls can return different results. A validate call never changes the credit status. ### Credit status Commit, the status endpoint, and the status webhook report these values. Before commit, the status endpoint returns `404`. | `TxSts` | Meaning | Terminal | | :- | :- | :- | | `ACSP` | Money movement is underway. | No | | `ACSC` | The funds reached the recipient. | **Yes** | | `RJCT` | The credit will not be delivered. | **Yes** | | `PDNG` | The outcome is not yet determinable. | No | The credit status moves only forward, from `ACSP` to `ACSC` or `RJCT`, though skipping `ACSP` is fine. Terminal statuses must never change once reported. `PDNG` is exempt from the ordering: you can report it at any point after commit and follow it with any status. ## Response model If your core system can settle synchronously, return a final result (`ACSC` or `RJCT`) straight from commit so Meridian can update the customer sooner. This is an optimization, not a requirement. Meridian polls the status endpoint until the credit is terminal either way. Most partners settle asynchronously, and that is the expected case. Return `ACSP` from commit and expose the final state via `GET /credits/{endToEndId}` and, ideally, the [status webhook](/products/bank-transfers/credit-api-example/status-webhook). Align the polling and retry strategy with Meridian during onboarding. ### When you cannot determine the outcome Report `PDNG` when you genuinely do not know the result yet, such as a timeout inside your core system or an unreachable downstream rail. It is not a terminal status, so Meridian keeps polling, and you can follow it with any other status later. > [!TIP] > > `PDNG` is always better than guessing `RJCT`. `ACSC` and `RJCT` are terminal and can never be walked back, so a wrong guess is unrecoverable. ## Rejection reasons Every `RJCT` must carry an ISO 20022 reason code in `StsRsnInf[].Rsn.Cd`. Meridian maps these codes to customer-facing outcomes. Meridian holds a rejection without a reason for manual review instead of reversing it automatically, which delays the customer. | Code | Use for | | :- | :- | | `AC01` | Account number not found or invalid | | `AC04` | Account closed | | `AC06` | Account blocked or dormant | | `BE01` | Recipient name does not match the account | | `AM02` | Amount exceeds an account or transaction limit | | `AM12` | Amount invalid | | `AM04` | Insufficient funds in Meridian's sponsored account | | `AM05` | Duplicate instruction | | `RR04` | Regulatory or compliance rejection | | `NARR` | Any other reason. Explain it in `AddtlInf`. | ## Idempotency `EndToEndId` is Meridian's unique identifier for the credit and serves as the idempotency key. If you receive a commit for an `EndToEndId` you already processed, return the original outcome and do not credit twice. If the payload differs from the original request for the same `EndToEndId`, return `409`. Meridian sets `GrpHdr.CreDtTm` each time it builds a request, so leave it out of the comparison. ## Business failures vs transport failures When you reject a credit on business rules, such as a closed account, an exceeded limit, or a compliance block, return a **`200` response**. The response carries a pacs.002 with `TxSts: RJCT` and a reason code. Reserve `4xx` and `5xx` for malformed requests, authentication failures, and genuine server faults. > [!WARNING] > > This distinction is critical. Meridian retries transport failures but treats business failures as final. ## Conventions These are the conventions used throughout this specification. Meridian can adapt to the bank's preferred formats during onboarding. * Amounts (`Amt`) are JSON numbers with up to 2 decimal places. Parse them into a decimal type, never a binary float. Currencies are ISO 4217 codes. * Country codes are ISO 3166-1 alpha-2 (`PH`, `US`). * Timestamps are ISO 8601 with a timezone offset or `Z`. * `EndToEndId` and `InstrId` can be up to 64 characters, and `MsgId` up to 83. Both are longer than the ISO 20022 `Max35Text` limit. * All requests and responses are `application/json` over HTTPS (TLS 1.2+). * Ignore JSON properties you do not recognize. Meridian may add optional fields without a version change. # Message reference Source: https://docs.mnai.com/products/bank-transfers/credit-api-example/message-reference What Meridian puts in each pacs.008 field, what the bank returns in the pacs.002, and the Meridian extensions to ISO 20022. This page lists every field Meridian sends in the pacs.008 credit transfer and every field the bank returns in the pacs.002 status report. The endpoint pages carry the full JSON schemas and examples. Paths are relative to the single transaction, `FIToFICstmrCdtTrf.CdtTrfTxInf[0]`, unless they start with `GrpHdr`. ## pacs.008: what Meridian sends ### Group header | Field | Always sent | Content | | :- | :- | :- | | `GrpHdr.MsgId` | Yes | `-withdrawal-request`. Echo it as `OrgnlMsgId`. | | `GrpHdr.CreDtTm` | Yes | When Meridian built this request, in UTC. It can differ between calls for the same credit. | | `GrpHdr.NbOfTxs` | Yes | Always `"1"`. | | `GrpHdr.SttlmInf.SttlmMtd` | Yes | Always `CLRG`. | | `GrpHdr.InstgAgt` | Yes | Meridian's side of the transfer. | | `GrpHdr.InstdAgt` | Yes | The bank. | ### Identification and amount | Field | Always sent | Content | | :- | :- | :- | | `PmtId.EndToEndId` | Yes | Meridian's unique ID for the credit and the **idempotency key**. The path parameter of the status endpoint. | | `PmtId.InstrId` | Yes | Meridian's credit reference. Echo it as `OrgnlInstrId`. | | `IntrBkSttlmAmt.Amt` | Yes | The amount to credit, as a JSON number with up to 2 decimal places. | | `IntrBkSttlmAmt.Ccy` | Yes | The currency to credit, ISO 4217. | | `XchgRate` | Cross-border only | The rate applied from the sender's currency, as a decimal string. | | `Purp.Prtry` | Yes | The Meridian product that originated the credit, for example `REMITTANCES` or `VIRTUAL_ACCOUNTS`. | | `RmtInf.Ustrd` | When present | The sender's remarks and a customer-facing reference. Print them on statements where your core allows. | ### Recipient | Field | Always sent | Content | | :- | :- | :- | | `CdtrAcct.Id.Othr.Id` | Yes | The account to credit, in the bank's canonical format. Meridian uses `Id.IBAN` instead where IBANs apply. | | `CdtrAcct.Ccy` | Yes | The account currency. | | `Cdtr.Nm` | Yes | The recipient's full name. | | `Cdtr.GvnNm`, `Cdtr.FmlyNm` | When known | The recipient's first and last name. See [Meridian extensions](#meridian-extensions). | | `Cdtr.PstlAdr.AdrLine` | When known | The recipient's address lines. | | `CdtrAgt.FinInstnId` | Yes | The bank, by `BICFI` or `ClrSysMmbId.MmbId`. | ### Sender The sender is the originator of the funds and the subject of compliance screening. | Field | Always sent | Content | | :- | :- | :- | | `Dbtr.Nm` | Yes | The sender's full name. | | `Dbtr.GvnNm`, `Dbtr.FmlyNm` | Yes | The sender's first and last name. See [Meridian extensions](#meridian-extensions). | | `Dbtr.PrvtId.DtAndPlcOfBirth.BirthDt` | Yes | The sender's date of birth (YYYY-MM-DD). | | `Dbtr.PstlAdr` | When known | The sender's address: `AdrLine`, `TwnNm`, `CtrySubDvsn`, `PstCd`, `Ctry`. | | `Dbtr.CtryOfRes` | When known | The sender's country of residence, ISO 3166-1 alpha-2. | | `DbtrAcct.Id.Othr.Id` | Yes | The sender's account identifier at Meridian. | | `DbtrAcct.Ccy` | Yes | The currency the sender funded the transfer in. | | `DbtrAgt.FinInstnId` | Yes | The sender's institution. `Othr.Id` is `UNKNOWN_INSTITUTION` when Meridian has no BIC or clearing member ID for it. | ## Meridian extensions The message follows the ISO 20022 structure with two additions. Support both, and ignore any key you do not recognize. > **Split names: GvnNm and FmlyNm** > > pacs.008 carries a party's name only as a single `Nm`. Many banks match recipients on first and last name separately, so Meridian also sends `GvnNm` (given name) and `FmlyNm` (family name) on `Dbtr` and `Cdtr`. `Nm` is normally the two joined with a space. > **Cross-border data: SplmtryData /MeridianPay/CrossBorderInfo** > > On cross-border credits, Meridian sends one `SplmtryData` entry with `PlcAndNm` set to `/MeridianPay/CrossBorderInfo`. Its `Envlp` is a flat object of string values, and every key is optional: > > | Key | Content | > | :- | :- | > | `sourceAmount` | The amount the sender paid, as a decimal string | > | `sourceCurrency` | The currency the sender paid in, ISO 4217 | > | `exchangeRate` | The customer exchange rate applied | > | `baseExchangeRate` | The exchange rate before Meridian's margin | > | `midMarketRate` | The mid-market rate at the time of the transfer | > | `reference` | A customer-facing reference for the transfer | > | `relationToReceiver` | The sender's relationship to the recipient | > | `sourceOfIncome` | The sender's source of income | > > Some corridors need further compliance fields. Meridian agrees them with the bank during onboarding and adds them to this envelope. ## pacs.002: what the bank returns Every endpoint and the status webhook return the same pacs.002 shape, with exactly one element in `TxInfAndSts`. | Field | Required | Content | | :- | :- | :- | | `GrpHdr.MsgId` | Yes | The bank's ID for this status report. Meridian stores it for support requests. | | `GrpHdr.CreDtTm` | Yes | When the bank produced the report. | | `OrgnlGrpInfAndSts.OrgnlMsgId` | Yes | Echo of `GrpHdr.MsgId` from the pacs.008. | | `OrgnlGrpInfAndSts.OrgnlMsgNmId` | No | `pacs.008`. | | `TxInfAndSts[0].OrgnlEndToEndId` | Yes | Echo of `PmtId.EndToEndId`. Meridian matches the response to the credit on this field. | | `TxInfAndSts[0].OrgnlInstrId` | No | Echo of `PmtId.InstrId`. | | `TxInfAndSts[0].TxSts` | Yes | `RCVD` from create, `ACCP` or `RJCT` from validate, and `ACSP`, `ACSC`, `RJCT` or `PDNG` from commit onward. See [Status codes](/products/bank-transfers/credit-api-example/index#status-codes). | | `TxInfAndSts[0].StsRsnInf[].Rsn.Cd` | On `RJCT` | An ISO 20022 reason code. See [Rejection reasons](/products/bank-transfers/credit-api-example/index#rejection-reasons). | | `TxInfAndSts[0].StsRsnInf[].AddtlInf` | No | Human-readable detail. Not shown to customers verbatim. | | `TxInfAndSts[0].AccptncDtTm` | On `ACSC` and `RJCT` | When the bank reached the reported status. | | `TxInfAndSts[0].AcctSvcrRef` | After commit | The bank's own reference for the transfer, used for reconciliation and support. | # Status webhook Source: https://docs.mnai.com/products/bank-transfers/credit-api-example/status-webhook products/bank-transfers/credit-api-example/credit-api-example.yaml webhook creditStatusChanged Push a final transaction status to Meridian instead of waiting for polling. # Validate a credit Source: https://docs.mnai.com/products/bank-transfers/credit-api-example/validate-credit products/bank-transfers/credit-api-example/credit-api-example.yaml POST /credits/validate Confirm the recipient account can receive the credit before any money moves. # Debit API Source: https://docs.mnai.com/products/bank-transfers/debit-api Pull funds from a customer's account into Meridian's sponsored account at the home bank. The Debit API is the bank partner's endpoint for receiving and processing pull instructions from Meridian. Meridian calls it each time a product event requires funds to be pulled from a customer — for example, when a customer converts local currency between their home bank accounts. Meridian's Implementation team integrates with the bank partner's Debit API during onboarding. The exact specification is partner-dependent, and Meridian adapts to what the bank provides. > [!NOTE] > > If the bank partner does not have an existing Debit API, Meridian can provide consultation to help scope the integration and define requirements before development begins. See [Reference implementation](#reference-implementation) below for a recommended starting point. ## How it works When a customer converts local currency between their home bank accounts, Meridian sends a debit instruction to the bank partner's Debit API. The instruction includes the transfer amount and relevant customer details. The bank executes an intra-bank transfer from the customer's local currency account into Meridian's sponsored account at the home bank. The bank then returns a confirmation, and Meridian uses it to update the transaction status. ## Integration requirements **1. Authentication** Meridian supports multiple authentication methods and aligns with the bank partner's Debit API requirements during onboarding. **2. Authorize debit (optional)** Some banks require a one-time approval before funds can be pulled from a customer's account. If required, Meridian will request authorization before proceeding to commit the debit. **3. Request payload** Meridian formats the request body according to the bank partner's specification, including any required encryption or signing. Meridian passes transaction details and sender/recipient KYC data with every debit instruction. Meridian confirms exact fields during onboarding. **4. Account validation** If the bank exposes a validation endpoint, Meridian can confirm the source account is eligible and has sufficient funds before executing a debit. **5. Commit debit** Meridian submits the debit instruction to the bank partner's API to initiate the actual money movement from the customer's account into Meridian's sponsored account. **6. Transaction status** Meridian requires a mechanism to retrieve final transaction status after you submit a debit instruction. ## Planning considerations If you are building a Debit API for the first time, account for the following before development begins. **Idempotency** — Meridian includes a unique transaction ID with every debit instruction. The Debit API must use this ID to detect and safely reject duplicate requests, ensuring a customer is never debited twice for the same transaction. **Response model** — `commitDebit` should return a final result synchronously. This allows Meridian to update the transaction status immediately and deliver the best customer experience. If the bank's core system cannot support a synchronous response, an asynchronous model is possible — Meridian will poll `getStatus` to confirm the outcome. Treat this as a fallback and align it with Meridian during scoping. **Sandbox environment** — A non-production environment is required before go-live. Meridian will use it to validate the integration end-to-end before any real money movement occurs. **Insufficient funds handling** — Unlike a credit, a debit can fail because the source account does not have enough funds. The bank must provide clear error codes for this scenario, and you should define a retry strategy with Meridian during onboarding. ## Reference implementation If the bank partner does not have an existing Debit API, the following endpoints provide a recommended baseline. Meridian can use this as a starting point during the scoping and requirements phase. | Endpoint | Purpose | | :- | :- | | `authorizeDebit` | *(Optional)* Requests one-time approval from the bank before funds can be pulled from the customer's account. | | `createDebit` | Establishes the transaction and handles any required setup before money movement begins. | | `validateDebit` | Confirms the source account is eligible and has sufficient funds. | | `commitDebit` | Initiates the actual money movement from the customer's account into Meridian's sponsored account. | | `getStatus` | Fetches the current transaction status to confirm success or failure. Must support detailed error codes and align on retry strategy with Meridian during onboarding. | # Overview Source: https://docs.mnai.com/products/bank-transfers/overview How Meridian instructs bank partners to move money in real time. Instant bank transfers work by instructing the bank to perform an intra-bank transfer between Meridian's sponsored account and the customer's account at the home bank. Because these transfers settle within the bank's own systems, a single bank partner integration can serve all Meridian products — Retail Virtual Accounts, Meridian Accounts for Business, and Realtime Collections. ## Meridian's integration approach Meridian's Implementation team owns the integration with each bank partner's Credit or Debit API. Because API specifications are entirely partner-dependent, Meridian adapts to the API specification the bank partner provides. > [!NOTE] > > In many cases, the Credit and Debit API may be the same API at the bank. That single intra-bank account-to-account book transfer API supports both push and pull directions. If a bank partner does not have an existing API, Meridian can provide consultation to help scope the integration and define requirements before development begins. ## Integration types > **[Credit API](/products/bank-transfers/credit-api)** > > Push funds from Meridian's sponsored account to an account holder's account. > **[Debit API](/products/bank-transfers/debit-api)** > > Pull funds from an account holder's account into Meridian's sponsored account. # Overview Source: https://docs.mnai.com/products/meridian-accounts/api-reference/index Learn how to authenticate and use the Meridian Accounts API. ## Base URLs | Environment | URL | Purpose | | :- | :- | :- | | Sandbox | `https://sandbox-api.va.meridianpay.com` | Testing | | Production | `https://api.va.mnai.com` | Live traffic | Meridian provides your Sandbox environment during onboarding. Validate your integration there before using production. ## Authentication The Meridian Accounts API supports two authentication layers: * Partner HMAC authentication when your backend proves partner identity with `X-Meridian-*` headers and an HMAC SHA-256 signature * JWT bearer authentication when your client or backend sends `Authorization: Bearer {token}` after a token has already been minted for a user In a typical integration, your backend uses partner HMAC authentication to call `POST /v1/auth/token`, then later user-scoped requests use the returned JWT. For the conceptual auth guides, see: * [Authentication overview](/products/meridian-accounts/guides/authentication-overview) * [Server-to-server authentication with HMAC](/products/meridian-accounts/guides/server-to-server-authentication-with-hmac) * [Client-server authentication with JWT](/products/meridian-accounts/guides/client-server-authentication-with-jwt) # Authentication overview Source: https://docs.mnai.com/products/meridian-accounts/guides/authentication-overview Learn when Meridian Accounts uses partner HMAC authentication versus Meridian JWT bearer authentication. Meridian Accounts uses two authentication layers that serve different purposes: * **Partner HMAC authentication** proves that your backend is an entitled Meridian integrator * **JWT bearer authentication** carries Meridian user context after a token has already been minted These are not competing end-user options. In a typical integration, your backend first authenticates with partner credentials and HMAC, then receives a Meridian access token, and later uses that JWT on behalf of the target user. ## Which authentication method to use Use this decision tree for each request: * If your backend is calling Meridian directly with your partner API Key and Secret, use [server-to-server authentication with HMAC](/products/meridian-accounts/guides/server-to-server-authentication-with-hmac) * If you already have a Meridian access token and are making a user-scoped API call, use [client-server authentication with JWT](/products/meridian-accounts/guides/client-server-authentication-with-jwt) ## Authentication matrix | Scenario | Caller | Credential type | Required auth fields | Typical endpoints | | :- | :- | :- | :- | :- | | Backend proves partner identity | Your server | Meridian API Key + HMAC signature | `X-Meridian-Api-Key`, `X-Meridian-Program-Id`, `X-Meridian-Timestamp`, `X-Meridian-Signature`, and `X-Meridian-User-Id` for `MULTI_USER` | `POST /v1/auth/token`, partner-key endpoints supported by the API | | User-scoped API access | Your client or server | Meridian bearer token | `Authorization: Bearer {token}` | `GET /v1/enrollment`, `GET /v1/virtual-accounts`, `GET /v1/transactions` | ## How the flow fits together 1. Your backend sends a partner-authenticated request signed with HMAC. 2. Meridian issues a JWT access token for the target user. 3. Your client or backend uses `Authorization: Bearer {token}` on later API requests. Keep your Meridian API Secret on the server only. Do not try to generate HMAC signatures from a browser or mobile client. ## Related guides * [Choose your integration model](/products/meridian-accounts/guides/choose-your-integration-model) * [Server-to-server authentication with HMAC](/products/meridian-accounts/guides/server-to-server-authentication-with-hmac) * [Client-server authentication with JWT](/products/meridian-accounts/guides/client-server-authentication-with-jwt) # Choose your integration model Source: https://docs.mnai.com/products/meridian-accounts/guides/choose-your-integration-model Understand how SINGLE_USER and MULTI_USER integrations differ and how that choice affects authentication and onboarding. Before you implement authentication or onboarding, decide whether your Meridian Accounts integration is configured as `SINGLE_USER` or `MULTI_USER`. This is an integration configuration choice, not an authentication method. It determines how your partner credentials relate to Meridian users and whether `X-Meridian-User-Id` is required on partner-authenticated requests. ## Integration types at a glance | Integration type | Meridian user represents | `X-Meridian-User-Id` on partner-key calls | `POST /v1/auth/users` | Credential scope | | :- | :- | :- | :- | :- | | `SINGLE_USER` | One fixed Meridian user configured for the partner integration | Must be omitted | Not allowed | One partner credential set serves one fixed Meridian user | | `MULTI_USER` | One Meridian user per customer or business you manage | Required | Allowed | One partner credential set can act on many Meridian users | ## Exact request rules The Meridian Accounts API applies these rules to partner-key requests: * `MULTI_USER`: send `X-Meridian-User-Id` to identify which Meridian user the request targets * `SINGLE_USER`: omit `X-Meridian-User-Id`; Meridian uses the single configured user for that partner key These rules apply when your backend is authenticating directly with Meridian by using partner credentials and HMAC. ## How this affects token creation When you call `POST /v1/auth/token`, the integration type changes how Meridian determines the user: * `SINGLE_USER`: Meridian derives the user from the partner key configuration, so `X-Meridian-User-Id` must not be sent * `MULTI_USER`: your request must identify the target user with `X-Meridian-User-Id` If you are managing many customers, `MULTI_USER` also allows you to create or look up Meridian users and then mint access tokens for the correct user in the selected `programId`. ## How this affects onboarding `SINGLE_USER` and `MULTI_USER` do not change the program enrollment lifecycle, but they do change how you identify the user entering that lifecycle: * `SINGLE_USER`: onboarding operates on the one Meridian user already tied to the partner configuration * `MULTI_USER`: onboarding operates on the specific Meridian user identified by `X-Meridian-User-Id` For `MULTI_USER` integrations, you should keep your own mapping between customer records, `externalId`, and Meridian `userId`. Persist the returned `userId` when the user is first created so later token requests can reuse that mapping. ## Recommended reading order After choosing the integration model, continue with: * [Authentication overview](/products/meridian-accounts/guides/authentication-overview) * [Server-to-server authentication with HMAC](/products/meridian-accounts/guides/server-to-server-authentication-with-hmac) * [Client-server authentication with JWT](/products/meridian-accounts/guides/client-server-authentication-with-jwt) # Client-server authentication with JWT Source: https://docs.mnai.com/products/meridian-accounts/guides/client-server-authentication-with-jwt Use the Meridian access token as a bearer token after your backend has minted it with partner HMAC authentication. The Meridian JWT is the access token returned by `POST /v1/auth/token`. After your backend mints that token, your client or backend can send it on later Meridian Accounts requests by using: ```http theme={null} Authorization: Bearer {token} ``` ## Intended flow JWT authentication is the second step in the Meridian Accounts authentication model: 1. Your backend authenticates to Meridian with partner HMAC credentials. 2. Meridian returns an access token for the target user. 3. Your client or backend uses that JWT on later user-scoped API requests. Use this flow when you need to call Meridian on behalf of a specific user after you have already established that user context. ## What JWT-authenticated requests need JWT-authenticated requests use the bearer token in the `Authorization` header. They do not need partner authentication headers such as: * `X-Meridian-Api-Key` * `X-Meridian-Program-Id` * `X-Meridian-Timestamp` * `X-Meridian-Signature` * `X-Meridian-User-Id` The access token already carries the user context for the request. ## Typical JWT use cases Use JWT bearer authentication for user-scoped operations such as: * checking the current enrollment with `GET /v1/enrollment` * reading information requests and submitting onboarding data * listing balances * reading transaction history For frontend-facing account experiences, JWT bearer authentication is the recommended model because it avoids exposing partner HMAC credentials to the client. ## Refreshing tokens When the current access token expires, your backend can exchange a previously issued refresh token by calling `POST /v1/auth/token` with the `refresh_token` grant. Meridian derives the user from the refresh token in that flow, so the request does not depend on a new client-side HMAC signature. Keep the refresh-token exchange on the backend together with your other partner-authenticated token management logic. ## Security notes * never mint Meridian JWTs directly from a browser or mobile client * keep the Meridian API Secret on the backend only * issue or refresh access tokens from trusted server-side code, then pass the resulting bearer token to the component that needs to call Meridian ## Related guides * [Authentication overview](/products/meridian-accounts/guides/authentication-overview) * [Server-to-server authentication with HMAC](/products/meridian-accounts/guides/server-to-server-authentication-with-hmac) * [Displaying account information](/products/meridian-accounts/guides/displaying-account-information) # Displaying account information Source: https://docs.mnai.com/products/meridian-accounts/guides/displaying-account-information Learn how to use Meridian Accounts to display deposit instructions and account activity in your UI. Once a customer's enrollment is `ACTIVE`, your integration can begin showing Meridian balance information in your UI. ## Authentication for account reads These account-read operations can be performed with a Meridian JWT and, where the API supports direct partner authentication, with HMAC-signed partner credentials. For client-facing account views, prefer JWT bearer authentication so your frontend uses `Authorization: Bearer {token}` instead of partner credentials. Review [Client-server authentication with JWT](/products/meridian-accounts/guides/client-server-authentication-with-jwt) before implementing frontend-facing account reads. In Meridian Accounts, the balance is the customer-facing object to display. It represents how your customer receives local payments. Its schema is designed to present deposit instructions that can be shared with senders. ## How to display account information Use the balance and transaction APIs as read models for your UI. At a high level, the display flow is: **1. List the customer's balances** Call `GET /v1/balances` to retrieve the balances available for the customer in the current program context. **2. Show deposit instructions** Call `GET /v1/balances/{balanceId}` to retrieve the full balance detail and render the deposit instructions your customer can share with a sender. **3. Load transaction history** Call `GET /v1/transactions` to populate the customer's account activity feed. **4. Show transaction details when needed** Call `GET /v1/transactions/{transactionId}` when your UI needs the full detail for a selected transaction. ## What the balance represents The balance is meant for display in your UI. Use it to show the customer where to send funds, which deposit instructions to share, and what funds are available versus actual. When a sender uses those deposit instructions, the balance receives the incoming funds. Depending on how that balance is configured, the deposited funds may either remain on the balance or trigger a follow-on sweep to the configured credit instrument. ## How to interpret transactions The transaction APIs are designed for display purposes. Treat them as read-only account activity, not as endpoints for creating, editing, or reversing movement. Any funds received into a balance result in a `deposit` transaction. If the balance is configured with a `SWEEP` routing policy, Meridian may also initiate a subsequent `withdrawal` transaction for that same flow. This means your customer may see both: * an inbound `deposit` into the balance * a follow-on `withdrawal` that moves funds to the configured credit instrument ## Using `sourceType` to explain what happened The `sourceType` on a transaction helps explain what caused that transaction. For now, your integration should interpret: * `DEPOSIT_INSTRUCTION` as activity directly associated with a balance deposit instruction * `ROUTING_POLICY` as follow-on movement that Meridian initiated automatically from the balance routing policy Use `type` together with `sourceType` to help the customer understand whether they are looking at the incoming deposit or at a subsequent automatic withdrawal. ## Recommended UI behavior Your integration should: * use the balance responses to render shareable deposit instructions and balance amounts * present transactions as a ledger or activity feed * treat transaction responses as read-only history * use transaction `type` and `sourceType` to explain why a movement appears in the account timeline ## Next steps After implementing this flow, review: * [Onboarding your customer](/products/meridian-accounts/guides/onboarding-your-customer) * [Client-server authentication with JWT](/products/meridian-accounts/guides/client-server-authentication-with-jwt) * [API reference overview](/products/meridian-accounts/api-reference) * [Webhooks overview](/products/meridian-accounts/webhooks) * [Transaction webhooks](/products/meridian-accounts/webhooks/webhooks-transaction) * [Account webhooks](/products/meridian-accounts/webhooks/webhooks-account) # Getting started Source: https://docs.mnai.com/products/meridian-accounts/guides/getting-started Set up your Meridian Accounts Sandbox credentials, understand how Meridian identifies your customers, and prepare for production access. Meridian starts your integration in Sandbox. You will receive a Sandbox API Key, Sandbox Secret, and Program ID to build and test against the Meridian Accounts API. ## Before you integrate You need these Sandbox credentials from Meridian: * `API Key` * `Secret` * `Program ID` Use the Program ID when authenticating with the API. ## Integration path **1. Receive Sandbox credentials** Meridian provides your Sandbox API Key, Sandbox Secret, and Program ID. **2. Build in Sandbox** Use those credentials to integrate your application with the Meridian Accounts API and webhooks in the Sandbox environment. **3. Complete certification** Meridian reviews and certifies your integration before enabling production access. **4. Move to production** After certification, Meridian provides your production API Key and Secret. ## How Meridian identifies your customers Your system keeps its own customer record. In Meridian, each of your customers maps to a single Meridian user. You supply `externalId` as your unique identifier for that customer. Use `externalId` whenever you need to refer to that customer from your system. Meridian returns `userId` as the Meridian identifier for that same user. You should store Meridian `userId` in your system after you first receive it. A Meridian user can belong to one or more programs. `programId` identifies the program context used when authenticating with the API and when Meridian sends webhook metadata. | Identifier | Issued by | Purpose | | :- | :- | :- | | `externalId` | Integrator | Identifies your customer and enforces uniqueness in your system | | `userId` | Meridian | Identifies the Meridian user that represents that customer | | `programId` | Meridian | Identifies the program context for authentication and webhook metadata | ## Choose your integration and authentication model first Before you implement token creation, decide: * whether your Meridian Accounts integration is `SINGLE_USER` or `MULTI_USER` * whether you are making a given request with partner HMAC authentication or a Meridian JWT bearer token These decisions affect whether `X-Meridian-User-Id` is required, how your backend mints tokens, and which authentication headers later requests should send. Review these guides before building the auth flow: * [Choose your integration model](/products/meridian-accounts/guides/choose-your-integration-model) * [Authentication overview](/products/meridian-accounts/guides/authentication-overview) * [Server-to-server authentication with HMAC](/products/meridian-accounts/guides/server-to-server-authentication-with-hmac) * [Client-server authentication with JWT](/products/meridian-accounts/guides/client-server-authentication-with-jwt) ## Where you first see this For a first-time `MULTI_USER` customer flow, create the Meridian user first with `POST /v1/auth/users`, then mint a token with `POST /v1/auth/token`. The create-user response is typically the first place you will see Meridian `userId` for that customer. Store that `userId` in your system so you can track the Meridian user alongside your own customer record and reuse it on later token requests. ## Handling duplicate users Meridian enforces uniqueness for `externalId` at the Meridian-user level. If you send `POST /v1/auth/users` with an `externalId` that has already been used, the request fails instead of creating another Meridian user for that customer. Before creating a user, determine whether the customer already has a Meridian `userId` in your system. If they do, use that `userId` to create the token, even if the customer still needs enrollment in the selected program. Store the returned `userId` when you first create the Meridian user, and rely on your own customer-to-user mapping on later token requests. This matters because Meridian users can belong to more than one program. A customer may already have a Meridian user record but still need enrollment in the program you selected for the current flow. ## Next steps After you receive your Sandbox credentials, review: * [Choose your integration model](/products/meridian-accounts/guides/choose-your-integration-model) * [Authentication overview](/products/meridian-accounts/guides/authentication-overview) * [API reference overview](/products/meridian-accounts/api-reference) * [Webhooks overview](/products/meridian-accounts/webhooks) # Information we collect Source: https://docs.mnai.com/products/meridian-accounts/guides/kyb-information The business, ownership, and document information collected when opening a Meridian Account for a business (KYB). When a business opens a Meridian Account, Meridian collects the information below for Know Your Business (KYB) verification. This page explains what is asked for and why, so you know what to gather before you start. For how the information is collected and submitted, see [Onboarding your customer](/products/meridian-accounts/guides/onboarding-your-customer). > [!WARNING] > > **Every program is configured differently.** This page describes the information Meridian typically collects. It is not a contract, and it is not a field reference. > > Your program's actual requirements are returned by the [Information Request API](/products/meridian-accounts/api-reference): which items apply, whether each is mandatory, the field names to submit, and the accepted values. Build your integration against the schema that API returns, not against this page. All information is subject to independent verification, and Meridian may request additional documentation at any time. ## Business information Details about the legal entity itself. | Information | Notes | | :- | :- | | Legal name of entity | Exactly as it appears on formation documents | | Trade name or DBA | If the business operates under a different name | | Business registration number | The number issued by the registry the business is incorporated with. Must match the number shown on the proof of registration. | | Date of formation | Cannot be a future date | | Industry | A short description of the industry the business operates in. Free text, not a fixed list. | | Tax ID number | For US businesses this is the EIN, in `12-3456789` format. Tax IDs for businesses outside the US are not format-checked. | | Business type | The legal entity type. See below for typical values. | | Website | Required. Must be a full URL starting with `http://` or `https://`. | | Business address | The entity's legal or formation address: street address, city, state or province, postal code, and country. | > **Typical business types** > > * Benefit corporation > * Closely held corporation > * Cooperative > * Corporation > * General partnership > * Limited liability company > * Limited partnership > * Nonprofit corporation > * Non-US entity > * Small business corporation > * Sole proprietorship ## People connected to the business Everyone connected to the business is submitted as a separate entry, and each entry carries one or more roles. | Information | Notes | | :- | :- | | Full name | First and last name, plus middle name where the person has one | | Date of birth | Must be at least 18 years ago, and cannot be a future date | | Citizenship | The person's country of citizenship | | Home address | Street address, city, state or province, postal code, and country | | Email | Must be a valid email address | | Phone number | In E.164 format, starting with `+` and the country code | | Tax number | The person's individual tax identifier. For US persons this is the SSN, in `123-45-6789` format. | | Job title | For example, CEO | | Roles | One or more. See below. | | Ownership percentage | Zero for someone with no stake. See below. | ### Roles One person can hold several roles. A CEO who owns 40% and signs the application is a control person, a beneficial owner, and the signer, all in a single entry. | Role | Definition | | :- | :- | | Beneficial owner | An individual who ultimately owns or controls the business, directly or through another entity. | | Shareholder | An individual who holds shares in the business. | | Director | A member of the board of directors or the equivalent governing body. | | Control person | A senior official with significant responsibility to control, manage, or direct the business: CEO, CFO, COO, President, Vice President, Treasurer, Managing Member, or General Partner. | | Signer | The person who signs the application and agreements for the business. | | Representative | A person authorized to act and sign for the business who is not an owner or director. | | Admin | A person with administrative authority over the business account. | | Business controller | A person who directs the business's operations or finances but holds no ownership stake. Treated the same as a control person. | | Business admin | A person who manages the business's affairs on its behalf, such as an office manager or company secretary. Treated the same as an admin. | Some roles are treated the same during verification, so use whichever matches your records: * Beneficial owner and shareholder. * Control person, representative, admin, business controller, and business admin. The API accepts all nine roles. The labels it returns, such as `Authorised signer` for the signer role, are safe to show your customer. > [!TIP] > > List a person's most significant role first. Some verification paths read only the first role in the list. ### Who to list **Required.** At least one person must be marked as the signer. Submission fails without one. **Recommended.** To pass verification without follow-up questions, the people you list should also cover all three of: * one beneficial owner (or shareholder) * one director * one control person One person can cover all four. If ownership is spread thin and no individual stands out as an owner, list the senior manager who runs the business as the beneficial owner. ### Ownership percentage Every person carries an ownership percentage, and two rules are enforced when you submit: * each person's percentage must be a whole number between 0 and 100 * the total across all people cannot exceed 100 The total does not have to reach 100. A total below 100 is normal and passes, since the individuals you list may hold only part of the business, with the rest held through other entities. Someone with no stake, such as a signer who owns nothing, is submitted with zero. ### Age Everyone listed must be at least 18 years old, checked against their date of birth. A date of birth in the future is rejected. ## Documents about each person Uploaded for each person connected to the business. | Document | What to upload | | :- | :- | | Passport | Photo page of a valid passport, with the machine-readable lines legible | | Driver's license, front | Front of a valid government driver's license | | Driver's license, back | Back of the same license | | Proof of address | Utility bill (power, water, gas, internet), bank or card statement, government tax notice, or a signed lease, showing the person at their home address | **Identity document.** Upload a passport, or both sides of a driver's license: * If you send a passport, it satisfies the identity requirement and any driver's license files are ignored. They are still stored, they just do not count as the identity document. * If you send no passport, both license sides are required. One side without the other is rejected when you submit. * A passport also silences the missing-side error. A passport plus a front-only license passes, because the passport is checked first. > [!NOTE] > > **Recommended.** Reviewers expect identity documents to be unexpired and proof of address to be dated within 90 days. > > Not accepted for proof of address: PO box addresses, mobile phone bills, screenshots with no issuer name, and documents in another person's name. ## Documents about the business Uploaded once for the business. | Document | What to upload | | :- | :- | | Proof of registration | Certificate of incorporation or registration, SEC or DTI registration, or articles of incorporation. Must show the legal name, registration number, and date of formation. | | Proof of ownership | A chart or cap table showing every direct and indirect owner with the percentage held at each level, down to the individual people. Signed and dated by an authorized signatory. | | Bank statement | The most recent business bank statement, showing the bank name, account holder name, and account number. A bank letter is accepted if a statement is not available. | > [!WARNING] > > The names and percentages on the ownership document must match the people you listed. A mismatch is the most common reason an application comes back for more information. > [!NOTE] > > **Recommended.** Reviewers expect proof of registration to show the entity as currently active, proof of ownership to be dated within 12 months or after the last ownership change, and the bank statement to be the most recent, within 90 days. ## Upload rules ### File limits Each document has its own file types and, for PDFs, a page limit. Meridian checks the page limit when you upload and rejects a file over the limit before review starts. | Document | File types | Max PDF pages | | :- | :- | :- | | Passport | PDF, JPEG, PNG | 3 | | Driver's license, front | PDF, JPEG, PNG | 3 | | Driver's license, back | PDF, JPEG, PNG | 3 | | Proof of address | PDF | 5 | | Proof of registration | PDF | 20 | | Proof of ownership | PDF | 20 | | Bank statement | PDF | 10 | These limits can change. The [Information Request](/products/meridian-accounts/api-reference) returns them for each document slot as `supportedMimeTypes`, `maxFileSizeBytes`, and `maxPdfPages`, so read them from the API rather than hard-coding the values above. ### Rules for every file * 10 MB maximum, one file per document. * A PDF must open without a password. A file that only has an owner password, the kind that opens without a prompt, is fine. * Meridian rejects a file sent as a PDF that cannot be read. Images are not page-checked. * Send the correct content type. The upload itself accepts any type, but a file of the wrong type makes the document `INVALID` when the request is validated, and you cannot submit until you replace it. * Full page, all four corners visible, text readable. * English, or include a certified translation. * Name, address, dates, and the issuer must stay visible. You may hide transaction lines and all but the last four digits of an account number. ### Upload errors A rejected upload returns `400` with one of these codes in `errorCode`. The `message` names the document and the limit, and `validationErrors` points at the `file` field. | `errorCode` | Meaning | What to do | | :- | :- | :- | | `PDF_PAGE_LIMIT_EXCEEDED` | The PDF has more pages than the slot allows | Ask the customer for a shorter file, or just the relevant pages | | `PDF_PASSWORD_PROTECTED` | The PDF needs a password to open | Ask the customer to remove the password and upload again | | `PDF_UNREADABLE` | The file claims to be a PDF but cannot be parsed | Ask the customer to export the PDF again, or send an image instead where images are accepted | A file over 10 MB, or more than one file in a request, also returns `400`, with the reason in `validationErrors` and no `errorCode`. ## Other requirements Depending on the features enabled for your program, Meridian may collect more than the above, such as a settlement bank account for the destination currency, a destination wallet address, or a routing policy. The [Information Request API](/products/meridian-accounts/api-reference) returns the exact set of requirements for your program, including any of these. # Making a payout Source: https://docs.mnai.com/products/meridian-accounts/guides/making-a-withdrawal Register a beneficiary, then use the Transaction Intent API to preview and commit a payout from a Meridian balance. A payout moves funds from a customer's Meridian balance to a destination you have registered: a bank account, or a wallet address for stablecoin. The API records a payout as a transaction of type `WITHDRAWAL`. A payout has four steps: **1. Register the destination** Create a beneficiary with a payout instrument. You do this once per destination. **2. Create a transaction intent** Price the payout and get a preview. No funds move. **3. Commit the intent** Create the transaction and move the funds. **4. Track the payout** Follow the transaction to completion through the API or webhooks. ## Prerequisites Your program must use the `RETAIN` routing policy. The policy is set during enrollment and decides what happens to funds when they arrive: * **`RETAIN`**: deposits stay on the balance until you move them. This is what makes payouts possible. * **`SWEEP`**: Meridian forwards every deposit to a preconfigured destination. There is nothing to trigger, and this guide does not apply. > [!WARNING] > > If your program uses `SWEEP`, funds leave the balance on their own. Contact your Solutions Engineer if you need a `RETAIN` program. You also need: * an enrollment in `ACTIVE` status (see [Onboarding your customer](/products/meridian-accounts/guides/onboarding-your-customer)) * an access token for the program context with these scopes: `beneficiaries:create` and `beneficiaries:list` to register destinations, `accounts:list` to find the balance, `transactions:create` to create and commit intents, and `transactions:get` or `transactions:list` to track the payout * a funded balance to pay out from ## Step 1: Register the destination Register each destination once with [`POST /v1/beneficiaries`](/api-reference/beneficiaries/create-a-beneficiary). One call creates the beneficiary and its first payout instrument. The request is the same for both kinds of destination except for `instrument`: **Bank account (PHP)** ```json theme={null} { "type": "PERSON", "firstName": "Maria", "lastName": "Santos", "email": "maria.santos@example.com", "relationshipToUser": "THIRD_PARTY", "address": { "addressLine1": "12 Mabini Street", "city": "Makati", "stateProvince": "Metro Manila", "postalCode": "1200", "countryCode": "PH" }, "instrument": { "type": "BANK_ACCOUNT", "idempotencyKey": "c3d4e5f6-3333-4444-5555-666677778888", "bankAccount": { "displayName": "Maria PHP savings", "accountHolderName": "Maria Santos", "currency": "PHP", "country": "PH", "accountType": "SAVINGS", "accountNumber": "1234567890", "bankCode": "010999999", "bankName": "Example Bank of the Philippines" } } } ``` **Wallet (USDC)** ```json theme={null} { "type": "BUSINESS", "name": "Harbour Freight Holdings", "email": "payments@harbourfreight.example", "relationshipToUser": "THIRD_PARTY", "address": { "addressLine1": "1 Market Street", "city": "San Francisco", "stateProvince": "CA", "postalCode": "94105", "countryCode": "US" }, "instrument": { "type": "BLOCKCHAIN_WALLET", "idempotencyKey": "b2c3d4e5-2222-3333-4444-555566667777", "displayName": "Supplier settlement wallet", "walletAddress": "0x2d7c1f5a90b3E4c86d1A0f7b2C5e8D3a6F9b41C0", "currency": "USDC_ETH" } } ``` `currency` names the asset and its network. If you need cross-currency stablecoin payouts, such as USD to USDC, your Solutions Engineer must enable them for your program during onboarding. The response returns the beneficiary but not its instrument. Call [`GET /v1/beneficiaries/{beneficiaryId}/instruments`](/api-reference/beneficiaries/list-beneficiary-instruments) and take the instrument `id`. That is the `creditInstrumentId` for every payout to this destination. ```json GET /v1/beneficiaries/{beneficiaryId}/instruments theme={null} { "instruments": [ { "id": "uspay-pzrxbd1856x0tyxmdrkzhhml", "type": "BANK_ACCOUNT", "status": "ACTIVE", "displayName": "Maria PHP savings", "currency": "PHP", "mask": "7890" } ] } ``` > [!NOTE] > > `relationshipToUser` is `SELF` for the customer's own account or wallet, and `THIRD_PARTY` for anyone else. Your agreement with Meridian sets which beneficiary types and relationships your program may register. Confirm this before you build, since it decides who your customers can pay. ## Step 2: Create the intent [`POST /v1/transaction-intents`](/api-reference/transaction-intents/create-a-transaction-intent) prices the payout and returns a preview. No funds move. It needs two instrument IDs: * `debitInstrumentId` is the balance the funds leave from. Take its `id` from [`GET /v1/accounts`](/api-reference/accounts/list-accounts). * `creditInstrumentId` is the beneficiary instrument from Step 1. `fixedSide` says which side `amount` applies to: `DEBIT` for what leaves the balance, `CREDIT` for what the destination receives. `amount` is a decimal string. `idempotencyKey` is your unique key for this payout, and you send it again to commit. `creditRail`, `externalId`, `purpose`, and `memo` are optional; see the API reference for each. **USD to PHP** ```json Request theme={null} { "debitInstrumentId": "uspay-pyj5tywsw7v0pzuyjirxvso2", "creditInstrumentId": "uspay-pzrxbd1856x0tyxmdrkzhhml", "fixedSide": "DEBIT", "amount": "250.00", "purpose": "PERSONAL_REMITTANCE", "idempotencyKey": "payout-2026-10-12-0001", "externalId": "payout-2026-10-12-0001", "memo": "October remittance" } ``` The response is the preview, in `CREATED` status. `creditAmount` is `debitAmount` times `exchangeRate`, rounded to two decimal places. ```json Response (201) theme={null} { "id": "usrem-b2k3e9qijzs3hl83265bmuue", "type": "WITHDRAWAL", "status": "CREATED", "createdAt": "2026-10-12T09:05:00.000Z", "updatedAt": "2026-10-12T09:05:00.000Z", "debitAmount": "250.00", "debitCurrency": "USD", "debitMemo": "October remittance", "externalId": "payout-2026-10-12-0001", "debitInstrument": { "instrumentType": "ACCOUNT", "account": { "id": "uspay-pyj5tywsw7v0pzuyjirxvso2", "displayName": "USD Balance", "currency": "USD" } }, "creditAmount": "14121.88", "creditCurrency": "PHP", "creditInstrument": { "instrumentType": "BANK_ACCOUNT", "bankAccount": { "id": "uspay-pzrxbd1856x0tyxmdrkzhhml", "displayName": "Maria PHP savings", "currency": "PHP", "mask": "7890" } }, "fees": [], "exchangeRate": "56.4875", "purpose": "PERSONAL_REMITTANCE" } ``` **USD to USDC** ```json Request theme={null} { "debitInstrumentId": "uspay-pyj5tywsw7v0pzuyjirxvso2", "creditInstrumentId": "uspay-hjzngk997lxa9ujhzwuj1ggs", "fixedSide": "DEBIT", "amount": "250.00", "purpose": "PAYMENT_FOR_SERVICES", "idempotencyKey": "payout-2026-10-12-0002", "externalId": "payout-2026-10-12-0002", "memo": "Invoice 4471" } ``` The response is the preview, in `CREATED` status. The stablecoin side carries six decimal places. ```json Response (201) theme={null} { "id": "usrem-f7q2w9ntk4xp8vd3mz6hc1jb", "type": "WITHDRAWAL", "status": "CREATED", "createdAt": "2026-10-12T09:12:30.000Z", "updatedAt": "2026-10-12T09:12:30.000Z", "debitAmount": "250.00", "debitCurrency": "USD", "debitMemo": "Invoice 4471", "externalId": "payout-2026-10-12-0002", "debitInstrument": { "instrumentType": "ACCOUNT", "account": { "id": "uspay-pyj5tywsw7v0pzuyjirxvso2", "displayName": "USD Balance", "currency": "USD" } }, "creditAmount": "249.375000", "creditCurrency": "USDC_ETH", "creditInstrument": { "instrumentType": "EXTERNAL_WALLET", "externalWallet": { "id": "uspay-hjzngk997lxa9ujhzwuj1ggs", "displayName": "Supplier settlement wallet", "currency": "USDC_ETH", "mask": "41C0", "network": "ETHEREUM" } }, "fees": [], "exchangeRate": "0.9975", "purpose": "PAYMENT_FOR_SERVICES" } ``` No funds have moved. Show the customer `debitAmount`, `creditAmount`, `exchangeRate`, and `fees`, and keep the `id` for the commit. ## Step 3: Commit the intent `POST /v1/transaction-intents/{transactionIntentId}/commit` moves the funds. Send the **same** `idempotencyKey` you used to create the intent: ```json Request theme={null} { "idempotencyKey": "payout-2026-10-12-0001" } ``` ```json Response (200) theme={null} { "id": "usrem-b2k3e9qijzs3hl83265bmuue", "type": "WITHDRAWAL", "status": "COMMITTED", "createdAt": "2026-10-12T09:05:00.000Z", "updatedAt": "2026-10-12T09:05:41.000Z", "relatedTransactionId": "uspay-h4n2qz8kx1cvb7twm3rd9pol", "debitAmount": "250.00", "debitCurrency": "USD", "debitMemo": "October remittance", "externalId": "payout-2026-10-12-0001", "debitInstrument": { "instrumentType": "ACCOUNT", "account": { "id": "uspay-pyj5tywsw7v0pzuyjirxvso2", "displayName": "USD Balance", "currency": "USD" } }, "creditAmount": "14121.88", "creditCurrency": "PHP", "creditInstrument": { "instrumentType": "BANK_ACCOUNT", "bankAccount": { "id": "uspay-pzrxbd1856x0tyxmdrkzhhml", "displayName": "Maria PHP savings", "currency": "PHP", "mask": "7890" } }, "fees": [], "exchangeRate": "56.4875", "purpose": "PERSONAL_REMITTANCE" } ``` `relatedTransactionId` is the transaction Meridian created. Committing the same intent again with the same key returns the same result and never creates a second transaction. ## Step 4: Track the payout Follow the transaction, not the intent: * `GET /v1/transactions/{transactionId}` with `relatedTransactionId` * `GET /v1/transactions?externalId=payout-2026-10-12-0001` with your own reference * the [transaction webhook](/products/meridian-accounts/webhooks/webhooks-transaction), which sends `transaction_created` when the payout is recorded and `transaction_status_updated` as it moves to `COMPLETED` or `FAILED` ## Quotes expire An intent holds a quoted exchange rate, so it does not stay valid for long. The API does not return the expiry time, so commit as soon as the customer confirms. If the commit returns `400` with the message `Quote is expired`, create a new intent and commit that one. ## Sandbox notes * The create-beneficiary response does not include the instrument. Call the instruments endpoint to get its `id`. * Simulated deposits must be under \$1,000. * Each KYB run needs a unique business tax ID, or activation returns `ACTIVATION_REJECTED` even when the KYB decision is approved. # Onboarding your customer Source: https://docs.mnai.com/products/meridian-accounts/guides/onboarding-your-customer Enroll customers into a program, complete required onboarding steps, and activate their Meridian Account. A Meridian Account always exists within the context of a program. In Meridian, that program relationship is represented by an enrollment. Before your customer can use a Meridian Account, you must enroll them into the correct program, collect any required information, and wait for the enrollment to become `ACTIVE`. ## Prerequisites Before you implement onboarding, decide: * whether your integration is `SINGLE_USER` or `MULTI_USER` * whether the current request is using partner HMAC authentication or a Meridian JWT bearer token Those choices determine how the target user is identified before enrollment starts. Review: * [Choose your integration model](/products/meridian-accounts/guides/choose-your-integration-model) * [Authentication overview](/products/meridian-accounts/guides/authentication-overview) * [Server-to-server authentication with HMAC](/products/meridian-accounts/guides/server-to-server-authentication-with-hmac) * [Client-server authentication with JWT](/products/meridian-accounts/guides/client-server-authentication-with-jwt) ## How Meridian Account onboarding works Your integration is responsible for choosing the correct program for the customer. Once you create an access token in that program context, you can begin the enrollment orchestration for that customer. The Meridian user identity and the program enrollment are separate concerns. A customer may already have a Meridian `userId` but still need a new enrollment in the program you selected for the current flow. At a high level, the onboarding flow is: **1. Select the program** Determine which Meridian program the customer should be enrolled into. The program defines the context in which the Meridian Account will exist. **2. Create an access token** Call `POST /v1/auth/token` to create an access token for the customer in the selected program context. **3. Check the enrollment** Call `GET /v1/enrollment` to determine whether the customer already has an enrollment for that program. **4. Activate enrollment when needed** If the returned enrollment status is `INACTIVE`, call `POST /v1/enrollment/activate` to activate or resume the enrollment for that program. **5. Complete the Information Request** Meridian creates an Information Request, which is a schema-driven information collection system. Your integration should present and complete the latest Information Request until it is ready for submission. **6. Wait for ACTIVE status** After the Information Request is submitted and approved, the enrollment status moves to `ACTIVE` and Meridian sends a webhook notification. ## Enrollment is the program-level record Think of the enrollment as the record that connects a Meridian user to a specific program. The same customer may exist in your system independently of Meridian. A Meridian Account is not ready until that customer has an active enrollment in the target program. `GET /v1/enrollment` always returns the enrollment state for the current program context. If the status is `INACTIVE`, the customer still needs enrollment activation and/or onboarding completion before you can use the account. ## First-time enrollment flow Use this sequence for a customer who is entering the program for the first time: 1. Create an access token with `POST /v1/auth/token`. 2. Call `GET /v1/enrollment`. 3. If the enrollment status is `INACTIVE`, call `POST /v1/enrollment/activate`. 4. Read the returned enrollment state and `latestInformationRequest` details. 5. Use the Information Request endpoints to collect the required customer information. 6. Submit the Information Request for review. 7. Listen for webhook updates until the enrollment becomes `ACTIVE`. A newly activated enrollment is typically returned as `INACTIVE` with a `latestInformationRequest` while onboarding is still in progress. The enrollment exists, but the customer still has required information to complete before the account can be used. ## Understanding the Information Request In Meridian Accounts, an Information Request is the mechanism used to collect the information and documentation required to complete enrollment. The Information Request is schema-driven, which means your integration should use the structure returned by the API to determine: * which steps to show * which fields are required * whether documents must be uploaded * whether the Information Request is complete and ready to submit Depending on the enrollment requirements, the Information Request may ask for business details, controller details, beneficial ownership information, or supporting documents. For a walkthrough of what Meridian collects and how to prepare it, see [Information we collect](/products/meridian-accounts/guides/kyb-information). ## Information Request lifecycle Use the Information Request endpoints to drive onboarding from the current Meridian state instead of hardcoding your own form flow. ### 1. Find the active Information Request After you activate an enrollment, check the enrollment response for `latestInformationRequest`. If the customer already has an in-progress onboarding flow, use that Information Request ID to continue from the existing state. You can also call `GET /v1/information-requests` to list Information Requests in the current program context, then call `GET /v1/information-requests/{informationRequestId}` to load the full schema for the latest Information Request. ### 2. Render the Information Request from the schema Use `GET /v1/information-requests/{informationRequestId}` to load the current Information Request definition. The response is organized into steps and requirements. Use that response to determine: * which sections to show * which fields belong to each section * whether a requirement has a single entry or repeated entries * how each field must be collected * whether each step, requirement, or field is currently `MISSING`, `INVALID`, or `VALID` Do not assume every customer sees the same fields or the same number of steps. Render the onboarding flow from the returned schema. ### 3. Use the two collection modes Each Information Request item tells you how Meridian expects that field to be collected by using `collectionMode`. The two collection modes are: * `DIRECT_INPUT`: collect a value directly in your UI and send it with `PATCH /v1/information-requests/{informationRequestId}` * `FILE_UPLOAD`: collect one or more files and upload them with `POST /v1/information-requests/{informationRequestId}/file-uploads` For `DIRECT_INPUT` items, use the `directInput` schema on the item to understand the expected value shape. Meridian uses the JSON Schema standard for this definition. A direct-input item may represent a single value, or a structured object whose data fields you must collect together. For `FILE_UPLOAD` items, use the `fileUpload` configuration on the item to enforce Meridian's file requirements, including: * minimum and maximum file count * allowed MIME types * allowed file extensions * maximum file size * maximum number of pages for a PDF Meridian checks the page limit when you upload. Meridian rejects a PDF over the limit, or one that needs a password to open, with a `400`. See [Upload rules](/products/meridian-accounts/guides/kyb-information#upload-rules) for the limits and error codes. Treat the API response as the source of truth for collection behavior. If an item is marked `DIRECT_INPUT`, do not treat it as a document upload. If it is marked `FILE_UPLOAD`, do not try to satisfy it with a text field. ### 4. Handle single and repeated requirements Each requirement also tells you whether it is `SINGLE` or `MULTIPLE`. * `SINGLE` means the requirement has one set of values * `MULTIPLE` means the customer may need to provide repeated entries, such as multiple beneficial owners For `MULTIPLE` requirements, use the metadata returned by the API to guide the experience: * `addInstanceLabel` to label the add action * `instanceHeaderPrefix` to label each repeated entry * `minInstanceCount` and `maxInstanceCount` to control how many entries are allowed * `instances` to render the values already collected When updating a repeated requirement, include `instanceIndex` in each patch item so Meridian can associate the value with the correct entry. ### 5. Save values and re-read validation Use `PATCH /v1/information-requests/{informationRequestId}` to save direct-input values as the customer progresses through onboarding. Each patch item identifies: * `requirementKey` for the requirement being updated * `itemKey` for the specific field * `value` for the field value * `instanceIndex` when the requirement supports multiple entries After saving values or uploading files, read the updated Information Request response and use Meridian's validation state to decide what happens next. The API guides valid collection in three ways: * step-level `validationStatus` shows whether the section is complete * requirement-level `validationStatus` shows whether that requirement is satisfied * field-level validation on current values shows whether an individual value is missing, invalid, or valid Use these validation states to decide: * which errors to show * which sections still need attention * whether the customer can continue * whether the Information Request is ready to submit Do not decide completeness from local form rules alone. Use the validation returned by Meridian as the source of truth. ### 6. Submit for review When the Information Request is complete, call `POST /v1/information-requests/{informationRequestId}/submit` to send it for review. A submitted Information Request does not mean the customer is fully onboarded yet. After submission, the Information Request may move through review before the enrollment becomes `ACTIVE`. ### 7. Wait for status updates After submission, continue monitoring both enrollment and Information Request state. Your integration should: * consume `information_request_status_updated` webhooks to detect review progress * consume `enrollment_status_updated` webhooks to know when onboarding is complete * treat enrollment `ACTIVE` as the final signal that the Meridian Account is ready to use If your system needs to recover state after a webhook delivery issue, reload the enrollment and latest Information Request from the API and continue from the latest returned state. ## When the account is ready The Meridian Account is ready for use only after the enrollment reaches `ACTIVE`. Until then, the customer is still in onboarding even if Meridian has created the enrollment record. Once the enrollment becomes `ACTIVE`: * the required onboarding flow is complete * Meridian sends an enrollment webhook * your integration can treat the Meridian Account as ready for use ## Recommended integration behavior Your integration should: * store the program context used for onboarding * treat enrollment status `INACTIVE` as the condition for calling `POST /v1/enrollment/activate` * drive the onboarding UI from the latest Information Request schema * treat enrollment status `ACTIVE` as the final ready-for-use signal, and `INACTIVE` as the activation or onboarding-in-progress state * wait for enrollment `ACTIVE` before enabling account functionality * consume enrollment and Information Request webhooks so your system stays synchronized with Meridian state ## Next steps * [Information we collect](/products/meridian-accounts/guides/kyb-information) After implementing this flow, review: * [Getting started](/products/meridian-accounts/guides/getting-started) * [Choose your integration model](/products/meridian-accounts/guides/choose-your-integration-model) * [Authentication overview](/products/meridian-accounts/guides/authentication-overview) * [API reference overview](/products/meridian-accounts/api-reference) * [Webhooks overview](/products/meridian-accounts/webhooks) * [Enrollment webhooks](/products/meridian-accounts/webhooks/webhooks-enrollment) * [Information Request webhooks](/products/meridian-accounts/webhooks/webhooks-information-request) # Server-to-server authentication with HMAC Source: https://docs.mnai.com/products/meridian-accounts/guides/server-to-server-authentication-with-hmac Use Meridian partner credentials and an HMAC SHA-256 signature when your backend calls the Meridian Accounts API directly. Use partner HMAC authentication when your backend calls Meridian directly with a Meridian API Key and Secret. This is the mechanism that proves partner identity. It is required for `POST /v1/auth/token` and for any other API operation that supports partner-key authentication. ## Required headers Partner-authenticated requests use these headers: | Header | Required | Use | | :- | :- | :- | | `X-Meridian-Api-Key` | Yes | Issued by Meridian for the environment | | `X-Meridian-Program-Id` | Yes | Identifies the program context for the request | | `X-Meridian-Timestamp` | Yes | Timestamp used to validate request freshness and the signature | | `X-Meridian-Signature` | Yes | HMAC SHA-256 signature for the request | | `X-Meridian-User-Id` | `MULTI_USER` only | Identifies the Meridian user targeted by the request | For `SINGLE_USER` integrations, omit `X-Meridian-User-Id`. Meridian uses the single configured user for that partner key. ## When to send `X-Meridian-User-Id` Your integration model determines whether `X-Meridian-User-Id` is required: * `MULTI_USER`: required on partner-key calls * `SINGLE_USER`: must be omitted on partner-key calls This rule applies to token creation and to other partner-key API calls that accept direct partner authentication. ## Constructing the signature Meridian authenticates partner-key requests with an HMAC SHA-256 signature. `X-Meridian-Program-Id` is required on the request, but it is not part of the canonical string used for signing. ### Canonical string Build the canonical string by concatenating these values in this exact order: 1. `API Key` 2. `Timestamp` 3. `HTTP method` 4. `Request path` 5. `Request body` For example, for `POST /v1/auth/token`: * the method is `POST` * the path is `/v1/auth/token` * the body is the exact JSON string sent on the wire If there is no request body, use an empty string for the body portion of the canonical string. ### How to compute the signature 1. Read your Meridian API Key and API Secret for the environment you are calling. 2. Generate a timestamp in milliseconds and send it as `X-Meridian-Timestamp`. 3. Serialize the JSON request body exactly as it will be sent. 4. Concatenate the API Key, timestamp, method, path, and serialized body to form the canonical string. 5. Compute the HMAC SHA-256 of that canonical string using your API Secret as the key. 6. Hex-encode the result and send it as `X-Meridian-Signature`. ### Example If your request is: ```http theme={null} POST /v1/auth/token X-Meridian-Api-Key: mer_test_123 X-Meridian-Program-Id: prog_abc123 X-Meridian-User-Id: user_123 X-Meridian-Timestamp: 1735689600000 Content-Type: application/json ``` ```json theme={null} { "grant_type": "client_credentials" } ``` Then the canonical string is: ```text theme={null} mer_test_1231735689600000POST/v1/auth/token{"grant_type":"client_credentials"} ``` Compute the HMAC SHA-256 of that canonical string using your API Secret, then hex-encode the output and send the result in `X-Meridian-Signature`. ### Example code ```javascript javascript theme={null} import crypto from "crypto"; const apiKey = process.env.MERIDIAN_API_KEY; const apiSecret = process.env.MERIDIAN_API_SECRET; const programId = process.env.MERIDIAN_PROGRAM_ID; const method = "POST"; const path = "/v1/auth/token"; const timestamp = Date.now().toString(); const body = JSON.stringify({ grant_type: "client_credentials", }); const canonical = apiKey + timestamp + method + path + body; const signature = crypto .createHmac("sha256", apiSecret) .update(canonical) .digest("hex"); const headers = { "Content-Type": "application/json", "X-Meridian-Api-Key": apiKey, "X-Meridian-Program-Id": programId, "X-Meridian-User-Id": "user_123", "X-Meridian-Timestamp": timestamp, "X-Meridian-Signature": signature, }; ``` ```kotlin kotlin theme={null} import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec fun buildMeridianTokenHeaders( apiKey: String, apiSecret: String, programId: String, userId: String?, body: String ): Map { val method = "POST" val path = "/v1/auth/token" val timestamp = System.currentTimeMillis().toString() val canonical = apiKey + timestamp + method + path + body val mac = Mac.getInstance("HmacSHA256").apply { init(SecretKeySpec(apiSecret.toByteArray(), "HmacSHA256")) } val signature = mac.doFinal(canonical.toByteArray()) .joinToString("") { "%02x".format(it) } return buildMap { put("Content-Type", "application/json") put("X-Meridian-Api-Key", apiKey) put("X-Meridian-Program-Id", programId) if (userId != null) { put("X-Meridian-User-Id", userId) } put("X-Meridian-Timestamp", timestamp) put("X-Meridian-Signature", signature) } } ``` ```bash bash theme={null} API_KEY="mer_test_123" API_SECRET="your_api_secret" PROGRAM_ID="prog_abc123" USER_ID="user_123" TIMESTAMP="$(python3 -c 'import time; print(int(time.time() * 1000))')" BODY='{"grant_type":"client_credentials"}' CANONICAL="${API_KEY}${TIMESTAMP}POST/v1/auth/token${BODY}" SIGNATURE="$(printf '%s' "$CANONICAL" | openssl dgst -sha256 -hmac "$API_SECRET" | sed 's/^.* //')" ``` ## Token minting flow Use `POST /v1/auth/token` when your backend needs a Meridian access token for a user. * `SINGLE_USER`: omit `X-Meridian-User-Id`; Meridian uses the user configured for that partner integration * `MULTI_USER`: include `X-Meridian-User-Id` to identify the target Meridian user Depending on your flow, the token request body can: * use `client_credentials` for a target user * use `refresh_token` to exchange a refresh token for a new access token For a first-time `MULTI_USER` flow, create the Meridian user first with `POST /v1/auth/users`, then mint the token for that `userId`. If `POST /v1/auth/users` fails because `externalId` already belongs to an existing Meridian user, retry token creation only if you already know the stored `userId` from your own customer-to-user mapping. ## Partner-key access beyond token minting Some Meridian Accounts endpoints support direct partner-key authentication in addition to JWT bearer authentication. When the OpenAPI for an endpoint lists both partner API Key security and JWT security, your backend may call that endpoint directly with HMAC-signed partner credentials. For client-facing account access, prefer JWT bearer authentication even when partner-key auth is technically supported. ## Common authentication errors The most common causes of HMAC authentication failures are: * omitting `X-Meridian-Api-Key` or another required partner header * concatenating canonical string fields in the wrong order * signing a different JSON string than the one actually sent * including `X-Meridian-Program-Id` in the canonical string * sending `X-Meridian-User-Id` for a `SINGLE_USER` integration * omitting `X-Meridian-User-Id` for a `MULTI_USER` integration * using a timestamp outside the allowed 60-second window * using Sandbox credentials against production, or production credentials against Sandbox > [!WARNING] > > The signature is sensitive to field order, whitespace, and request path. If > the request body or path differs from what you signed, Meridian will reject > the request. ## Related guides * [Choose your integration model](/products/meridian-accounts/guides/choose-your-integration-model) * [Authentication overview](/products/meridian-accounts/guides/authentication-overview) * [Client-server authentication with JWT](/products/meridian-accounts/guides/client-server-authentication-with-jwt) # Account Source: https://docs.mnai.com/products/meridian-accounts/webhooks/webhooks-account products/meridian-accounts/webhooks/webhooks.json webhook account Learn when account webhooks are sent and what data they include. # Enrollment Source: https://docs.mnai.com/products/meridian-accounts/webhooks/webhooks-enrollment products/meridian-accounts/webhooks/webhooks.json webhook enrollment Learn when enrollment webhooks are sent and what data they include. # Information Request Source: https://docs.mnai.com/products/meridian-accounts/webhooks/webhooks-information-request products/meridian-accounts/webhooks/webhooks.json webhook information_request Learn when Information Request webhooks are sent and what data they include. # Overview Source: https://docs.mnai.com/products/meridian-accounts/webhooks/webhooks-overview Learn how Meridian Accounts delivers webhook notifications, including security, retries, and delivery behavior. ## Configuration Meridian configures webhook endpoints during provisioning. Each event type has exactly one URL. You can point several event types at the same URL, or give each its own handler. There is no fallback between event types and no default URL, so Meridian does not deliver events for an event type with no URL configured. ## Event routing Use the `X-Meridian-Resource-Type` and `X-Meridian-Event-Type` headers to identify the webhook before you parse the body. These headers tell you which resource the event belongs to and which schema to use. | Header | Example values | Use | | :- | :- | :- | | `X-Meridian-Resource-Type` | `enrollment`, `information_request`, `transaction`, `account` | Routes the payload to the correct resource handler | | `X-Meridian-Event-Type` | See the event types table below | Selects the specific event parser for that resource | | Resource | Event types | | :- | :- | | `enrollment` | `enrollment_status_updated` | | `information_request` | `information_request_status_updated` | | `transaction` | `transaction_created`, `transaction_status_updated` | | `account` | `account_created`, `account_status_updated` | Read these headers first, then deserialize the request body with the matching webhook schema. This lets your handler reject unexpected combinations early and keeps your routing logic aligned with the payload shape. ## Payload shape Every body is a JSON object with two keys: the resource, and `metadata`. The resource key matches `X-Meridian-Resource-Type` in camel case, for example `informationRequest`. There is no other envelope. ```json theme={null} { "account": { "id": "uspay-e1vitcef84g1ekzio62mwe3x", "status": "ACTIVE", "...": "..." }, "metadata": { "userId": "ususr-c2yulm8b8phkkjpbzra2s263", "programId": "usrem-bvjxs5xuca4oa093pax3p6p2" } } ``` Meridian handles empty fields differently by resource: * **Enrollment and Information Request** send every key. A field with no value is `null`. * **Transaction and account** leave out fields with no value. Timestamps are always UTC with milliseconds, for example `2026-09-29T14:00:00.000Z`. ## Security Meridian signs every webhook with HMAC SHA-256, using a Webhook API Key and Secret. Meridian issues these separately from your REST API credentials, and one pair covers every webhook event. Each request includes the following headers: | Header | Description | | :- | :- | | `X-Meridian-Api-Key` | Your webhook API key. Use it to choose the secret to verify with. | | `X-Meridian-Timestamp` | When Meridian signed this delivery attempt, in milliseconds since the Unix epoch. Each retry is signed again. | | `X-Meridian-Signature` | Lowercase hex HMAC SHA-256 signature. | To verify a request, build this string and compute its HMAC SHA-256 with your webhook secret: ```text theme={null} apiKey + timestamp + "POST" + path + body ``` * `apiKey` and `timestamp` are the header values. * `path` is the path of your webhook URL, without the query string. For `https://example.com/hooks/meridian?env=prod` it is `/hooks/meridian`. * `body` is the raw request body, exactly as received. Verify before you parse it. Compare the result with `X-Meridian-Signature`, and reject the request if it does not match or a header is missing. To guard against replayed requests, also reject a timestamp that is far from your own clock. Webhooks use the same method as [Server-to-server authentication with HMAC](/products/meridian-accounts/guides/server-to-server-authentication-with-hmac), with two differences: the path never includes a query string, and there are no `X-Meridian-Program-Id` or `X-Meridian-User-Id` headers. ## Delivery and retries Meridian considers a webhook delivered when your endpoint returns an HTTP `2xx` response within **10 seconds**. Meridian retries any other result: a timeout, a connection error, or a non-`2xx` status, including `4xx` and `429`. Retries use exponential backoff. The wait starts at 2 seconds and doubles each time, up to 200 seconds. Meridian makes at most **10 attempts** over about 12 minutes, then stops. Return a `2xx` as soon as you have stored the event, and do the rest of the work afterwards. If your endpoint responds slowly, the request can time out and Meridian delivers the event again. Webhooks are best effort. An event can occasionally be lost before the first delivery attempt, so reconcile with the `GET` endpoints from time to time rather than relying on webhooks alone. ## Duplicates and ordering You can receive the same event more than once. Retries redeliver it, and Meridian sends some events again without any change: * `transaction_status_updated` is sent every time a status is written, even when it is the same status. * `account_status_updated` is sent after any change to an account, not only a status change. * `enrollment_status_updated` can repeat with `ACTIVE`, for example when a feature is added. Webhooks carry no event ID. Make your handler idempotent by deduplicating on the resource `id`, `X-Meridian-Event-Type`, `status`, and `updatedAt`. Events can also arrive out of order. Store the `updatedAt` you already have for each resource, and ignore an event whose `updatedAt` is older. For example, an Information Request moves from `PROCESSING_SUBMISSION` to `PENDING_REVIEW` to `COMPLETED`, but you may receive `COMPLETED` first. # Transaction Source: https://docs.mnai.com/products/meridian-accounts/webhooks/webhooks-transaction products/meridian-accounts/webhooks/webhooks.json webhook transaction Learn when transaction webhooks are sent and what data they include. # Authentication Source: https://docs.mnai.com/products/payout-origination/authentication Secure your API requests using HMAC SHA-256 signatures with pre-shared API keys, including step-by-step instructions and code examples. * All API calls use an HMAC authentication strategy with pre-shared API and secret keys. * Sandbox and production environments have different API credentials. Do not reuse across environments. * All requests must use HTTPS (TLS 1.2 or higher). * You must allow outbound connections to the Payout Origination API base URLs. * IP whitelisting may be required for production access depending on your program configuration. Meridian provides details if applicable. Every request must include the following headers: | Header | Description | | :- | :- | | `X-Meridian-Api-Key` | Issued by Meridian | | `X-Meridian-Program-Id` | Identifies your Meridian program | | `X-Meridian-Timestamp` | Current time in milliseconds (must be within 60 seconds of the request) | | `X-Meridian-Signature` | HMAC SHA-256 signature | > [!NOTE] > > Meridian issues your API Key and Secret during Sandbox and production provisioning. When Meridian receives the request, it validates the signature. If the signature does not match, the API immediately returns a `401 Unauthorized` response. ## Constructing the signature **1. Build the canonical string** Compute the signature as an HMAC SHA-256 hash over a canonical string that concatenates the following values in order: | Signature component | Notes | | - | - | | `API-Key` | Same value as `X-Meridian-Api-Key` | | `Timestamp` | Same value as `X-Meridian-Timestamp` | | `Request method` | Example: `GET`, `POST`, `PATCH` | | `Request path` | Including any parameters. For example: `/v1/payout-origination/{format}`, `/v1/transactions`, or `/v1/transactions/txn_01JVY8Y4N9X2M6S5Q7T1` | | `Body contents` | The exact request body sent on the wire. For Payout Origination POST requests, this is the encoded payload. If there is no body, this can be an empty string. | `X-Meridian-Program-Id` is required on every request but is not included in the canonical signing string. **2. Compute the HMAC** Compute the HMAC using the language-appropriate crypto library with the secret key. > [!WARNING] > > Make sure you compute the HMAC with the API Secret that Meridian issued to you. **3. Compute the SHA-256 hash** Hash the final value using the SHA-256 algorithm and set it as the `X-Meridian-Signature` header. ```kotlin kotlin theme={null} import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec fun buildMeridianSignature(apiKey: String, apiSecret: String, programId: String, method: String, path: String, body: String = ""): Map { val timestamp = System.currentTimeMillis().toString() val canonical = apiKey + timestamp + method + path + body val mac = Mac.getInstance("HmacSHA256").apply { init(SecretKeySpec(apiSecret.toByteArray(), "HmacSHA256")) } val signature = mac.doFinal(canonical.toByteArray()) .joinToString("") { "%02x".format(it) } return mapOf( "X-Meridian-Api-Key" to apiKey, "X-Meridian-Program-Id" to programId, "X-Meridian-Timestamp" to timestamp, "X-Meridian-Signature" to signature, "Content-Type" to "text/plain" ) } ``` ```swift swift theme={null} import CryptoKit import Foundation func buildMeridianSignature(apiKey: String, apiSecret: String, programId: String, method: String, path: String, body: String = "") -> [String: String] { let timestamp = String(Int(Date().timeIntervalSince1970 * 1000)) let canonical = apiKey + timestamp + method + path + body let key = SymmetricKey(data: Data(apiSecret.utf8)) let signature = HMAC.authenticationCode(for: Data(canonical.utf8), using: key) .map { String(format: "%02x", $0) }.joined() return [ "X-Meridian-Api-Key": apiKey, "X-Meridian-Program-Id": programId, "X-Meridian-Timestamp": timestamp, "X-Meridian-Signature": signature, "Content-Type": "text/plain" ] } ``` ```javascript postman theme={null} // ─── Config ────────────────────────────────────────────────────────────────── // Set these as collection variables (never hardcode secrets) const apiKey = pm.variables.get("meridianApiKey"); const apiSecret = pm.variables.get("meridianApiSecret"); // ─── Timestamp (milliseconds) ───────────────────────────────────────────────── const timestamp = Date.now().toString(); // ─── Request components ─────────────────────────────────────────────────────── const method = pm.request.method; // e.g. "POST" const url = pm.request.url; const qs = url.getQueryString(); const path = url.getPath() + (qs ? "?" + qs : ""); // e.g. "/v1/payout-origination/pacs008" or "/v1/transactions" const body = pm.request.body?.raw ?? ""; // raw Base64 payload for POST requests // ─── Canonical string ───────────────────────────────────────────────────────── const canonical = apiKey + timestamp + method + path + body; // ─── HMAC-SHA256 signature ──────────────────────────────────────────────────── const signature = CryptoJS.HmacSHA256(canonical, apiSecret).toString(CryptoJS.enc.Hex); // ─── Inject into request headers ───────────────────────────────────────────── pm.variables.set("meridianTimestamp", timestamp); pm.variables.set("meridianSignature", signature); pm.request.headers.add({ key: "X-Meridian-Program-Id", value: pm.variables.get("meridianProgramId") }); // ─── Debug ──────────────────────────────────────────────────────────────────── console.log("Timestamp :", timestamp); console.log("Canonical :", canonical); console.log("Signature :", signature); ``` ## Common errors > [!WARNING] > > Meridian returns 401 Unauthorized for requests with missing or invalid signatures. The most common causes are: * Canonical string fields concatenated in the wrong order. * Timestamp too far from the server's current time. * Forgetting to compute the HMAC with your unique Secret Key. * Signing a different Base64 string than the one actually sent. * Forgetting to SHA-256 hash the final string. * Including `X-Meridian-Program-Id` in the canonical string. # Base64 payload encoding Source: https://docs.mnai.com/products/payout-origination/base64-payload-encoding Learn how to prepare the Base64-encoded payload you send to the Payout Origination API. Use this guide when you need to turn a complete message payload into the `text/plain` request body the Payout Origination API accepts. The API does not accept raw plaintext XML or JSON. You must Base64-encode the full source payload and send the encoded payload as the raw request body. ## What you encode Encode the complete payload you want Meridian to process. That means: * finish building the payload first * validate the payload content before encoding * encode the full payload as one artifact * send the encoded result as-is Do not: * wrap the source payload in another envelope before encoding * split the message into separate fields * encode only part of the payload * modify the encoded payload after it is produced > [!WARNING] > > Base64 encoding is not encryption. It preserves the source payload as plain > reversible data. Rely on HTTPS, access controls, and careful logging > practices to protect message contents. ## Recommended flow **1. Build the final payload** Produce the exact payload Meridian should process. If you need the expected message shape for your format, review [Supported formats](/products/payout-origination/supported-formats). **2. Encode the payload as Base64** Base64-encode the full payload and produce a single ASCII string. Send the encoded payload directly as the HTTP request body. **3. Submit with the correct headers** Send the body with: * `Content-Type: text/plain` * Meridian HMAC authentication headers * a separate `Idempotency-Key` header ## Encoding guidance Apply Base64 to the exact bytes of the source payload you want Meridian to process. That means: * use a stable character encoding such as UTF-8 for the source payload * Base64-encode the entire payload in one step * hash and sign the exact Base64 string that you send * avoid inserting extra whitespace, line wrapping, or formatting changes after encoding ## Example payloads Example XML to encode: ```xml theme={null}
20260407-000123
``` Base64-encode it: ```bash theme={null} base64 -i message.xml ``` Example result: ```text theme={null} PE1lc3NhZ2U+CiAgPEhlYWRlcj4KICAgIDxJZD4yMDI2MDQwNy0wMDAxMjM8L0lkPgogIDwvSGVhZGVyPgo8L01lc3NhZ2U+ ``` Send that Base64 string as the raw HTTP body. Example JSON to encode: ```json theme={null} { "entityId": "ent_01JVY8Y4N9X2M6S5Q7T1", "beneficiaryId": "bnf_01JVY8Y4N9X2M6S5Q7T2", "amount": "1250.00", "currency": "USD", "reference": "invoice-100045" } ``` ## Handling guidance * Keep the source payload in a stable encoding such as UTF-8 before Base64-encoding it. * Treat the Base64 output as an opaque string. * Send the encoded payload as the raw request body, not as a JSON property. * Log identifiers such as `messageId` and your own business reference instead of logging raw payment contents. * Reuse the same Base64 payload only when retrying the same logical submission with the same idempotency intent. ## Common mistakes | Mistake | Result | | :- | :- | | Sending plaintext XML with `application/xml` | Meridian rejects the request because the API expects `text/plain` with Base64 content | | Sending plaintext JSON with `application/json` | Meridian rejects the request because the API expects `text/plain` with Base64 content | | Wrapping the Base64 string in JSON | Signature and body handling become inconsistent with the documented request shape | | Encoding a partial payload | Meridian receives an artifact that does not represent the original business message | | Changing the body after signing | HMAC verification fails | ## Relationship to retrieval After Meridian accepts the request, you can query the latest processing state with `GET /v1/transactions`, `GET /v1/transactions?messageId=...`, or `GET /v1/transactions/{id}`. These GET endpoints return transaction records and status information. They do not return the stored Base64 payload, and they do not replace your responsibility to manage the original source payload safely in your own systems where required. ## Related pages * [Overview](/products/payout-origination/overview) * [Supported formats](/products/payout-origination/supported-formats) * [Authentication](/products/payout-origination/authentication) # JSON format Source: https://docs.mnai.com/products/payout-origination/json-format Understand the Base64-encoded JSON payload Meridian accepts when you submit `POST /v1/payout-origination/json`. Use this page when you want to originate a payout from a Base64-encoded JSON payload instead of an ISO 20022 XML document. > [!NOTE] > > Use `json` as the `{format}` path parameter when submitting this message type: > `POST /v1/payout-origination/json` Meridian accepts a single JSON object as the source payload for this format. Build the JSON document first, then Base64-encode that exact JSON string and send it as the raw `text/plain` request body. ## Meridian intake requirements | Item | Requirement | | :- | :- | | Path slug | `json` | | Document format | JSON object | | Submission model | Base64-encode the full JSON payload and send it as the request body | | Content type | `text/plain` | | Required fields | `entityId`, `beneficiaryId`, `amount`, `currency`, `reference` | | Required values | All five fields must be present and each field must have a non-empty value | | Transformations | Do not wrap the payload in another JSON envelope after encoding | ## Required payload shape The decoded JSON payload must contain all of these fields. All values are required. | Field | Type | Description | | :- | :- | :- | | `entityId` | string | Meridian entity identifier for the payout originator | | `beneficiaryId` | string | Meridian beneficiary identifier that should receive the payout | | `amount` | string or number | Payout amount before Base64 encoding | | `currency` | string | Three-letter payout currency code | | `reference` | string | Your business reference for the payout | ## Example JSON payload ```json theme={null} { "entityId": "ent_01JVY8Y4N9X2M6S5Q7T1", "beneficiaryId": "bnf_01JVY8Y4N9X2M6S5Q7T2", "amount": "1250.00", "currency": "USD", "reference": "invoice-100045" } ``` ## Practical expectations When you prepare a JSON payout origination payload: * include all five required fields with values * use the exact `entityId` and `beneficiaryId` values Meridian assigned * preserve the JSON bytes you intend to submit before encoding * Base64-encode the complete JSON object in one step * send the encoded output as the raw request body Do not: * send plaintext JSON with `application/json` * split the payload fields into query parameters or headers * Base64-encode one field at a time * wrap the Base64 string in another JSON property ## Before you submit Before calling `POST /v1/payout-origination/json`: 1. Build the final JSON object with `entityId`, `beneficiaryId`, `amount`, `currency`, and `reference`. 2. Validate that all five fields are present and that all five values are populated in your own system. 3. Base64-encode the full JSON document. 4. Send the encoded payload with HMAC authentication headers and a separate `Idempotency-Key` header. ## Transaction model mapping Use this section to understand how Meridian maps the decoded JSON payload into the transaction model returned by `GET /v1/transactions` and `GET /v1/transactions/{id}`. | Transaction field | JSON source | Notes | | :- | :- | :- | | `externalId` | `reference` | Meridian exposes your reference as the business-facing external identifier | | `amounts.debit.amount` | `amount` | Uses the submitted payout amount | | `amounts.debit.currency` | `currency` | Uses the submitted currency | | `amounts.credit.amount` | `amount` | Currently mirrors the same payout amount | | `amounts.credit.currency` | `currency` | Currently mirrors the same payout currency | | `creditInstrument.beneficiaryAccount.id` | `beneficiaryId` | Uses the beneficiary identifier in the payload | | `source.type` | Internal value `external_submission` | Current documented behavior for JSON-ingested transactions | Meridian assigns these transaction fields directly; they are not sourced from the JSON payload: * `id` * `messageId` * `status` * `statusReason` * `createdAt` * `updatedAt` * `relatedTransactionId` ## Related pages * [Supported formats](/products/payout-origination/supported-formats) * [Base64 payload encoding](/products/payout-origination/base64-payload-encoding) * [Authentication](/products/payout-origination/authentication) # Overview Source: https://docs.mnai.com/products/payout-origination/overview Submit payout requests to Meridian for asynchronous validation and processing. Use the Payout Origination API when you need to deliver a payout message to Meridian as a Base64-encoded payload. You Base64-encode the original message payload, sign the request with Meridian HMAC headers, and submit it to the ingestion endpoint. Meridian accepts the encoded payload, stores it, and processes it asynchronously. If you need help building the request body, see [Base64 payload encoding](/products/payout-origination/base64-payload-encoding). ## What this API does The Payout Origination API is designed for one job: receiving a full message as an encoded payload and moving it into Meridian's processing pipeline. Use this API when you need to: * send a complete payment payload without splitting it across API fields * preserve the exact message bytes while using a simple text transport format * list resulting transactions and read their latest processing status * protect against duplicate submissions with an idempotency key * receive an acknowledgement immediately instead of waiting for full processing to finish > [!WARNING] > > Base64 encoding does not encrypt the message or provide confidentiality. Use > HTTPS in transit and avoid logging or exposing the raw payload in client and > server logs. ## Request flow **1. Base64-encode the message payload** Encode the complete source payload as a Base64 string. Send the encoded payload as the raw request body with `Content-Type: text/plain`. **2. Sign the request** Authenticate the request with Meridian HMAC headers: * `X-Meridian-Api-Key` * `X-Meridian-Timestamp` * `X-Meridian-Signature` **3. Add an idempotency key** Send `Idempotency-Key` as a separate request header. Use a unique `Idempotency-Key` for each business submission so retries do not create duplicates. **4. Submit the message** Send `POST /v1/payout-origination/{format}`, where `format` identifies the message type. Meridian validates the headers, checks that the body is valid Base64, stores the encoded payload, and queues the message for asynchronous processing. **5. Track processing status** A successful request returns `202 Accepted` with a Meridian `messageId`, a `transactionId`, an initial `RECEIVED` status, and a `receivedAt` timestamp. Meridian then continues downstream processing after the API request completes. ## What a successful response means `202 Accepted` means Meridian has safely accepted the encoded payload for processing. It does not mean the message has already been validated or completed. Store the returned `messageId` and `transactionId`. Use them for later status tracking, reconciliation, and transaction lookup. Example accepted response: ```json theme={null} { "messageId": "msg_01JVY8Y4N9X2M6S5Q7T1", "transactionId": "txn_01JVY8Y4N9X2M6S5Q7T1", "status": "RECEIVED", "receivedAt": "2026-04-07T20:11:42Z" } ``` ## Endpoint summary ### Supported format values | `{format}` value | Source payload | | :- | :- | | `pacs008` | ISO 20022 `pacs.008.*` XML document | | `json` | JSON object with `entityId`, `beneficiaryId`, `amount`, `currency`, and `reference` | ### Submit an encoded message | Item | Value | | :- | :- | | Method | `POST` | | Path | `/v1/payout-origination/{format}` | | Content type | `text/plain` | | Request body | Base64 string containing the full message payload | | Authentication headers | `X-Meridian-Api-Key`, `X-Meridian-Timestamp`, `X-Meridian-Signature` | | Idempotency header | `Idempotency-Key` | | Success response | `202 Accepted` | ### List transactions | Item | Value | | :- | :- | | Method | `GET` | | Path | `/v1/transactions` | | Authentication headers | `X-Meridian-Api-Key`, `X-Meridian-Timestamp`, `X-Meridian-Signature` | | Optional query parameters | `pageIndex`, `pageSize`, `messageId` | | Success response | `200 OK` | | Response body | JSON containing paginated transaction records with their latest processing status | ### Get a transaction by ID | Item | Value | | :- | :- | | Method | `GET` | | Path | `/v1/transactions/{id}` | | Authentication headers | `X-Meridian-Api-Key`, `X-Meridian-Timestamp`, `X-Meridian-Signature` | | Success response | `200 OK` | | Response body | JSON containing the transaction record and latest processing status | ## Error handling Build your client to handle these common response classes: | Status | Meaning | | :- | :- | | `400` | The request is malformed. Common causes include a missing body, invalid timestamp format, or an invalid Base64 payload. | | `401` | HMAC authentication failed. The signature may be invalid or the timestamp may be outside the allowed replay window. | | `403` | Your credentials are valid, but you are not authorized to submit this sender or message type. | | `404` | The supplied transaction `id` does not match a stored transaction when you use the detail endpoint. | | `409` | Meridian detected a duplicate submission, usually from a reused `Idempotency-Key`. | | `413` | The Base64 payload exceeds the allowed size. | | `415` | `Content-Type` is not `text/plain`. | | `422` | The request was structurally valid but failed early semantic validation. | ## Integration guidance * Treat the request body as an opaque Base64 string. Do not transform the message content before submission. * Use the path slug that matches the decoded payload you are sending. * Generate a fresh idempotency key for each new message and reuse it only when retrying the same submission. * Keep your system clock accurate. Timestamp skew can cause authentication failures. * Store the Meridian `messageId`, returned `transactionId`, your own business reference, and the idempotency key together for audit and reconciliation. * Hash and sign the exact Base64 string you send on the wire. * Use `GET /v1/transactions` to reconcile batches of transactions. * Use `GET /v1/transactions?messageId=...` when you want to find the transaction created from a specific submission. * Use `GET /v1/transactions/{id}` when you already know the Meridian internal transaction ID. ## Next steps * Review [Supported formats](/products/payout-origination/supported-formats) for the message types Meridian accepts. * Review [Base64 payload encoding](/products/payout-origination/base64-payload-encoding) for practical guidance on building the request body. * Review [Authentication](/products/payout-origination/authentication) for request signing details. * Use the API reference for the exact request and response schema. # pacs.008 format Source: https://docs.mnai.com/products/payout-origination/pacs008-format Understand the pacs.008 XML document Meridian expects you to Base64-encode and submit to the Payout Origination API. Use this page to understand the message document you send to the Payout Origination API before you Base64-encode it. > [!NOTE] > > Use `pacs008` as the `{format}` path parameter when submitting this message type: > `POST /v1/payout-origination/pacs008` Meridian accepts `pacs.008.*` as one of the supported payout origination formats. You must submit the full original ISO 20022 XML document as a single opaque payload. Do not split the message into API fields or convert it into another structure. ## Meridian intake requirements | Item | Requirement | | :- | :- | | Message family | `pacs.008.*` | | Document format | ISO 20022 XML | | Submission model | Base64-encode the full message document and send it as the request body | | Content type | `text/plain` | | Transaction blocks | Exactly one `CdtTrfTxInf` block per submitted message | | Transformations | Do not flatten, normalize, or partially serialize the message before encoding | ## What Meridian means by full pacs.008 Meridian expects the complete business message content for the payment instruction you are submitting. That means you should preserve: * the original XML structure * the version-specific namespace * the element hierarchy * the values and formatting used in the original message The API treats the Base64 body as an opaque artifact at intake. Meridian does not expect you to map `pacs.008` fields into separate JSON properties. ## High-level document structure A `pacs.008` message is an ISO 20022 customer credit transfer instruction. The exact variant depends on the message version you use, but the structure is typically organized like this: ```xml theme={null} ... ... ``` In most integrations: * `Document` is the XML root element * `FIToFICstmrCdtTrf` contains the credit transfer message * `GrpHdr` contains message-level metadata * `CdtTrfTxInf` contains the payment transaction details Depending on the `pacs.008` version and network rules, the document may include one or more transaction blocks and additional optional elements. For Meridian Payout Origination, the submitted message must contain exactly one `CdtTrfTxInf` block. Messages with multiple transaction blocks are outside the documented contract for this API. ## Common sections The table below describes the sections most integrators will recognize in a `pacs.008` document. Exact child elements vary by schema version and implementation profile. | Section | Purpose | | :- | :- | | `GrpHdr` | Message-level identifiers, creation time, settlement information, and transaction count | | `PmtId` | End-to-end and transaction reference identifiers | | `IntrBkSttlmAmt` | Interbank settlement amount and currency | | `IntrBkSttlmDt` | Settlement date | | `Dbtr` and `DbtrAcct` | Debtor party and account information | | `DbtrAgt` | Debtor agent or sending institution | | `CdtrAgt` | Creditor agent or receiving institution | | `Cdtr` and `CdtrAcct` | Creditor party and account information | | `RmtInf` | Remittance information | | `Purp` or `CtgyPurp` | Payment purpose or category purpose when applicable | ## Settlement currency The `Ccy` attribute on `IntrBkSttlmAmt` in your submitted message determines the payout currency and flows through to `amounts.debit.currency` and `amounts.credit.currency` in the transaction model. ```xml theme={null} 4800.00 ``` | Currency | Status | | :- | :- | | `USD` | Supported | | `EUR` | Coming soon | | `GBP` | Coming soon | | `AUD` | Coming soon | ## Practical expectations When you prepare a message for Meridian: * send a complete `pacs.008` message document, not a fragment * preserve the schema namespace that matches your chosen `pacs.008` version * preserve the original identifiers used for the payment instruction * include exactly one `CdtTrfTxInf` block in each submitted message * keep the XML as the source artifact that you encode and submit * validate the message against your own network and schema rules before encoding If your implementation profile requires additional envelopes or headers outside the `Document` payload, align that packaging with Meridian during onboarding. The ingestion endpoint itself expects the encoded payload to contain the message document you want processed. ## Example skeleton This example shows the shape Meridian expects at a document level. It is intentionally abbreviated and not a complete, schema-valid payment message. ```xml theme={null} 20260407-000123 2026-04-07T20:11:40Z 1 CLRG ABC-12345 E2E-12345 TX-12345 1250.00 2026-04-08 Sender Name AAAAUS33XXX BBBBGB22XXX Receiver Name Invoice 100045 ``` ## Before you submit Before calling `POST /v1/payout-origination/pacs008`: 1. Build or receive the complete `pacs.008` message document. 2. Validate the XML and business content in your own system. 3. Base64-encode that message document. 4. Send the encoded payload to Meridian with HMAC authentication headers and a separate `Idempotency-Key` header. ## Transaction model mapping Use this section to understand how Meridian maps a submitted `pacs.008` message into the internal transaction model that `GET /v1/transactions` and `GET /v1/transactions/{id}` expose. The read APIs return Meridian's internal transaction model, not the original XML. Each submitted message must contain exactly one `CdtTrfTxInf` block. ### Mapping table | Transaction field | pacs.008 source | Notes | | :- | :- | :- | | `messageId` | `GrpHdr/MsgId` | Meridian uses the message identifier from the submitted `pacs.008` as the message correlation ID | | `externalId` | `CdtTrfTxInf/PmtId/TxId` | This is the primary business reference exposed as `externalId` | | `amounts.debit.amount` | `CdtTrfTxInf/IntrBkSttlmAmt` | Uses the numeric value from the interbank settlement amount | | `amounts.debit.currency` | `CdtTrfTxInf/IntrBkSttlmAmt/@Ccy` | Uses the currency attribute from the interbank settlement amount | | `amounts.credit.amount` | `CdtTrfTxInf/IntrBkSttlmAmt` | Currently mirrors the same settlement amount | | `amounts.credit.currency` | `CdtTrfTxInf/IntrBkSttlmAmt/@Ccy` | Currently mirrors the same settlement currency | | `debitInstrument.originator.displayName` | `CdtTrfTxInf/Dbtr/Nm` | Debtor name | | `debitInstrument.originator.accountNumber` | `CdtTrfTxInf/DbtrAcct/...` | Use the debtor account identifier Meridian receives in the message | | `creditInstrument.beneficiaryAccount.displayName` | `CdtTrfTxInf/Cdtr/Nm` | Creditor name | | `creditInstrument.beneficiaryAccount.id` | `CdtTrfTxInf/CdtrAcct/...` | Use the creditor account identifier Meridian receives in the message | | `creditInstrument.beneficiaryAccount.currency` | `CdtTrfTxInf/IntrBkSttlmAmt/@Ccy` | The current documented example uses the settlement currency | | `source.type` | Internal value `external_submission` | Current documented behavior for pacs.008-ingested transactions | ### Internal fields These transaction fields are internal Meridian fields and are not sourced directly from the `pacs.008` payload: * `status` * `statusReason` * `createdAt` * `updatedAt` * `relatedTransactionId` ### Example mapping Given this `pacs.008` fragment: ```xml theme={null} 20260407-000123 ABC-12345 E2E-12345 TX-12345 1250.00 Sender Name 123456789 Receiver Name 987654321 ``` The transaction model can look like this: ```json theme={null} { "id": "txn_01JVY8Y4N9X2M6S5Q7T1", "messageId": "20260407-000123", "source": { "type": "external_submission", "externalSubmission": { "id": "sub_01JVY8Y4N9X2M6S5Q7T1", "displayName": "Customer message submission" } }, "type": "transfer", "status": "PENDING", "statusReason": null, "externalId": "TX-12345", "relatedTransactionId": null, "amounts": { "debit": { "amount": "1250.00", "currency": "USD" }, "credit": { "amount": "1250.00", "currency": "USD" }, "fees": [], "exchangeRate": "1.0000" }, "debitInstrument": { "type": "ORIGINATOR", "originator": { "displayName": "Sender Name", "accountNumber": "123456789" } }, "creditInstrument": { "type": "BENEFICIARY_ACCOUNT", "beneficiaryAccount": { "id": "987654321", "displayName": "Receiver Name", "currency": "USD" } } } ``` ## Related pages * [Overview](/products/payout-origination/overview) * [Base64 payload encoding](/products/payout-origination/base64-payload-encoding) * [Authentication](/products/payout-origination/authentication) # Request a Sandbox Source: https://docs.mnai.com/products/payout-origination/sandbox Request Sandbox credentials and set up a test environment to build and validate your Meridian Payout Origination integration before going live. ## Requesting Sandbox credentials Meridian provisions Sandbox access on request. To get started, contact your Solutions Engineer and provide the following: * Your organization's name and country of operation * A technical point of contact with name and email * The webhook URL you want Meridian to use for payout origination processing status events * The sender or program identifiers Meridian should associate with your Sandbox traffic * Any IP addresses Meridian should expect if your network policy requires allowlisting Once Meridian receives this information, it provisions your Sandbox environment and delivers your credentials securely. ## What you'll receive Upon provisioning, Meridian securely provides: * **API Key** — used in the `X-Meridian-Api-Key` header on all Payout Origination API requests * **API Secret** — used to compute the HMAC SHA-256 request signature * **Webhook API Key and Secret** — used exclusively for authenticating inbound webhook events * **Sandbox base URL** — for the Payout Origination API These credentials are specific to your Sandbox environment. Meridian issues production credentials separately as part of the go-live process. ## What to validate in Sandbox Before requesting production access, validate that your integration can: * Base64-encode the full source payload without changing its contents * Use the correct `{format}` path slug for the payload you submit * Sign requests with the correct HMAC headers * Submit a message to `POST /v1/payout-origination/{format}` successfully * Store the returned `messageId`, then reconcile the resulting transaction in your own system * Accept and verify webhook notifications for transaction processing status changes * Look up transactions with `GET /v1/transactions?messageId=...` or `GET /v1/transactions/{id}` # Supported formats Source: https://docs.mnai.com/products/payout-origination/supported-formats See which message formats the Payout Origination API accepts and which path slug to use for each one. Use this page to choose the correct `{format}` path slug before you Base64-encode and submit a payout request. Meridian currently accepts these payout origination formats: | Path slug | Source payload | Use when | | :- | :- | :- | | `pacs008` | ISO 20022 `pacs.008.*` XML document | You originate payouts from an existing ISO 20022 payment message | | `json` | JSON object | You want to submit a simpler structured payload where `entityId`, `beneficiaryId`, `amount`, `currency`, and `reference` are all required | ## Common intake rules These rules apply to every supported format: * Base64-encode the full source payload and send it as the raw `text/plain` request body. * Sign the request with Meridian HMAC headers. * Send `Idempotency-Key` as a separate request header. * Use `POST /v1/payout-origination/{format}` with the correct path slug for your message type. * Preserve the exact payload bytes you want Meridian to process before you Base64-encode them. ## Format-specific guides * Use [pacs.008 format](/products/payout-origination/pacs008-format) for ISO 20022 XML submissions. * Use [JSON format](/products/payout-origination/json-format) for Base64-encoded JSON submissions. ## Choosing a format Choose `pacs008` when your upstream system already produces a complete `pacs.008.*` payment message and you want Meridian to ingest that original XML. Choose `json` when you do not have an ISO 20022 payload and want to originate the payout from a minimal JSON document where all of these fields and values are required: * `entityId` * `beneficiaryId` * `amount` * `currency` * `reference` ## Related pages * [Overview](/products/payout-origination/overview) * [Base64 payload encoding](/products/payout-origination/base64-payload-encoding) * [Authentication](/products/payout-origination/authentication) # Overview Source: https://docs.mnai.com/products/payout-origination/webhooks-overview Learn how Meridian delivers webhook notifications, including security, retries, and delivery behavior. ## Configuration Meridian configures webhook endpoints during provisioning. You can configure a separate URL for each webhook event type. If your integration needs different handling per event, set each webhook URL to the appropriate handler endpoint. ## Event routing Use the `X-Meridian-Resource-Type` and `X-Meridian-Event-Type` headers to identify the webhook before you parse the body. These headers tell you which resource the event belongs to and which schema to use. | Header | Example values | Use | | :- | :- | :- | | `X-Meridian-Resource-Type` | `transaction` | Routes the payload to the correct resource handler | | `X-Meridian-Event-Type` | `processing_status_updated` | Selects the specific event parser for that resource | Read these headers first, then deserialize the request body with the matching webhook schema. This lets your handler reject unexpected combinations early and keeps your routing logic aligned with the payload shape. ## Security Webhook requests use HMAC SHA-256 with a dedicated Webhook API Key and Secret. Each request includes the following headers: | Header | Description | | :- | :- | | `X-Meridian-Api-Key` | Issued by Meridian for webhook delivery | | `X-Meridian-Timestamp` | Current time in milliseconds, within 60 seconds of the request | | `X-Meridian-Signature` | HMAC SHA-256 signature | Validate the signature on every inbound request before processing the payload. Use the same canonical string construction described in [Authentication](/products/payout-origination/authentication#constructing-the-signature). ## Delivery and retries Meridian considers a webhook delivered when your endpoint returns an HTTP `2xx` response within **30 seconds**. If the request times out or returns a non-`2xx` response, Meridian retries using exponential backoff, up to **6 attempts over 24 hours**. Because retries can produce duplicate deliveries, your webhook handler should be idempotent. Treat every webhook as potentially arriving out of order. Store the status and timestamp you already know for each transaction. Only overwrite when the incoming payload reflects a newer state or a valid progression. Log non-`2xx` responses and return as fast as possible; even short delays may trigger retries that redeliver the same event. # Processing Status Updated Source: https://docs.mnai.com/products/payout-origination/webhooks-processing-status-updated products/payout-origination/api-reference/payout-origination.json webhook processing_status_updated Learn when the processing status updated webhook is sent and what data it includes. # Introduction Source: https://docs.mnai.com/products/realtime-collections/api-reference/index API reference for the Realtime Collections API, including base URLs, credentials, and authentication requirements. > [!TIP] > > Make all API calls from your backend. Client applications must never call Meridian APIs directly. ## Request flow Every payment link is created against a short-lived session. Call [Create a Session](/products/realtime-collections/api-reference/sessions/create-session) to obtain a `sessionKey`, then pass that key in the body of [Create a Payment Link](/products/realtime-collections/api-reference/payment-links/create-payment-link). Session keys expire after 15 minutes and are tied to the operator details supplied when they were issued, so obtain a new key if those details change. ## Base URLs | Environment | URL | Purpose | | :- | :- | :- | | Sandbox | `https://us-merchant.sandbox.api-us.meridianapps.dev` | Testing and certification | | Production | `https://us-merchant.api-us.payments.meridianpay.com` | Live traffic | Meridian provides your credentials and endpoint access for the Sandbox environment during onboarding. Always validate your integration against Sandbox before requesting production access. ## Example credentials | Credential | Sandbox | Production | | - | - | - | | API Key | `akey_test_uzbz2fpfswgx22mjjrysljad` | `akey_live_7785ul8u9ymalmpftq311xqw` | | API Secret | `asec_test_4zxavmk5iomub4nfm3odynd0` | `asec_live_w69ob7claqhdkw4uo183yl74` | | Webhook API Key | `wkey_test_wtr8lqsjaecr4e2h8f0hb58f` | `wkey_live_1m3wfbs0p1m1v09mjff3vd54` | | Webhook API Secret | `wsec_test_glimkmggzki3mzj1tgml054u` | `wsec_live_ntvxqrmq1t9t546p9zaehop0` | # Create a Payment Link Source: https://docs.mnai.com/products/realtime-collections/api-reference/payment-links/create-payment-link products/realtime-collections/api-reference/payment-links.json POST /v1/payment-links Create a payment link to collect a one-off, recurring, or variable payment from a customer. # Retrieve a Payment Link Source: https://docs.mnai.com/products/realtime-collections/api-reference/payment-links/get-payment-link products/realtime-collections/api-reference/payment-links.json GET /v1/payment-links/{id} Retrieve a payment link and its current state by Meridian id. # Update a Payment Link Source: https://docs.mnai.com/products/realtime-collections/api-reference/payment-links/update-payment-link products/realtime-collections/api-reference/payment-links.json PATCH /v1/payment-links/{id} Apply partial changes to an existing payment link, including its amount, schedule, and status. # Create a Session Source: https://docs.mnai.com/products/realtime-collections/api-reference/sessions/create-session products/realtime-collections/api-reference/payment-links.json POST /v1/payment-links/sessions Create a Realtime Collections session to obtain the session key required to create payment links. # Payment Link Source: https://docs.mnai.com/products/realtime-collections/api-reference/webhooks/webhooks-payment-link products/realtime-collections/api-reference/payment-links.json webhook payment_link Learn when the payment link webhook is sent and what data it includes. # Payment Link Transaction Source: https://docs.mnai.com/products/realtime-collections/api-reference/webhooks/webhooks-payment-link-transaction products/realtime-collections/api-reference/payment-links.json webhook payment_link_transaction Learn when the payment link transaction webhooks are sent and what data they include. # Authentication Source: https://docs.mnai.com/products/realtime-collections/authentication Secure your API requests using HMAC SHA-256 signatures with pre-shared API keys, including step-by-step instructions and code examples. * All API calls use an HMAC authentication strategy with pre-shared API and secret keys. * Sandbox and production environments have different API credentials. Do not reuse across environments. * All requests must use HTTPS (TLS 1.2 or higher). * You must allow outbound connections to the [Meridian Base URLs](/products/realtime-collections/api-reference#base-urls). * Depending on your program configuration, Meridian may require IP whitelisting for production access. Meridian provides details if applicable. Every request must include the following headers: | Header | Description | | :- | :- | | `X-Meridian-Api-Key` | Issued by Meridian | | `X-Meridian-Timestamp` | Current time in milliseconds (must be within 10 seconds of the request) | | `X-Meridian-Signature` | HMAC SHA-256 signature | > [!NOTE] > > Meridian issues your API Key and Secret during Sandbox and production provisioning. When Meridian receives the request, it validates the signature. If the signature does not match, the API immediately returns a `401 Unauthorized` response. ## Constructing the signature **1. Build the canonical string** Compute the signature as an HMAC SHA-256 hash over a canonical string that concatenates the following values in order: | Signature component | Notes | | - | - | | `API-Key` | Same value as `X-Meridian-Api-Key` | | `Timestamp` | Same value as `X-Meridian-Timestamp` | | `Request method` | Example: `GET`, `POST`, `PATCH` | | `Request path` | Including any parameters. For example: `/v1/users/ususr-s8f9ds7f8sdf789d7fsd89f` | | `Body contents` | The full stringified JSON contents of the request body. If there is no body, this can be an empty string. | **2. Compute the HMAC** Compute the HMAC using the language-appropriate crypto library with the secret key. > [!WARNING] > > Make sure you compute the HMAC with the API Secret that Meridian issued to you. **3. Compute the SHA-256 hash** Hash the final value using the SHA-256 algorithm and set it as the `X-Meridian-Signature` header. ```kotlin kotlin theme={null} import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec fun buildMeridianSignature(apiKey: String, apiSecret: String, method: String, path: String, body: String = ""): Map { val timestamp = System.currentTimeMillis().toString() val canonical = apiKey + timestamp + method + path + body val mac = Mac.getInstance("HmacSHA256").apply { init(SecretKeySpec(apiSecret.toByteArray(), "HmacSHA256")) } val signature = mac.doFinal(canonical.toByteArray()) .joinToString("") { "%02x".format(it) } return mapOf( "X-Meridian-Api-Key" to apiKey, "X-Meridian-Timestamp" to timestamp, "X-Meridian-Signature" to signature, "Content-Type" to "application/json" ) } ``` ```swift swift theme={null} import CryptoKit import Foundation func buildMeridianSignature(apiKey: String, apiSecret: String, method: String, path: String, body: String = "") -> [String: String] { let timestamp = String(Int(Date().timeIntervalSince1970 * 1000)) let canonical = apiKey + timestamp + method + path + body let key = SymmetricKey(data: Data(apiSecret.utf8)) let signature = HMAC.authenticationCode(for: Data(canonical.utf8), using: key) .map { String(format: "%02x", $0) }.joined() return [ "X-Meridian-Api-Key": apiKey, "X-Meridian-Timestamp": timestamp, "X-Meridian-Signature": signature, "Content-Type": "application/json" ] } ``` ```javascript postman theme={null} // ─── Config ────────────────────────────────────────────────────────────────── // Set these as collection variables (never hardcode secrets) const apiKey = pm.variables.get("meridianApiKey"); const apiSecret = pm.variables.get("meridianApiSecret"); // ─── Timestamp (milliseconds) ───────────────────────────────────────────────── const timestamp = Date.now().toString(); // ─── Request components ─────────────────────────────────────────────────────── const method = pm.request.method; // e.g. "POST" const url = pm.request.url; const qs = url.getQueryString(); const path = url.getPath() + (qs ? "?" + qs : ""); // e.g. "/v1/payment-links/sessions" const body = pm.request.body?.raw ?? ""; // ─── Canonical string ───────────────────────────────────────────────────────── const canonical = apiKey + timestamp + method + path + body; // ─── HMAC-SHA256 signature ──────────────────────────────────────────────────── const signature = CryptoJS.HmacSHA256(canonical, apiSecret).toString(CryptoJS.enc.Hex); // ─── Inject into request headers ───────────────────────────────────────────── pm.variables.set("meridianTimestamp", timestamp); pm.variables.set("meridianSignature", signature); // ─── Debug ──────────────────────────────────────────────────────────────────── console.log("Timestamp :", timestamp); console.log("Canonical :", canonical); console.log("Signature :", signature); ``` ## Common errors > [!WARNING] > > Meridian returns 401 Unauthorized for requests with missing or invalid signatures. The most common causes are: * Canonical string fields concatenated in the wrong order. * Timestamp too far from the server's current time. * Forgetting to compute the HMAC with your unique API Secret. * Forgetting to SHA-256 hash the final string. # Realtime Collections Source: https://docs.mnai.com/products/realtime-collections/index Create and manage payment links to collect funds from your customers with Meridian Realtime Collections. ## How payment links work A payment link is a URL you generate for a specific amount in the currency you want to receive, such as Philippine pesos. You share the link with a U.S.-based buyer, who opens it, connects their U.S. bank account, and authorizes a debit. Meridian converts the debited U.S. dollars automatically so you are paid the target amount in your currency. You create and manage links through Meridian's REST API, or through the Meridian Realtime Collections portal. Both work against the same links, so you can create a link in the portal and read it back through the API. Every link carries a merchant-defined `referenceId`, so payments reconcile against the contract, invoice, or account number your own systems already use. ## Currency support **Customers pay in USD, and merchants receive payouts in PHP.** These are currently the only supported currencies for collection and settlement. ## Recurrence types Each payment link is created with a recurrence type that determines how many times it can be paid. | Type | Behavior | | :- | :- | | `SINGLE` | Paid once. After a successful payment the link becomes invalid and cannot be used again. | | `VARIABLE` | Recurring, where the amount and the next collection date can change between charges. The amount can be left blank at creation and set later. | | `RECURRING` | Recurring on a defined schedule, allowing multiple charges over time. | `VARIABLE` suits collections where the amount is not known in advance and arrives period by period, such as monthly amortization. `RECURRING` suits a fixed amount on a regular schedule. ## Automatic payments For recurring links, customers can set up an Auto Pay. Meridian then collects each payment automatically on a scheduled day of the month, so the customer does not need to return to the link each time. A link reports its Auto Pay arrangements, so you can tell whether a customer has automated their payments or is paying manually. ## Link access and security Meridian protects payment links in two ways: * **First-access deadline.** Every link carries a deadline for its first access. If the customer does not open the link by then, it expires automatically and can no longer be paid. * **Authentication challenge.** You can require the customer to answer a challenge before viewing the link. **This is required for recurring payment links**, since they can be charged more than once. > [!NOTE] > > Meridian configures your first-access expiry window during onboarding. ## Staying in sync Meridian sends [webhooks](/products/realtime-collections/webhooks-overview) when a payment link is created, completed, or archived. It also sends webhooks every time a payment link transaction changes status, so your systems can react without polling. To follow a payment from start to finish, see [Tracking a payment link payment with webhooks](/products/realtime-collections/tracking-payments-with-webhooks). Because a link's status changes over its life — active, processing, completed, expired, archived — treat the webhook as the trigger and the link's status as the source of truth. # Request a Sandbox Source: https://docs.mnai.com/products/realtime-collections/sandbox Request Sandbox credentials and set up a test environment to build and validate your Meridian integration before going live. ## Requesting Sandbox credentials Meridian provisions Sandbox access on request. To get started, contact your Solutions Engineer and provide the following: * Your organization's name and country of operation * A technical point of contact (name and email) * The webhook URL you'd like Meridian to deliver payment link events to * Your company logo, or logos if you are configuring multiple brands or entities. This appears in email notifications and on your payment links. Once Meridian receives this information, it provisions your Sandbox environment — including your branded UI configuration — and delivers your credentials securely. ## What you'll receive Upon provisioning, Meridian securely provides: * **API Key** — used in the `X-Meridian-Api-Key` header on all API requests * **API Secret** — used to compute the HMAC SHA-256 request signature * **Webhook API Key and Secret** — a separate key pair used exclusively for authenticating inbound webhook events These credentials are specific to your Sandbox environment. Meridian issues production credentials separately as part of the go-live process. # Tracking a payment link payment with webhooks Source: https://docs.mnai.com/products/realtime-collections/tracking-payments-with-webhooks Follow a payment link payment from creation to completion, failure, or reversal using payment link transaction webhooks. Meridian sends a webhook every time a payment link transaction changes status. Each webhook carries both the payment link and the transaction, so you can tie a payment to its link without another API call. This works for single and recurring links. ## Before you start Ask Meridian to register an endpoint for each event you want. Meridian registers endpoints per event name, so register all four to follow a payment end to end: * `payment_link_transaction_created` * `payment_link_transaction_status_updated` * `payment_link_transaction_succeeded` * `payment_link_transaction_failed` Meridian signs these webhooks the same way as every other webhook. See [Security](/products/realtime-collections/webhooks-overview#security) to verify them. ## Route the event Every delivery includes these headers: | Header | Value | | :- | :- | | `X-Meridian-Resource-Type` | `payment_link_transaction` | | `X-Meridian-Event-Type` | One of the four events above | The body has no envelope. It is a JSON object with `paymentLink` and `transaction`, and the event name is only in the `X-Meridian-Event-Type` header. Read the header first, then act on `transaction.status`. ```json theme={null} { "paymentLink": { "id": "usmer-n1hpx2d89pdpnmpdjlorb995", "status": "ACTIVE", "referenceId": "INV-7755-1791463748", "...": "..." }, "transaction": { "id": "uspay-echo1fuz76usacpgicexz5vb", "createdAt": "2026-10-08T12:51:39.147Z", "updatedAt": "2026-10-08T12:51:49.571Z", "completedAt": "2026-10-08T12:51:49.570Z", "status": "COMPLETED", "previousStatus": "USER_DEDUCTED", "statusReason": null, "debitAmount": "150.00", "debitCurrency": "USD", "creditAmount": "150.00", "creditCurrency": "USD", "fee": "0.00" } } ``` `paymentLink` is the same object the Payment Links API returns. See [Payment Link Transaction](/products/realtime-collections/api-reference/webhooks/webhooks-payment-link-transaction) for the full schema and an example of each event. Amounts are decimal strings, timestamps are UTC, and fields with no value are sent as `null` instead of being omitted. The payload does not include payer names, bank accounts, or instrument details. ## Status flow A successful payment moves through these statuses: `PENDING` → `PROCESSING` → `USER_DEDUCTED` → `COMPLETED` | Status change | Event | | :- | :- | | Transaction created as `PENDING` | `payment_link_transaction_created` | | `PROCESSING`, `USER_DEDUCTED` | `payment_link_transaction_status_updated` | | `COMPLETED` | `payment_link_transaction_succeeded` | ### Failures A transaction that reaches `FAILED`, `DEBIT_FAILED`, `RETURNED`, or `REVERSED` sends `payment_link_transaction_failed`. The `statusReason` field holds the failure reason when there is one. For example, a failed payment sends `status: FAILED`, `previousStatus: PROCESSING`, and `statusReason: INSUFFICIENT_FUNDS`. ### Reversals A reversal sends `payment_link_transaction_status_updated` at `REVERSAL_IN_PROGRESS`. It sends `payment_link_transaction_failed` only when it reaches `REVERSED`. ### Other statuses These statuses also send `payment_link_transaction_status_updated`: `PENDING_MANUAL_REVIEW`, `EDD_REQUIRED`, `REFUND_IN_PROGRESS`, `REFUNDED`, `PARTIALLY_REFUNDED`, `TIMED_OUT_REVERSAL_DECISION_PENDING`, `RETURN_IN_PROGRESS`, and `NOT_RECEIVED`. ## Handle the webhooks correctly **1. Treat transaction.status as the source of truth** Use `transaction.status` and `transaction.previousStatus` to decide what happened, not the event name alone. **2. Expect repeated failed events** Each failure status is its own change, so one transaction can send `payment_link_transaction_failed` more than once. For example, `FAILED` then `DEBIT_FAILED`, or `COMPLETED` then `RETURNED` after an ACH return. Handle repeated `failed` events for the same `transaction.id`. **3. Don't trust paymentLink.status for the outcome** `paymentLink` is a snapshot from when Meridian sent the webhook. Meridian updates the link and the transaction separately, so the link's `status` can lag the transaction. In `payment_link_transaction_created` the link can still be `ACTIVE`, and in `payment_link_transaction_failed` it can show its previous status. Fetch the link through the API if you need its final state. **4. Process idempotently** Key on `transaction.id` plus `transaction.status`. Each status change sends exactly one event, but retries can still deliver the same webhook more than once. **5. Order by updatedAt** Meridian does not guarantee delivery order. Use `transaction.updatedAt` to order events for the same transaction. **6. Reconcile through the API** Delivery is best effort. Meridian retries a failed delivery up to 3 times, a few hundred milliseconds apart. It is not a guaranteed, replayable queue, so periodically reconcile your records through the API. ## Behavior change Payment link transactions no longer send the generic `transaction_created`, `transaction_succeeded`, and `transaction_failed` events. They send the `payment_link_transaction_*` events instead. `REVERSAL_IN_PROGRESS` used to be reported as a failure and is now a `payment_link_transaction_status_updated` event. Statuses such as `PROCESSING` and `USER_DEDUCTED` were not reported before. The `transaction_*` events still exist and are unchanged for Meridian Accounts. # Webhooks Source: https://docs.mnai.com/products/realtime-collections/webhooks-overview Learn how Meridian delivers real-time event notifications to your system via HTTP POST webhooks, including configuration, security, and retry behavior. ## Configuration > [!NOTE] > > Meridian configures webhook endpoints during provisioning. You can register separate URLs for Sandbox and production. Meridian registers endpoints per event name, so you need an endpoint for each event you want to receive. Meridian does not deliver an event that has no endpoint. You can register the same URL for several events. If you do, your handler must route them based on the event headers. To receive the `payment_link_transaction_*` events, ask Meridian to register an endpoint for each of the four event names. ## Event routing Before you parse the body, use the `X-Meridian-Resource-Type` and `X-Meridian-Event-Type` headers to identify which resource the event belongs to and which specific event occurred. | Header | Example values | Use | | :- | :- | :- | | `X-Meridian-Resource-Type` | `payment_link`, `payment_link_transaction` | Routes the payload to the correct resource handler | | `X-Meridian-Event-Type` | See the event types table below | Identifies the specific event for that resource | | Resource | Event types | | :- | :- | | `payment_link` | `payment_link_created`, `payment_link_completed`, `payment_link_archived` | | `payment_link_transaction` | `payment_link_transaction_created`, `payment_link_transaction_status_updated`, `payment_link_transaction_succeeded`, `payment_link_transaction_failed` | The three `payment_link` events carry the same [Payment Link](/products/realtime-collections/api-reference/webhooks/webhooks-payment-link) payload, so read the headers first to tell them apart, then deserialize the body with the webhook schema. Other payment link status transitions, including `PROCESSING`, `FAILED`, `EXPIRED`, `LOCKED`, and `INACTIVE`, do not emit a `payment_link` webhook. The four `payment_link_transaction` events carry the same [Payment Link Transaction](/products/realtime-collections/api-reference/webhooks/webhooks-payment-link-transaction) payload: the payment link and the transaction. The body has no envelope, so the event name is only in the `X-Meridian-Event-Type` header. ### Payment link transaction events Meridian sends a `payment_link_transaction` webhook every time a payment link transaction changes status, for single and recurring links. | Event | Sent when | | :- | :- | | `payment_link_transaction_created` | The transaction is created, with status `PENDING`. | | `payment_link_transaction_status_updated` | The transaction reaches an intermediate status with no dedicated event: `PROCESSING`, `USER_DEDUCTED`, `PENDING_MANUAL_REVIEW`, `EDD_REQUIRED`, `REVERSAL_IN_PROGRESS`, `REFUND_IN_PROGRESS`, `REFUNDED`, `PARTIALLY_REFUNDED`, `TIMED_OUT_REVERSAL_DECISION_PENDING`, `RETURN_IN_PROGRESS`, or `NOT_RECEIVED`. Never sent for `PENDING`, `COMPLETED`, or the failure statuses. | | `payment_link_transaction_succeeded` | The transaction reaches `COMPLETED`. | | `payment_link_transaction_failed` | The transaction reaches a final failure status: `FAILED`, `DEBIT_FAILED`, `RETURNED`, or `REVERSED`. | * Each status change sends exactly one event. If Meridian writes the same status twice, you receive one webhook. * A reversal sends `payment_link_transaction_status_updated` at `REVERSAL_IN_PROGRESS`, and `payment_link_transaction_failed` only when it reaches `REVERSED`. * One transaction can send `payment_link_transaction_failed` more than once, because each failure status is its own change. For example, `FAILED` then `DEBIT_FAILED`, or `COMPLETED` then `RETURNED` after an ACH return. Handle repeated `failed` events for the same `transaction.id`, and treat `transaction.status` and `transaction.previousStatus` as the source of truth. * The `paymentLink` object is a snapshot from when Meridian sent the webhook. Meridian updates the link and the transaction separately, so the link's `status` can lag the transaction. Use `transaction.status` for the payment outcome, and fetch the link through the API if you need its final state. > [!WARNING] > > Payment link transactions no longer send the generic `transaction_created`, `transaction_succeeded`, and `transaction_failed` events. They send the `payment_link_transaction_*` events instead. `REVERSAL_IN_PROGRESS` used to be reported as a failure and is now a `payment_link_transaction_status_updated` event. Statuses such as `PROCESSING` and `USER_DEDUCTED` were not reported before. The `transaction_*` events are unchanged for Meridian Accounts. For a walkthrough of the status flow, see [Tracking a payment link payment with webhooks](/products/realtime-collections/tracking-payments-with-webhooks). ## Security Webhook requests, including the `payment_link_transaction` events, use the same HMAC SHA-256 scheme as the REST API, but with a **dedicated Webhook API Key and Secret** issued separately from your REST API credentials. Each request includes the following headers: | Header | Description | | :- | :- | | `X-Meridian-Api-Key` | Issued by Meridian, specifically for Webhooks | | `X-Meridian-Timestamp` | Epoch milliseconds at the moment Meridian sent the request | | `X-Meridian-Signature` | HMAC SHA-256 signature | Validate the signature on every inbound request before processing the payload. Use the same canonical string construction described in [Authentication](/products/realtime-collections/authentication#constructing-the-signature), with one difference: the signature covers the **path only** of your webhook URL, not its query string. If your endpoint URL includes query parameters, exclude them when rebuilding the canonical string, or the signature will not match. **You decide the acceptable timestamp age.** Meridian stamps `X-Meridian-Timestamp` when it sends the webhook and does not enforce a window on its own deliveries. Choose a tolerance that suits your infrastructure. Meridian rejects inbound API requests more than 10 seconds old, which is a reasonable reference point. However, retries mean a redelivered webhook can legitimately arrive much later than its original timestamp. > [!WARNING] > > Do not reject a webhook solely because its timestamp is old. For `payment_link` events, Meridian retries for up to 10 attempts with doubling intervals, so a valid retry can arrive minutes after the event. ## Delivery and retries Meridian considers a webhook delivered when your endpoint returns an HTTP `2xx` response. If the request times out or returns a non-`2xx` status, Meridian retries using exponential backoff. The first retry is after 2 seconds and the interval doubles each time, up to **10 attempts**. ### Payment link transaction events Delivery of `payment_link_transaction` events is best effort. If your endpoint does not return a `2xx` response, Meridian retries up to **3 times**, a few hundred milliseconds apart. Meridian then raises an internal alert for undelivered `payment_link_transaction_succeeded` and `payment_link_transaction_failed` events. It is not a guaranteed, replayable queue, so reconcile through the API. Meridian does not guarantee delivery order. Use `transaction.updatedAt` to order events for the same transaction. Key your idempotency check on `transaction.id` plus `transaction.status`. ### Duplicates Because retries can result in duplicate deliveries, your webhook handler should be **idempotent** — processing the same event more than once should produce the same outcome. # Introduction Source: https://docs.mnai.com/products/retail-va/api-reference/index API reference for creating authenticated Virtual Accounts sessions, including base URLs, credentials, and authentication requirements. > [!TIP] > > All API calls must be made from your backend. Client applications must never call Meridian APIs directly. ## Base URLs | Environment | URL | Purpose | | :- | :- | :- | | Sandbox | `https://sandbox.api.mnai.com` | Testing and certification | | Production | `https://api.mnai.com` | Live traffic | Meridian provides your credentials and endpoint access for the Sandbox environment during onboarding. Always validate your integration against Sandbox before requesting production access. ## Example credentials | Credential | Sandbox | Production | | - | - | - | | API Key | `akey_test_q5zemhdwub8ztw2mlpykvhu0` | `akey_live_q5zemhdwub8ztw2mlpykvhu0` | | API Secret | `asec_test_t44ne6ipaneugxurs13ly55t` | `asec_live_t44ne6ipaneugxurs13ly55t` | | Webhook API Key | `wkey_test_z212127xb84fntpw4whvhd9x` | `wkey_live_z212127xb84fntpw4whvhd9x` | | Webhook API Secret | `wsec_test_tolihur9tjlbah7558uuojf3` | `wsec_live_tolihur9tjlbah7558uuojf3` | # Create a Session Source: https://docs.mnai.com/products/retail-va/api-reference/sessions/create-session products/retail-va/api-reference/retail-rtd.json POST /v1/virtual-accounts/sessions Create a Virtual Account Session to launch the hosted Meridian onboarding experience. # Delete a virtual account application Source: https://docs.mnai.com/products/retail-va/api-reference/simulations/delete-virtual-account-application products/retail-va/api-reference/retail-rtd.json DELETE /v1/remittances/users/{userId}/virtual-account-applications/{virtualAccountApplicationId} Delete a user's onboarding application so onboarding can be re-tested in the sandbox. > [!WARNING] > > **Sandbox only.** This endpoint is available exclusively in the sandbox environment to support testing. It is not possible to delete a virtual account application in production. Use this in the sandbox to reset a user's onboarding so it can be re-tested. After the application is deleted, the user can submit a new one and go through onboarding again. # Virtual Account Transaction Source: https://docs.mnai.com/products/retail-va/api-reference/webhooks/webhooks-transaction products/retail-va/api-reference/retail-rtd.json webhook virtual_account_transaction Learn when the virtual account transaction webhook is sent and what data it includes. Meridian sends this webhook whenever money moves into or out of a customer's virtual account on any rail: ACH, wire, SWIFT, or stablecoin. Two events share one payload: * **`virtual_account_transaction_created`** when the transaction is first recorded. * **`virtual_account_transaction_updated`** when it reaches a final status. The `X-Meridian-Event-Type` header tells you which one you received. Retry timing, signature verification, and endpoint configuration are covered in the [webhooks overview](/products/retail-va/webhooks-overview). ## Deposits by funding rail Deposits can arrive over several rails, and the same fields carry different content depending on which one was used. | Rail | `depositType` | Currency values | Amount scale | `originatorName` / `originatorAccount` / `originatorInstitution` | `fees` | | :- | :- | :- | :- | :- | :- | | ACH | `ACH` | `USD` | 2 decimal places | Sender's name, bank account number, and bank name | `[]` | | Domestic wire | `WIRE` | `USD` | 2 decimal places | Sender's name, bank account number, and bank name | One `WIRE_PROCESSING_FEE` | | International wire | `SWIFT` | `USD` | 2 decimal places | Sender's name, bank account number, and bank name | One `SWIFT_PROCESSING_FEE` | | Stablecoin (on-chain) | `ETHEREUM`, `BASE`, or `POLYGON` | `USDC_ETH`, `USDC_BASE`, `USDC_POLY`, `USDT_ETH`, `USDT_BASE`, `USDT_POLY` | 6 decimal places | `""`, the sender's wallet address, and `"NOTPROVIDED"` | `[]` | ### Stablecoin sender fields On stablecoin deposits, the sender fields hold wallet data, not bank data. | Field | Value on a stablecoin deposit | | :- | :- | | `originatorAccount` | The sender's wallet address, for example `0x5aAe…BeAed` (42 characters) | | `originatorName` | The sender's name when the sending VASP discloses it, otherwise `""`, an empty string | | `originatorInstitution` | The sending VASP's name when it is known, otherwise `"NOTPROVIDED"` | > [!WARNING] > > Neither `originatorName` nor `originatorInstitution` is ever `null` on a stablecoin deposit, so a null check will not tell you the sender is unknown. Check for the empty string and the `NOTPROVIDED` sentinel. Stablecoin currency values such as `USDC_ETH` name both the asset and its network. They are up to 9 characters and are not ISO 4217 codes, so a 3-character currency column will reject them. ## Examples Use the picker in the top-right corner of the JSON block below to switch between these examples: * **ACH deposit (SUCCESS)** * **Wire deposit with fee (SUCCESS)** * **USDC deposit, sender not disclosed (SUCCESS)** * **USDC deposit, sender disclosed (SUCCESS)** * **Withdrawal (PENDING)** * **Withdrawal (SUCCESS)** * **ACH deposit (FAILED)** # Create a quote Source: https://docs.mnai.com/products/retail-va/api-reference/withdrawals/create-quote products/retail-va/api-reference/retail-rtd.json POST /v1/users/{userId}/quotes Create a currency conversion quote that locks an exchange rate before initiating a withdrawal. > [!WARNING] > > **Legacy endpoint.** This is part of the legacy withdrawal flow and will be deprecated. An upcoming **Transactions Intent API** will replace it. Avoid building new integrations against this endpoint — contact your Solutions Engineer for migration guidance. # Create a withdrawal Source: https://docs.mnai.com/products/retail-va/api-reference/withdrawals/create-withdrawal products/retail-va/api-reference/retail-rtd.json POST /v1/users/{userId}/virtual-accounts/{accountId}/withdrawals Execute a withdrawal from a virtual account using a previously created quote. > [!WARNING] > > **Legacy endpoint.** This is part of the legacy withdrawal flow and will be deprecated. An upcoming **Transactions Intent API** will replace it. Avoid building new integrations against this endpoint — contact your Solutions Engineer for migration guidance. # Authentication Source: https://docs.mnai.com/products/retail-va/authentication Secure your API requests using HMAC SHA-256 signatures with pre-shared API keys, including step-by-step instructions and code examples. * All API calls use an HMAC authentication strategy with pre-shared API and secret keys. * Sandbox and production environments have different API credentials. Do not reuse across environments. * All requests must use HTTPS (TLS 1.2 or higher). * You must allow outbound connections to the [Meridian Base URLs](/products/retail-va/api-reference#base-urls). * IP whitelisting may be required for production access depending on your program configuration. Meridian provides details if applicable. Every request must include the following headers: | Header | Description | | :- | :- | | `X-Meridian-Api-Key` | Issued by Meridian | | `X-Meridian-Timestamp` | Current time in milliseconds (must be within 10 seconds of the request) | | `X-Meridian-Signature` | HMAC SHA-256 signature | > [!NOTE] > > Meridian issues your API Key and Secret during Sandbox and production provisioning. When Meridian receives the request, it validates the signature. If the signature does not match, the API immediately returns a `401 Unauthorized` response. ## Constructing the signature **1. Build the canonical string** Compute the signature as an HMAC SHA-256 hash over a canonical string that concatenates the following values in order: | Signature component | Notes | | - | - | | `API-Key` | Same value as `X-Meridian-Api-Key` | | `Timestamp` | Same value as `X-Meridian-Timestamp` | | `Request method` | Example: `GET`, `POST`, `PATCH` | | `Request path` | Including any parameters. For example: `/v1/users/ususr-s8f9ds7f8sdf789d7fsd89f` | | `Body contents` | The full stringified JSON contents of the request body. If there is no body, this can be an empty string. | **2. Compute the HMAC** Compute the HMAC using the language-appropriate crypto library with the secret key. > [!WARNING] > > Make sure you compute the HMAC with the API Secret that Meridian issued to you. **3. Compute the SHA-256 hash** Hash the final value using the SHA-256 algorithm and set it as the `X-Meridian-Signature` header. ```kotlin kotlin theme={null} import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec fun buildMeridianSignature(apiKey: String, apiSecret: String, method: String, path: String, body: String = ""): Map { val timestamp = System.currentTimeMillis().toString() val canonical = apiKey + timestamp + method + path + body val mac = Mac.getInstance("HmacSHA256").apply { init(SecretKeySpec(apiSecret.toByteArray(), "HmacSHA256")) } val signature = mac.doFinal(canonical.toByteArray()) .joinToString("") { "%02x".format(it) } return mapOf( "X-Meridian-Api-Key" to apiKey, "X-Meridian-Timestamp" to timestamp, "X-Meridian-Signature" to signature, "Content-Type" to "application/json" ) } ``` ```swift swift theme={null} import CryptoKit import Foundation func buildMeridianSignature(apiKey: String, apiSecret: String, method: String, path: String, body: String = "") -> [String: String] { let timestamp = String(Int(Date().timeIntervalSince1970 * 1000)) let canonical = apiKey + timestamp + method + path + body let key = SymmetricKey(data: Data(apiSecret.utf8)) let signature = HMAC.authenticationCode(for: Data(canonical.utf8), using: key) .map { String(format: "%02x", $0) }.joined() return [ "X-Meridian-Api-Key": apiKey, "X-Meridian-Timestamp": timestamp, "X-Meridian-Signature": signature, "Content-Type": "application/json" ] } ``` ```javascript postman theme={null} // ─── Config ────────────────────────────────────────────────────────────────── // Set these as collection variables (never hardcode secrets) const apiKey = pm.variables.get("meridianApiKey"); const apiSecret = pm.variables.get("meridianApiSecret"); // ─── Timestamp (milliseconds) ───────────────────────────────────────────────── const timestamp = Date.now().toString(); // ─── Request components ─────────────────────────────────────────────────────── const method = pm.request.method; // e.g. "POST" const url = pm.request.url; const qs = url.getQueryString(); const path = url.getPath() + (qs ? "?" + qs : ""); // e.g. "/v1/virtual-accounts/sessions" const body = pm.request.body?.raw ?? ""; // ─── Canonical string ───────────────────────────────────────────────────────── const canonical = apiKey + timestamp + method + path + body; // ─── HMAC-SHA256 signature ──────────────────────────────────────────────────── const signature = CryptoJS.HmacSHA256(canonical, apiSecret).toString(CryptoJS.enc.Hex); // ─── Inject into request headers ───────────────────────────────────────────── pm.variables.set("meridianTimestamp", timestamp); pm.variables.set("meridianSignature", signature); // ─── Debug ──────────────────────────────────────────────────────────────────── console.log("Timestamp :", timestamp); console.log("Canonical :", canonical); console.log("Signature :", signature); ``` ## Common errors > [!WARNING] > > Meridian returns 401 Unauthorized for requests with missing or invalid signatures. The most common causes are: * Canonical string fields concatenated in the wrong order. * Timestamp too far from the server's current time. * Forgetting to compute the HMAC with your unique API Secret. * Forgetting to SHA-256 hash the final string. # Customization Source: https://docs.mnai.com/products/retail-va/customization Configure branding, localization, and customer support settings to tailor the Virtual Accounts experience to your program. ## Branding The hosted UI is intentionally simple and utilitarian, designed to complement your application rather than compete with it. To align the experience with your visual identity, you can configure: * **Primary brand color** — applied to key interactive elements throughout the experience * **Secondary brand color** — used for supporting UI elements and accents * **Company logo** — displayed within the hosted experience to reinforce your brand Provide brand colors as hex values. Submit branding assets and colors to Meridian during the Sandbox provisioning process. ## Localization The Virtual Accounts UI automatically selects the display language based on your customer's device or browser settings. You do not need any additional configuration on your end. Supported languages vary by program. During onboarding, confirm with your Solutions Engineer which languages are available for your program. Ensure that any customer-facing messaging in your application aligns with what the hosted experience supports. ## Customer support center You can configure a custom support entry point within the hosted experience, giving customers a way to access help without leaving the Virtual Accounts flow. The support center can be configured as either: * **A deep link** — routes the customer back into your mobile application to a specific support screen * **A browser URL** — opens an external support page When configured, the hosted UI surfaces the support entry point directly. Contact your Solutions Engineer to set or update your support center URL. # Deposits Source: https://docs.mnai.com/products/retail-va/deposits Learn how customers receive ACH, wire, and stablecoin deposits into their Virtual Account, including the transaction lifecycle and Auto-sweep feature. ## How deposits work From the Account Home screen, your customer can access their Virtual Account details, including the account and routing information needed to receive funds. Your customer provides these details to the sending party, who submits the transfer through their own bank via ACH or wire. Once Meridian receives the funds, it credits the deposit to the customer's Virtual Account and sends a [webhook](/products/retail-va/webhooks-overview) notification to your system. ```mermaid theme={null} sequenceDiagram actor Employer participant VA as Customer Virtual Account participant Backend as Partner Backend Employer->>VA: Submits payroll deposit VA->>VA: Balance refreshed in UI session VA->>Backend: Triggers webhook notification ``` ## Deposit methods Funds can reach a Virtual Account over the following rails. The rail is reported in the `depositType` field of the [transaction webhook](/products/retail-va/api-reference/webhooks/webhooks-transaction), and the shape of the sender information depends on it. | Method | `depositType` | Description | | :- | :- | :- | | **ACH** | `ACH` | Standard bank transfer via the ACH network | | **Wire** | `WIRE` | Domestic wire transfer. A processing fee applies | | **International wire** | `SWIFT` | Cross-border wire. A processing fee applies | | **Stablecoin** | `ETHEREUM`, `BASE`, `POLYGON` | USDC or USDT sent on-chain to the account's wallet address. Amounts carry six decimal places, and the sender is identified by wallet address rather than bank details | ## Transaction lifecycle Deposits move through the following statuses: | Status | Description | | :- | :- | | `PENDING` | Deposit has been initiated and is awaiting settlement | | `SUCCESS` | Funds have been successfully credited to the Virtual Account | | `FAILED` | Deposit could not be completed. See `statusReason` for details | | `ERROR` | Defensive fallback for an internal state that should not occur. Treat it as a failure and contact Meridian support | Meridian sends the `created` webhook event when the deposit enters `PENDING`. Meridian sends the `updated` event only when the deposit reaches `SUCCESS`, `FAILED`, or `ERROR`. ## Auto-sweep feature When funds arrive in a Virtual Account configured for "Auto-sweep," Meridian immediately moves them to the linked account on your platform. Meridian sets your program defaults during onboarding, and you can manage individual account configurations through Meridian's REST API. ```mermaid theme={null} sequenceDiagram actor Employer participant VA as Customer Virtual Account participant LA as Customer Local Account Employer->>VA: Submits payroll deposit VA->>VA: Calculate Conversion from USD to Local currency VA->>LA: Executes domestic transfer in local currency ``` # Retail Virtual Accounts Source: https://docs.mnai.com/products/retail-va/index Embed Meridian Virtual Accounts in your app so users can generate U.S. bank account details and receive transfers, including payroll deposits. ## Customer and program eligibility The Retail Virtual Accounts product is available to **individual customers within supported jurisdictions**, subject to Meridian's compliance and risk requirements. Beyond the customer information you provide, Meridian collects further details within the hosted experience to determine eligibility. This includes: * Employment and source-of-funds information * Government-issued photo ID * A live selfie for identity verification Meridian performs these checks as part of the onboarding flow. Customers must complete them to open and maintain a virtual account. Customers **must not hold a U.S. Social Security Number (SSN)** to be eligible for the Virtual Accounts product. ## Embedded web experience The Virtual Accounts product is a **web-based experience** designed to be **embedded inside your mobile application using a WebView**. This lets you integrate the full onboarding and account management flow directly into your app while maintaining a consistent, branded experience. The Virtual Accounts flow requires **camera access** for identity verification, including photo ID capture and selfie checks. Configure the embedded WebView to support camera permissions, cookies, local storage, JavaScript, and standard browser APIs. ## Money movement Meridian notifies you when funds arrive in the Virtual Account and can move them to your platform through an integration with your systems. You can configure each account to "Auto-sweep" funds immediately to the linked account on your platform. Alternatively, let your customers manage their balance and decide when to transfer funds. **To perform money movement, Meridian needs the ability to instruct fund transfers on your platform.** Your Solutions Engineer will request documentation on your [Credit API](/products/retail-va/withdrawals#credit-api-requirements). > [!NOTE] > > Meridian configures your Auto-sweep preference during the onboarding process. # Information we collect Source: https://docs.mnai.com/products/retail-va/kyc-information The customer information collected during Retail Virtual Account onboarding for KYC. When you onboard a customer into a Retail Virtual Account, Meridian collects the information below for Know Your Customer (KYC) verification. > [!NOTE] > > **This list can change.** The fields collected are driven dynamically by the session API and may be added to, removed, or adjusted over time as compliance requirements evolve — so this page may not always reflect the exact current set. For the authoritative, up-to-date list, refer to the [Create a Session](/products/retail-va/api-reference/sessions/create-session) API reference. ## Customer information | Field | Required | Notes | | :- | :- | :- | | First name | Yes | | | Last name | Yes | | | Email | Yes | | | Phone number | Yes | International format (e.g. `+1234567890`) | | Date of birth | No | | | Address line 1 | No | | | Address line 2 | No | | | City | No | | | State / Province | No | | | Postal code | No | | | Country code | No | ISO country code (e.g. `US`) | | Employment status | No | One of `EMPLOYEE`, `SELF-EMPLOYED`, `RETIRED`, `UNEMPLOYED`, `OTHER` (`SELF_EMPLOYED` also accepted) | | Employment description | No | Free-text description of employment | | Occupation | No | | | Primary source of funds | No | One of `EMPLOYMENT`, `SAVINGS`, `WINNINGS`, `MARITAL`, `REAL_ESTATE`, `TRUST`, `INVESTMENT`, `COMPANY`, `COMPANY_CAPITAL`, `LOAN`, `PRIVATE_CAPITAL`, `GRANT`, `OTHER` | | Source description | No | Free-text description of the funding source | | Monthly deposit volume | No | Expected monthly deposit amount | | Deposit currency | No | ISO currency code (e.g. `USD`) | # Request a Sandbox Source: https://docs.mnai.com/products/retail-va/sandbox Request Sandbox credentials and set up a test environment to build and validate your Meridian integration before going live. ## Requesting Sandbox credentials Meridian provisions Sandbox access on request. To get started, contact your Solutions Engineer and provide the following: * Your organization's name and country of operation * A technical point of contact (name and email) * The webhook URL you'd like Meridian to deliver transaction events to * The customer support site URL for your tech support team (if you have one) * The Credit API endpoint details for any withdrawal requests * Your company logo * Your primary and secondary brand colors (hex values preferred) * Your preference for whether Meridian should Auto-sweep USD funds from Virtual Accounts to local linked accounts Once Meridian receives this information, it provisions your Sandbox environment — including your branded UI configuration — and delivers your credentials securely. ## What you'll receive Upon provisioning, Meridian securely provides: * **API Key** — used in the `X-Meridian-Api-Key` header on all session API requests * **API Secret** — used to compute the HMAC SHA-256 request signature * **Webhook API Key and Secret** — a separate key pair used exclusively for authenticating inbound webhook events These credentials are specific to your Sandbox environment. Meridian issues production credentials separately as part of the go-live process. # Session management Source: https://docs.mnai.com/products/retail-va/session-management Create and manage authenticated sessions that launch Meridian's hosted Virtual Accounts UI inside your application's WebView. ## Create a new UI Session Each session represents a single, self-contained interaction. Your customer completes onboarding or account management actions within the embedded experience and exits when finished. Because the experience runs in a WebView, your application must provide a way for your customer to leave the flow. This is typically a **custom navigation bar or close control** in the host application. Each time your customer re-enters the Virtual Accounts experience, you must create a **new authentication session** and launch a new embedded experience. This ensures that access is explicit, time-bound, and scoped to a single interaction. Your application must follow the sequence below each time a customer enters the Virtual Accounts experience: **1. Create a UI Session** Your backend calls the Virtual Accounts API to [create a new session](/products/retail-va/api-reference/sessions/create-session) and provide user details in the `customer` object with a unique `customerId`. **2. Build the Session URL** The Virtual Accounts API returns a `sessionKey` that you use to generate a fully formed `sessionURL`. Pass this URL to the client without modification. ```jsx Sandbox theme={null} sessionURL = `https://sandbox.va.meridianpay.com?sessionKey=${sessionKey}` ``` ```jsx Production theme={null} sessionURL = `https://va.meridianpay.com?sessionKey=${sessionKey}` ``` **3. Load a WebView** The client loads the `sessionURL` directly inside a [WebView](/products/retail-va/web-view) that meets the required configuration and permission requirements. **4. User completes the flow and exits** Your customer completes onboarding or account actions and exits the experience using the host application's navigation controls. ```mermaid theme={null} sequenceDiagram actor Customer participant App as Partner Mobile App participant Backend as Partner Backend participant API as Meridian Backend participant UI as Meridian UI Session Customer->>App: Taps "Open Virtual Account (VA)" App->>Backend: Create a new VA UI Session Backend->>API: POST /v1/virtual-accounts/sessions API-->>Backend: 201 Created Backend-->>App: Return the sessionKey Note over App,UI: Session is single-use · expires in 30 min · must not be cached or reused App->>UI: Generate and Load the sessionURL in a WebView
(camera · cookies · localStorage · JS enabled) ``` ## UI Session lifecycle Virtual Accounts sessions follow these rules: * Sessions are **single-use** and must not be reused. * Sessions are **short-lived** and expire after **30 minutes**. * Sessions are **payload-bound** to the customer identifier and any onboarding or receiver details submitted at creation time. * Any change to customer identifiers, KYC information, or receiver details requires creating a **new session**. * Session URLs must **not** be cached or stored for later use. These constraints ensure data freshness, deterministic behavior, and secure access to the hosted experience. # Simulate KYC outcomes Source: https://docs.mnai.com/products/retail-va/simulate-kyc Use sandbox test inputs to simulate different KYC application outcomes during development and testing. By default, all KYC applications in the sandbox are automatically approved. Use the inputs below to simulate specific outcomes during testing. | Outcome | How to trigger | | :- | :- | | **Approved** (default) | Submit any application normally — no special input required | | **Document verification declined** | Set the customer's first name to `Decline` (case-sensitive) | | **EDD Required** | Set occupation type to `Other` with the value `EDD Required` | When **EDD Required** is triggered, the application pauses and must be manually resolved by a Meridian ops user before onboarding can continue. See [Handling onboarding exceptions](/products/retail-va/web-view#handling-onboarding-exceptions) for details on how to handle this status in your integration. ## Resetting a test user In sandbox, you can delete a user's virtual account application to reset their onboarding state. This lets you run the same user through onboarding repeatedly in automated test scripts without needing to create a new user each time. Use the [Delete a virtual account application](/products/retail-va/api-reference/simulations/delete-virtual-account-application) endpoint to delete the application, then submit a new one to go through onboarding again from the beginning. > [!WARNING] > > This endpoint is only available in sandbox. Virtual account applications cannot be deleted in production. # WebView Source: https://docs.mnai.com/products/retail-va/web-view The Virtual Accounts experience must be loaded in a WebView environment that supports all required capabilities for onboarding and account management. ## Requirements The WebView must be configured with: * Camera access enabled for photo ID capture and selfie checks * Cookies enabled to maintain session state * Local storage enabled * JavaScript enabled * Standard browser APIs available Unsupported or restricted WebView configurations — such as incognito mode, disabled storage, or camera-disabled environments — can prevent your customer from completing onboarding or cause unrecoverable flows. Verify that your WebView implementation explicitly grants camera permissions and supports persistent storage before launching the Virtual Accounts experience. Once you generate a [Session URL](/products/retail-va/session-management), you can initialize your WebView for either iOS or Android. ```kotlin kotlin theme={null} import android.webkit.PermissionRequest import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient fun loadVirtualAccountsSession(webView: WebView, sessionKey: String) { webView.settings.apply { javaScriptEnabled = true domStorageEnabled = true // localStorage allowFileAccess = true mediaPlaybackRequiresUserGesture = false } // Persist cookies across sessions android.webkit.CookieManager.getInstance().setAcceptCookie(true) android.webkit.CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true) webView.webViewClient = WebViewClient() // Required to grant camera access for photo ID capture and selfie webView.webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { request.grant(request.resources) } } webView.loadUrl("https://va.meridianpay.com?sessionKey=$sessionKey") } ``` ```swift swift theme={null} import WebKit class VirtualAccountsViewController: UIViewController, WKUIDelegate { var webView: WKWebView! override func loadView() { let config = WKWebViewConfiguration() config.allowsInlineMediaPlayback = true config.mediaTypesRequiringUserActionForPlayback = [] // Enable camera access for photo ID capture and selfie config.preferences.javaScriptEnabled = true config.websiteDataStore = WKWebsiteDataStore.default() // persistent cookies & localStorage webView = WKWebView(frame: .zero, configuration: config) webView.uiDelegate = self view = webView } func loadSession(sessionKey: String) { let url = URL(string: "https://va.meridianpay.com?sessionKey=\(sessionKey)")! webView.load(URLRequest(url: url)) } // Required to allow camera permission prompts inside the WebView func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) { decisionHandler(.grant) } } ``` ## User experience The Meridian UI Session provides two initial experiences at start-up: ### 1. KYC onboarding When your customer opens the Virtual Accounts experience for the first time, Meridian detects that no approved KYC application exists and presents the onboarding screens. The flow guides them through providing employment and source-of-funds details, uploading a government-issued photo ID, and completing a live selfie check. Once verified, the account opens and your customer can begin transacting. #### Handling onboarding exceptions Sometimes, your customer must submit additional documentation to open an account. In this scenario, the application has a status of `EDD_REQUIRED`. You and the Meridian team collect the additional documentation outside of the product flow. Meridian may reject a virtual account application for the following reasons: * `BANK_REJECTION` - The bank or banking provider rejected the application. Your customer must reach out to support to resolve the issue. * `RISK_REJECTION` - Your customer failed Meridian's risk assessment. This can happen for many reasons, so your customer must contact support to resolve the issue. * `INTERNAL_ERROR` - An unexpected error occurred while processing the application. Your customer may resubmit, but verify whether the account was still created before allowing a new submission. ### 2. Account home For returning customers with an approved KYC application on record, Meridian routes them directly to the Account Home screen. From here your customer can review their transaction history and initiate new deposits or withdrawals. You do not need to do anything beyond the standard session creation flow. # Webhooks Source: https://docs.mnai.com/products/retail-va/webhooks-overview Learn how Meridian delivers real-time event notifications to your system via HTTP POST webhooks, including configuration, security, and retry behavior. ## Configuration > [!NOTE] > > Meridian configures webhook endpoints during provisioning. You can register separate URLs for Sandbox and production. Meridian delivers all events for a given environment to a single endpoint. If you need to route events differently, your handler must direct them based on the event headers. ## Event routing Use the `X-Meridian-Resource-Type` and `X-Meridian-Event-Type` headers to identify the webhook before you parse the body. These headers tell you which resource the event belongs to and which specific event occurred. | Header | Example values | Use | | :- | :- | :- | | `X-Meridian-Resource-Type` | `virtual_account_transaction` | Routes the payload to the correct resource handler | | `X-Meridian-Event-Type` | See the event types table below | Identifies the specific event for that resource | | Resource | Event types | | :- | :- | | `virtual_account_transaction` | `virtual_account_transaction_created`, `virtual_account_transaction_updated` | Both transaction events carry the same [Virtual Account Transaction](/products/retail-va/api-reference/webhooks/webhooks-transaction) payload: `created` fires when a new transaction is initiated, and `updated` fires when the status of an existing transaction changes. Read the headers first, then deserialize the body with the matching webhook schema. ## Security Webhook requests use the same HMAC SHA-256 scheme as the REST API, but with a **dedicated Webhook API Key and Secret** issued separately from your REST API credentials. Each request includes the following headers: | Header | Description | | :- | :- | | `X-Meridian-Api-Key` | Issued by Meridian, specifically for Webhooks | | `X-Meridian-Timestamp` | Current time in milliseconds (within 60 seconds of the request) | | `X-Meridian-Signature` | HMAC SHA-256 signature | Validate the signature on every inbound request before processing the payload. Use the same canonical string construction described in [Authentication](/products/retail-va/authentication#constructing-the-signature). ## Delivery and retries Meridian considers a webhook delivered when your endpoint returns an HTTP `2xx` response within **10 seconds**. Meridian retries any other outcome: a timeout, a connection error, or a non-`2xx` status, including `4xx`. Retries use exponential backoff starting at 2 seconds and doubling each time, for up to **10 attempts over roughly 12 minutes**. After the final attempt, Meridian does not deliver the event again automatically. The `virtual_account_transaction_created` event is the exception. Meridian sends it once, on a best-effort basis, and does not retry it. Treat the `updated` event as the authoritative record of a transaction's outcome. Because retries can result in duplicate deliveries, your webhook handler should be **idempotent**. Processing the same event more than once should produce the same outcome. # Withdrawals Source: https://docs.mnai.com/products/retail-va/withdrawals Learn how customers withdraw funds from their Virtual Account and receive local currency payouts through your Credit API. ## How withdrawals work From the Account Home screen, your customer selects Withdrawal and enters the amount of USD to convert. Meridian deducts the amount from the Virtual Account balance and marks the transaction as pending. Meridian then sends a payout request to your designated Credit API. Your Credit API transfers the funds in local currency to your customer's local bank account. Once the transfer is confirmed, the Virtual Account balance updates to reflect the settled status. ```mermaid theme={null} sequenceDiagram actor Customer participant UI as Meridian UI Session participant Backend as Partner Backend Customer->>UI: Customer requests a withdrawal UI->>UI: Refresh balance with Pending status UI->>Backend: POST a request to the Credit API Backend->>Backend: Transfers local currency into customer's local account Backend-->>UI: Returns confirmation response UI->>UI: Refresh balance with Settled status ``` ## How withdrawals are funded Meridian fulfills withdrawals through the bank partner's Credit API. During onboarding, Meridian's Implementation team integrates with that API to push funds from Meridian's sponsored account to the customer's account at the home bank. For full details on how the Credit API works and what the integration requires, see the [Credit API](/products/bank-transfers/credit-api) page.