Developers

Build payments into your product

One HTTPS request creates a payment and returns a hosted checkout link. Everything documented here is served by the live API today; anything not built yet is listed as unavailable rather than promised.

Developers

Getting started

This is the API a business website uses with MIRRA Pay today: a merchant key (mpk_live_… / mpk_test_…) and one hosted checkout endpoint.

1. Get your merchant key

MIRRA issues an mpk_test_… key to build with and an mpk_live_… key for real money. Store it in a server environment variable such as MIRRA_API_KEY.

2. Work out the total on your server

Your website posts an order or cart id to your own backend. Your backend recalculates the total from its own data — never from a value the browser sent.

3. Create the checkout session

POST /checkout/sessions with amount_minor, currency, description, reference and an Idempotency-Key — or with price_id for a fixed MIRRA price.

4. Redirect to data.checkout_url

MIRRA hosts the payment page with the amount locked. The payer never types an amount.

5. Confirm from MIRRA, not the browser

Release the order on the signed payment.succeeded webhook, or by reading the session back with checkout:read. A return to your success URL proves nothing.

# Dynamic cart total — call this from your server, never from a browser.
curl -X POST "https://mirrapaysolution.com/api/public/v1/checkout/sessions" \
  -H "Authorization: Bearer $MIRRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-123" \
  -d '{
  "amount_minor": 42300,
  "currency": "USD",
  "description": "Seafood order #123",
  "reference": "ORDER-123",
  "customer_name": "Nuel Jo",
  "customer_email": "customer@example.com"
}'

# Fixed MIRRA price instead of a cart total:
curl -X POST "https://mirrapaysolution.com/api/public/v1/checkout/sessions" \
  -H "Authorization: Bearer $MIRRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-123" \
  -d '{
  "price_id": "1f9c2b64-1d3e-4a91-b0c8-9e4a12345678",
  "quantity": 1,
  "reference": "ORDER-123"
}'

Developers

Authentication

Authorization: Bearer mpk_live_<key_id>.<secret>
Authorization: Bearer mpk_test_<key_id>.<secret>
  • The key alone resolves your merchant account, integration and environment. An account identifier sent in a body is never used for authorisation.
  • A live key is re-authorised on every request against your account's current permission to accept live payments; a test key is unaffected.
  • The secret half is stored only as a SHA-256 hash and compared in constant time. It is shown once, at creation, and cannot be recovered.
  • Call the API from your own server only. Never place a key in browser code.
checkout:create

Create hosted checkout sessions.

checkout:read

Retrieve a checkout session you created.

products:read

List the MIRRA catalogue items belonging to your account.

Developers

Products & multi-product cart

MIRRA Pay can hold your products for you. Read them, show them, then send back only the chosen items and quantities — MIRRA Pay adds up its own approved prices and returns one locked checkout link.

1. Get the products from MIRRA

GET /products with products:read returns every active, approved item on your account: price_id, name, description, image_url, price_minor, currency and sku. You do not need a product database of your own.

2. Show them however you like

Render the returned items as product cards on your site, or ignore them and keep your own layout — only the price_id matters later.

3. The customer picks products

Your page collects the chosen items and posts them to your own backend, as a list of price_id and quantity. No price and no total.

4. Send price_id + quantity to MIRRA

POST /checkout/sessions with items: [{ price_id, quantity }] and an Idempotency-Key. MIRRA multiplies and totals its own approved prices.

5. Redirect to data.checkout_url

One cart becomes one checkout with one locked total. The payer sees the ordered products, the quantities and the total, and can edit nothing.

6. Confirm from the signed webhook

Release the order on payment.succeeded, or by reading the session back with checkout:read. A return to your success URL proves nothing.

  • Send items only. A request that carries items together with amount_minor, price_id, product_id or a top-level quantity is refused with invalid_request.
  • MIRRA computes the total as the sum of price_minor × quantity from its own approved price records. A total sent by your site or a browser is never used.
  • Every price_id must belong to your account and this environment, and must be active and approved — otherwise price_not_found or price_not_active.
  • One cart, one currency. Mixed currencies are refused with mixed_currencies_not_supported. There is no currency conversion in checkout creation.
  • Between 1 and 50 distinct items per cart; each quantity is a whole number of at least 1. The same price_id twice in one cart is refused.
  • Idempotency-Key is required: the same key with the same cart returns the original checkout; the same key with a different cart is refused with idempotency_key_reuse.
  • One cart creates exactly one payment and one checkout_url — never one payment per product.
# Multi-product cart. MIRRA adds up its own approved prices.
curl -X POST "https://mirrapaysolution.com/api/public/v1/checkout/sessions" \
  -H "Authorization: Bearer $MIRRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-123" \
  -d '{"items":[{"price_id":"84df4deb-e50b-4040-bcab-5ead5a6d6cd3","quantity":4},{"price_id":"1f0b2c9e-2f4a-4c11-9d2e-77a1b3c4d5e6","quantity":2}],"reference":"ORDER-123","description":"Seafood order #123","customer_name":"John Smith","customer_email":"customer@example.com"}'

Developers

Direct payment API

Already know the final order amount on your own backend? Post it to MIRRA Pay and get one payment link back. No price_id, no MIRRA Pay catalogue, no product synchronisation.

1. Your backend knows the amount

Your own website and backend stay authoritative for the cart or order. Recalculate the final total server-side — never trust a total that came from the browser.

2. POST /payments

Send amount_minor, currency, description, reference and an Idempotency-Key with your mpk_live_… key. No price_id, no MIRRA catalogue, no product synchronisation.

3. Redirect to data.checkout_url

MIRRA hosts the payment page with the amount locked. The payer never types an amount.

4. Confirm from the signed webhook

Release the order on payment.succeeded. A return to your success URL is not proof of payment.

  • This endpoint is catalogue-free: price_id, product_id and items are refused with invalid_request. Use POST /checkout/sessions when you want MIRRA to hold your products.
  • Server-to-server only. A request carrying a browser Origin header is refused with server_to_server_required — your secret key must never reach browser code.
  • Idempotency-Key is required. The same key with the same body returns the original payment; the same key with a different body is refused with idempotency_key_reuse.
  • amount_minor is in minor units: 42300 means $423.00 USD. It must be a positive whole number.
  • currency must be one of AED, USD, EUR, GBP.
  • One call creates exactly one payment, one checkout session and one checkout_url. Retries never charge twice.
  • The same route also serves the older sub-account API. mpk_live_/mpk_test_ keys are Merchant Integration API credentials and require checkout:create; msk_live_/msk_test_ keys are Sub-account API credentials and require payments:write. The two key families are separate systems and are not interchangeable.
# 42300 = $423.00 USD. Call from YOUR SERVER ONLY.
curl -X POST https://mirrapaysolution.com/api/public/v1/payments \
  -H "Authorization: Bearer $MIRRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-123" \
  -d '{"amount_minor": 42300, "currency": "USD", "description": "Seafood order #123", "reference": "ORDER-123", "customer_name": "John Smith", "customer_email": "john@example.com", "success_url": "https://seafoodpremium.world/payment/mirra/success", "cancel_url": "https://seafoodpremium.world/en/checkout"}'
{
  "data": {
    "id": "3c7b18d2-90ae-4a51-8f6c-1d2e3f405162",
    "object": "payment",
    "payment_request_id": "3c7b18d2-90ae-4a51-8f6c-1d2e3f405162",
    "status": "open",
    "amount_minor": 42300,
    "currency": "USD",
    "description": "Seafood order #123",
    "reference": "ORDER-123",
    "checkout_url": "https://mirrapaysolution.com/pay/9f2c…",
    "payment_reference": "MR-10241",
    "session_id": "b41d9c07-53ea-49c6-8f5b-2a0c9d7e1f34",
    "success_url": "https://seafoodpremium.world/payment/mirra/success",
    "cancel_url": "https://seafoodpremium.world/en/checkout",
    "livemode": true,
    "expires_at": "2026-01-14T10:31:22.104Z",
    "created_at": "2026-01-14T09:31:22.104Z"
  },
  "request_id": "req_…"
}

Developers

Payment identifiers

A payment request, a checkout session and a final payment are different MIRRA Pay objects. This is which identifier appears when, so you never have to guess how to correlate an event with an order.

payment_request_id

The MIRRA payment request created when POST /payments succeeds. data.id in that response currently equals this value and keeps doing so for backward compatibility; data.payment_request_id repeats it explicitly.

payment_id

The final MIRRA payment record, created or updated after the provider has processed the payment. It does not exist yet when POST /payments returns, and it arrives as data.payment_id in the payment.succeeded and payment.failed webhooks.

session_id

The hosted checkout session behind checkout_url. Returned as data.session_id by POST /payments and as data.session_id in checkout.session.completed.

reference

Your own order identifier, passed in on creation and echoed back unchanged in the response and in every webhook. It is the easiest field for matching an event to your internal order.

  • data.id from POST /payments is the payment request id — it is NOT the webhook's data.payment_id. They are different objects.
  • Correlate with reference, or with payment_request_id, which is present in both the creation response and the webhook payloads.
  • reference is your own value and is not made globally unique across merchants by MIRRA.

Developers

Endpoints

One endpoint creates every checkout. Exactly one amount mode applies per request, and the payer can never edit the amount.

Fixed MIRRA price

Send price_id (and an optional quantity). MIRRA reads the amount from the approved price record; your page can never change it.

MIRRA catalogue product

Send product_id (and an optional quantity). MIRRA reads the amount from the active product record.

Multi-product cart

Send items: an array of { price_id, quantity }. MIRRA multiplies and adds up its own approved prices, so no total travels from your site. Do not send amount_minor, currency, price_id or product_id. Requires an Idempotency-Key.

Dynamic cart / order amount

Send amount_minor with currency, description and reference, from your own trusted backend. Do not send price_id or product_id. Requires an Idempotency-Key and no browser Origin header.

  • Calculate the order total in your own trusted backend from your own authoritative data, then send that locked amount. Never forward a total that a browser could edit.
  • Your website must call your own backend, and your backend calls MIRRA. A request carrying a browser Origin header is refused with server_to_server_required.
  • Idempotency-Key is required in dynamic mode: a retried cart submission returns the original checkout instead of charging twice.
  • quantity must be omitted or exactly 1 — the amount you send is already the order total.
  • currency must be one of the currencies MIRRA can charge today: AED, USD, EUR, GBP.
  • Redirect the customer to data.checkout_url. The payer never sees an editable amount field.
  • A return to your success URL is not proof of payment. Confirm from the signed webhook or by reading the session back.
GEThttps://mirrapaysolution.com/api/public/v1/products

List your MIRRA catalogue

Returns the active, approved catalogue items belonging to the presenting key's merchant account and environment — enough to render product cards, and the price_id you send back when the customer checks out. Another merchant's catalogue is never visible.

Required scope: products:read

Query parameters

FieldTypeMeaning
limitOptionalinteger (1–100)Page size. Defaults to 50.
offsetOptionalinteger ≥ 0Rows to skip. Defaults to 0.
activeOptionaltrue | falseDefaults to true. Pass false to review paused items.
searchOptionalstring (≤120)Matches the item name or SKU.

Request

curl "https://mirrapaysolution.com/api/public/v1/products?limit=50" \
  -H "Authorization: Bearer $MIRRA_API_KEY"
Example response
{
  "data": [
    {
      "object": "product",
      "price_id": "84df4deb-e50b-4040-bcab-5ead5a6d6cd3",
      "product_id": null,
      "name": "Bamboo Lobster — 1 kg",
      "description": "Fresh, packed on ice.",
      "image_url": "https://cdn.example/lobster.jpg",
      "price_minor": 5000,
      "currency": "USD",
      "sku": "LOBSTER-1KG",
      "active": true,
      "available": true,
      "created_at": "2026-09-15T09:04:11.204Z",
      "updated_at": "2026-09-15T09:04:11.204Z"
    }
  ],
  "has_more": false,
  "livemode": true,
  "request_id": "8f2a…"
}

Response fields

FieldTypeMeaning
data[].price_idOptionalstring (uuid)Send this back in items when the customer checks out.
data[].nameOptionalstringItem name.
data[].descriptionOptionalstring | nullOptional description.
data[].image_urlOptionalstring | nullOptional product image.
data[].price_minorOptionalintegerUnit price in minor units (5000 means 50.00).
data[].currencyOptionalstringUnit price currency.
data[].skuOptionalstring | nullYour own item reference.
data[].availableOptionalbooleanWhether the item is sellable.
has_moreOptionalbooleanTrue when another page exists.
request_idOptionalstringUnique identifier for this call.

Errors

HTTPCodeMeaning
422invalid_requestlimit, offset, active or search is out of range.
401invalid_api_keyMissing, malformed or unrecognised mpk key.
401api_key_revokedThe key was revoked.
401api_key_mode_mismatchA test key was used against live data or the reverse.
403insufficient_scopeThe key lacks the required scope.
403integration_disabledThe website integration is not active.
403live_mode_not_enabledA live key was used while the account is not currently approved for live payments.
429rate_limited120 requests per 60 seconds per key exceeded.
500internal_errorUnexpected failure. Retry and quote request_id.
POSThttps://mirrapaysolution.com/api/public/v1/checkout/sessions

Create a checkout session

Creates a hosted checkout and returns data.checkout_url. Exactly one amount mode applies: a MIRRA price, a MIRRA product, a multi-product cart of MIRRA prices, or a dynamic order amount sent by your own backend. Acceptance limits and pricing are resolved server-side from your account.

Required scope: checkout:create

Body parameters

FieldTypeMeaning
itemsOptionalarray of { price_id: uuid, quantity: integer 1–1000 }Cart mode: 1–50 distinct MIRRA prices. MIRRA totals its own approved prices. Never send this together with price_id, product_id, quantity, amount_minor or currency, and always send an Idempotency-Key.
price_idOptionaluuidFixed mode: an approved MIRRA price. Never send this together with amount_minor.
product_idOptionaluuidFixed mode: an active MIRRA catalogue product. Never send this together with amount_minor.
quantityOptionalinteger (1–1000)Fixed modes only. In dynamic mode it must be omitted or exactly 1.
amount_minorRequiredinteger > 0Dynamic mode: the order total in minor units (42300 means 423.00), calculated by your own backend. Required in dynamic mode only.
currencyRequiredstring (3)Dynamic mode: one of AED, USD, EUR, GBP. Required in dynamic mode only.
descriptionRequiredstring (1–300)Dynamic mode: what the customer is paying for. Required in dynamic mode only.
referenceRequiredstring (1–120)Your own order identifier. Required in dynamic mode; optional in fixed modes.
customer_nameOptionalstring (≤120)Optional customer name.
customer_emailOptionalstring (email)Optional customer email.
success_urlOptionalstring (https, ≤512)Optional. Must start with a website address registered for this environment. Returning here is not proof of payment.
cancel_urlOptionalstring (https, ≤512)Optional. Same registration rule as success_url.
metadataOptionalobject (string values, ≤500 each)Optional key/value pairs stored with the session and returned to you.

Request

# Dynamic cart total — call this from your server, never from a browser.
curl -X POST "https://mirrapaysolution.com/api/public/v1/checkout/sessions" \
  -H "Authorization: Bearer $MIRRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-123" \
  -d '{
  "amount_minor": 42300,
  "currency": "USD",
  "description": "Seafood order #123",
  "reference": "ORDER-123",
  "customer_name": "Nuel Jo",
  "customer_email": "customer@example.com"
}'

# Fixed MIRRA price instead of a cart total:
curl -X POST "https://mirrapaysolution.com/api/public/v1/checkout/sessions" \
  -H "Authorization: Bearer $MIRRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-123" \
  -d '{
  "price_id": "1f9c2b64-1d3e-4a91-b0c8-9e4a12345678",
  "quantity": 1,
  "reference": "ORDER-123"
}'
Example response
{
  "data": {
    "id": "7c0f9a52-4a1d-4a0e-9b1a-2f6d5e8c1234",
    "object": "checkout.session",
    "status": "open",
    "amount_minor": 42300,
    "currency": "USD",
    "quantity": 1,
    "description": "Seafood order #123",
    "reference": "ORDER-123",
    "payment_id": "b8e1…",
    "payment_reference": "MR-0440EAF284",
    "checkout_url": "https://mirrapaysolution.com/pay/abc123",
    "success_url": "https://shop.example/payment/success",
    "cancel_url": "https://shop.example/payment/cancel",
    "livemode": true,
    "expires_at": "2026-09-16T11:12:04.881Z",
    "created_at": "2026-09-16T10:12:04.881Z"
  },
  "request_id": "8f2a…"
}

Response fields

FieldTypeMeaning
data.idOptionalstring (uuid)Checkout session identifier.
data.checkout_urlOptionalstring (url)Redirect the customer here. The amount is locked and cannot be edited by the payer.
data.statusOptionalstringopen, completed or expired.
data.amount_minorOptionalintegerAmount charged, in minor units.
data.currencyOptionalstringCharge currency.
data.quantityOptionalinteger1 in dynamic mode; the total number of units in cart mode.
data.items[]OptionalarrayCart mode only: a snapshot of each ordered line — price_id, name, unit_amount_minor, quantity, line_total_minor, currency.
data.referenceOptionalstring | nullYour own reference as sent.
data.payment_referenceOptionalstringThe permanent MIRRA payment reference (MR-…).
data.livemodeOptionalbooleanTrue for an mpk_live key.
data.expires_atOptionalstring (ISO 8601)When the checkout expires.
request_idOptionalstringUnique identifier for this call.

Errors

HTTPCodeMeaning
400invalid_jsonThe body could not be parsed as JSON.
400idempotency_key_requiredCart mode or dynamic mode was used without an Idempotency-Key header.
403server_to_server_requiredA dynamic request arrived with a browser Origin header. Call from your server.
403origin_not_allowedThe Origin is not on the key's allow-list.
422invalid_requestA field is missing or out of range, or modes were mixed (price_id/product_id together with amount_minor, items together with any other amount field, the same price_id twice in one cart, or quantity other than 1 in dynamic mode).
422mixed_currencies_not_supportedThe items in one cart do not all share the same currency.
422unsupported_currencycurrency is not one of AED, USD, EUR, GBP.
404price_not_foundNo such price on your account.
422price_not_activeThe price is paused or not approved.
404product_not_foundNo such product on your account.
422product_not_activeThe product is paused.
422invalid_amountThe resolved amount is not a positive integer.
422amount_below_minimumBelow the minimum for your account.
422amount_over_capAbove the ceiling for your account.
422invalid_success_urlsuccess_url is not an absolute https URL, or is too long.
422invalid_cancel_urlcancel_url is not an absolute https URL, or is too long.
422redirect_origin_not_registeredThe URL's website address is not registered for this environment.
409idempotency_key_reuseThe same Idempotency-Key was replayed with a different body.
409idempotency_in_progressA request with the same Idempotency-Key is still running. Retry shortly.
401invalid_api_keyMissing, malformed or unrecognised mpk key.
401api_key_revokedThe key was revoked.
401api_key_mode_mismatchA test key was used against live data or the reverse.
403insufficient_scopeThe key lacks the required scope.
403integration_disabledThe website integration is not active.
403live_mode_not_enabledA live key was used while the account is not currently approved for live payments.
429rate_limited120 requests per 60 seconds per key exceeded.
500internal_errorUnexpected failure. Retry and quote request_id.
GEThttps://mirrapaysolution.com/api/public/v1/checkout/sessions/{id}

Retrieve a checkout session

Returns one session belonging to the presenting key's merchant and environment. The status is derived from the confirmed payment record, never from a browser return: open until the provider confirms, then completed; an expired open session reports expired.

Required scope: checkout:read

Request

curl "https://mirrapaysolution.com/api/public/v1/checkout/sessions/7c0f9a52-4a1d-4a0e-9b1a-2f6d5e8c1234" \
  -H "Authorization: Bearer $MIRRA_API_KEY"
Example response
{
  "data": {
    "id": "7c0f9a52-4a1d-4a0e-9b1a-2f6d5e8c1234",
    "object": "checkout.session",
    "status": "completed",
    "amount_minor": 42300,
    "currency": "USD",
    "quantity": 1,
    "description": "Seafood order #123",
    "reference": "ORDER-123",
    "customer_email": "customer@example.com",
    "payment_reference": "MR-0440EAF284",
    "paid_at": "2026-09-16T10:15:22.104Z",
    "checkout_url": "https://mirrapaysolution.com/pay/abc123",
    "livemode": true,
    "expires_at": "2026-09-16T11:12:04.881Z",
    "completed_at": "2026-09-16T10:15:22.104Z",
    "created_at": "2026-09-16T10:12:04.881Z"
  },
  "request_id": "8f2a…"
}

Response fields

FieldTypeMeaning
data.statusOptionalstringopen, completed or expired.
data.paid_atOptionalstring | nullSet once the payment is confirmed.
data.referenceOptionalstring | nullYour own order reference.
data.payment_referenceOptionalstring | nullThe permanent MIRRA payment reference (MR-…).
data.amount_minorOptionalintegerAmount charged, in minor units.
request_idOptionalstringUnique identifier for this call.

Errors

HTTPCodeMeaning
404session_not_foundNo such session on your account in this environment.
401invalid_api_keyMissing, malformed or unrecognised mpk key.
401api_key_revokedThe key was revoked.
401api_key_mode_mismatchA test key was used against live data or the reverse.
403insufficient_scopeThe key lacks the required scope.
403integration_disabledThe website integration is not active.
403live_mode_not_enabledA live key was used while the account is not currently approved for live payments.
429rate_limited120 requests per 60 seconds per key exceeded.
500internal_errorUnexpected failure. Retry and quote request_id.
POSThttps://mirrapaysolution.com/api/public/v1/payments

Create a payment (direct)

Use this when your own backend already knows the final order amount. Your server posts the locked amount, MIRRA returns one hosted payment link, you redirect the customer, and the signed webhook confirms the result. No price_id and no MIRRA catalogue are required. Internally this creates exactly the same single payment record, single checkout session and single hosted checkout as POST /checkout/sessions.

Required scope: checkout:create

Body parameters

FieldTypeMeaning
amount_minorRequiredintegerOrder total in minor units. $423.00 USD is 42300. Must be a positive whole number.
currencyRequiredstringOne of AED, USD, EUR, GBP.
descriptionRequiredstringShown to the payer.
referenceRequiredstringYour own order reference.
customer_nameOptionalstringOptional payer name.
customer_emailOptionalstringOptional payer email.
success_urlOptionalstringAbsolute https URL on a registered origin.
cancel_urlOptionalstringAbsolute https URL on a registered origin.
metadataOptionalobjectYour own key/value pairs, e.g. order_id.

Request

# 42300 = $423.00 USD. Call from YOUR SERVER ONLY.
curl -X POST https://mirrapaysolution.com/api/public/v1/payments \
  -H "Authorization: Bearer $MIRRA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-123" \
  -d '{"amount_minor": 42300, "currency": "USD", "description": "Seafood order #123", "reference": "ORDER-123", "customer_name": "John Smith", "customer_email": "john@example.com", "success_url": "https://seafoodpremium.world/payment/mirra/success", "cancel_url": "https://seafoodpremium.world/en/checkout"}'
Example response
{
  "data": {
    "id": "b41e7d02-8a4c-4c39-9a5f-2a1c7d9e4f10",
    "object": "payment",
    "status": "open",
    "amount_minor": 42300,
    "currency": "USD",
    "description": "Seafood order #123",
    "reference": "ORDER-123",
    "checkout_url": "https://mirrapaysolution.com/pay/abc123",
    "payment_reference": "MR-0440EAF284",
    "session_id": "7c0f9a52-4a1d-4a0e-9b1a-2f6d5e8c1234",
    "livemode": true,
    "expires_at": "2026-09-16T11:12:04.881Z",
    "created_at": "2026-09-16T10:12:04.881Z"
  },
  "request_id": "8f2a…"
}

Response fields

FieldTypeMeaning
data.checkout_urlOptionalstringRedirect the customer here. The amount is already locked.
data.idOptionalstringThe MIRRA payment identifier.
data.session_idOptionalstringThe checkout session behind this payment.
data.payment_referenceOptionalstring | nullThe permanent MIRRA payment reference (MR-…).
request_idOptionalstringUnique identifier for this call.

Errors

HTTPCodeMeaning
400invalid_jsonThe body is not valid JSON.
400idempotency_key_requiredIdempotency-Key is mandatory on this endpoint.
403server_to_server_requiredThe call carried a browser Origin header. Call it from your backend only.
422invalid_requestThe body is not a JSON object, or it also named price_id, product_id or items — this endpoint is catalogue-free.
422invalid_amountamount_minor must be a positive JSON integer in minor units: 42300 means 423.00. A string ("42300") or a decimal (423.00) is refused.
422invalid_currencycurrency must be a 3-letter code.
422description_requireddescription is required, 1–300 characters.
422reference_requiredreference is required, 1–120 characters.
422invalid_customer_namecustomer_name must be a string of 120 characters or fewer.
422invalid_customer_emailcustomer_email must be a valid email address.
422invalid_metadatametadata must be a flat object whose values are STRINGS of 500 characters or fewer — numbers, booleans, nulls, arrays and nested objects are refused.
422unsupported_currencyThe currency cannot be charged today.
422amount_below_minimumBelow the accepted minimum.
422amount_over_capAbove your current acceptance cap.
422invalid_success_urlsuccess_url is not an absolute https URL.
422invalid_cancel_urlcancel_url is not an absolute https URL.
422redirect_origin_not_registeredThe URL's website address is not registered for this environment.
409idempotency_key_reuseThe same Idempotency-Key was replayed with a different body.
409idempotency_in_progressA request with the same Idempotency-Key is still running. Retry shortly.
401invalid_api_keyMissing, malformed or unrecognised mpk key.
401api_key_revokedThe key was revoked.
401api_key_mode_mismatchA test key was used against live data or the reverse.
403insufficient_scopeThe key lacks the required scope.
403integration_disabledThe website integration is not active.
403live_mode_not_enabledA live key was used while the account is not currently approved for live payments.
429rate_limited120 requests per 60 seconds per key exceeded.
500internal_errorUnexpected failure. Retry and quote request_id.

Developers

Errors and retries

Every failure returns a stable machine code and a request identifier. Retry 429 and 5xx responses with exponential backoff; never retry a 4xx without changing the request.

{
  "error": { "code": "invalid_request" },
  "request_id": "8f2a…"
}

Errors carry a stable machine code and nothing else. Database messages, stack traces and infrastructure details are never returned.

Every response body — success or error — carries a unique request_id. Quote it when contacting support.

Developers

Rate limits

120 requests per 60 seconds, per API key. Exceeding the limit returns HTTP 429 with code rate_limited.

Rate-limit response headers are not returned yet. Handle HTTP 429 and retry with backoff.

Developers

Duplicate protection

Send your own unique Idempotency-Key on POST /payments. A retry with the same key and the same body returns the original payment (with idempotent_replay: true) instead of creating a second one. The same key with a different body is rejected with 409 idempotency_key_reuse; a retry while the first call is still running returns 409 idempotency_in_progress. Keys are scoped to your own API key.

AvailableIdempotency-Key

Developers

Return URLs

Send success_url and cancel_url when you create a payment, or set defaults per environment in your workspace. They decide where the customer's browser goes after checkout — nothing more.

  • A return is never proof of payment. Read the payment or wait for the signed event before you release goods, credit an account or send a licence.
  • MIRRA appends mirra_payment_id to your success and cancel URLs, so your page knows which payment to check.
  • Both URLs must be absolute and https, at most 512 characters, and must start with one of the website addresses you registered for the same environment. Local addresses such as http://localhost:3000 are allowed in test only.
  • If you send no URL, the default recorded for that environment is used. If there is no default either, the customer stays on the MIRRA receipt page.
  • Once a payment is created its return URLs are fixed and cannot be changed, so a later request cannot redirect an existing payment somewhere else.
// Your success page — verify before you fulfil.
app.get("/payment/success", async (req, res) => {
  const id = req.query.mirra_payment_id; // appended by MIRRA on return
  if (!id) return res.status(400).send("missing payment reference");

  const check = await fetch(`https://mirrapaysolution.com/api/public/v1/payments/${id}`, {
    headers: { Authorization: `Bearer ${process.env.MIRRA_API_KEY}` },
  });
  const payload = await check.json();

  // Only a succeeded payment reported by MIRRA may release the order.
  if (payload.data?.payment_status === "succeeded") {
    await fulfilOrder(payload.data.reference);
    return res.render("thank-you");
  }
  res.render("payment-pending");
});

Developers

Webhooks

MIRRA Pay signs every event it sends to your endpoint. Verify the signature over the raw request body before trusting a delivery, and reject anything outside the replay window.

Availablepayment.succeeded · payment.failed

Failed deliveries are retried up to 6 times with increasing delays. Every attempt repeats the same Mirra-Event-Id value, so de-duplicate on it and treat repeats as the same event.

Signed payload
<timestamp>.<raw_request_body>
Algorithm
HMAC-SHA256, hex encoded
Replay tolerance
300 seconds
  • Verify the signature over the RAW request body before parsing it.
  • Reject deliveries whose timestamp is outside the tolerance window to defeat replay.
  • Compare signatures in constant time.
  • Never accept an event because of the sending IP address.
  • Treat Mirra-Event-Id as the de-duplication key: the same event id may arrive more than once.
  • Respond 2xx quickly and process asynchronously.
// Verify every delivery before trusting it.
import crypto from "node:crypto";

function verify(rawBody, timestamp, signature, secret) {
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (age > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(signature, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Merchant integration events

These are the events sent to merchant integrations authenticated with mpk_live_/mpk_test_ keys. Every field below is taken from the live MIRRA Pay serializer — nothing here is aspirational.

Availablepayment.succeeded · payment.failed · checkout.session.completed
Headers
Mirra-Signature · Mirra-Event-Id · Mirra-Delivery-Attempt
Signed payload
t=<unix_timestamp>,v1=<hex_hmac> — <timestamp>.<raw_request_body>
Algorithm
HMAC-SHA256, hex encoded
  • Verify Mirra-Signature over the RAW request body, before any JSON parsing or re-serialisation.
  • The signed message is <timestamp>.<raw_body>, hashed with HMAC-SHA256 using your endpoint's signing secret and compared in constant time.
  • A failed delivery is retried up to 6 times with delays of 30s, 2m, 10m, 30m and 2h. Mirra-Delivery-Attempt carries the attempt number.
  • Event ids are deterministic, so a provider replay produces the same id and no second delivery. De-duplicate on Mirra-Event-Id and treat a repeat as already processed.
  • Respond with any 2xx as soon as you have stored the event; anything else counts as a failure and is retried.
  • A return to your browser success_url is NOT authoritative confirmation. Only the signed webhook, or reading the session back from the API, confirms a payment.
  • test.ping is only sent when you trigger a connectivity test from your MIRRA integration screen. It carries no payment data.

payment.succeeded

{
  "id": "evt_<payment_id>_payment_succeeded",
  "type": "payment.succeeded",
  "created_at": "2026-01-14T09:31:22.104Z",
  "livemode": true,
  "data": {
    "payment_id": "8f0c1f3a-2b44-4d90-9a1f-7c2d5e6f8a11",
    "payment_request_id": "3c7b18d2-90ae-4a51-8f6c-1d2e3f405162",
    "reference": "ORDER-123",
    "gross_amount": 42300,
    "fee_amount": 4230,
    "additional_fee_amount": 0,
    "net_amount": 38070,
    "currency": "USD",
    "payment_status": "succeeded",
    "settlement_status": "available"
  }
}

payment.failed

{
  "id": "evt_<payment_id>_payment_failed",
  "type": "payment.failed",
  "created_at": "2026-01-14T09:33:02.881Z",
  "livemode": true,
  "data": {
    "payment_id": "8f0c1f3a-2b44-4d90-9a1f-7c2d5e6f8a11",
    "payment_request_id": "3c7b18d2-90ae-4a51-8f6c-1d2e3f405162",
    "reference": "ORDER-123",
    "gross_amount": 42300,
    "fee_amount": 0,
    "additional_fee_amount": 0,
    "net_amount": 0,
    "currency": "USD",
    "payment_status": "failed",
    "settlement_status": "none"
  }
}

checkout.session.completed

{
  "id": "evt_<session_id>_checkout_session_completed",
  "type": "checkout.session.completed",
  "created_at": "2026-01-14T09:31:22.310Z",
  "livemode": true,
  "data": {
    "session_id": "b41d9c07-53ea-49c6-8f5b-2a0c9d7e1f34",
    "payment_id": "8f0c1f3a-2b44-4d90-9a1f-7c2d5e6f8a11",
    "payment_request_id": "3c7b18d2-90ae-4a51-8f6c-1d2e3f405162",
    "reference": "ORDER-123",
    "amount_minor": 42300,
    "currency": "USD"
  }
}

Developers

Sub-account API (legacy, account-scoped)

A separate, older account-scoped API used by sub-accounts with msk_… credentials. If you were given an mpk_… key, use the merchant checkout API above instead — not this one.

Authorization: Bearer msk_live_<key_id>.<secret>
Authorization: Bearer msk_test_<key_id>.<secret>
  • A key resolves the account on its own. Any account identifier sent in a body or query string is ignored for authorisation, so one key can never read another merchant's data.
  • A live key rejects requests made against test credentials and the reverse, so the two environments can never be mixed by accident.
  • The secret half of a key is stored only as a SHA-256 hash and compared in constant time. It is shown once, at creation, and cannot be recovered afterwards.
payments:write

Create payments and hosted checkout links.

payments:read

List and retrieve payments belonging to your account.

POSThttps://mirrapaysolution.com/api/public/v1/payments

Create a payment

Creates a payment and returns a hosted checkout URL. Send your customer to that URL to pay. Amount limits and the applicable rate are resolved server-side from your own account pricing; a request cannot choose its own rate.

Required scope: payments:write

Body parameters

FieldTypeMeaning
amount_minorRequiredintegerAmount in the currency's minor unit. 10000 means 100.00.
currencyRequiredstring (3)ISO 4217 code, e.g. USD.
descriptionRequiredstring (1–300)What the customer is paying for. Shown on checkout.
customer_nameOptionalstring (≤120)Optional customer name.
customer_emailOptionalstring (email)Optional customer email.
referenceOptionalstring (≤120)Your own order or invoice identifier. Returned on reads.
success_urlOptionalstring (url, ≤512)Where the browser returns after a successful payment. Must be https and start with one of the website addresses registered for the same environment (test or live). MIRRA appends mirra_payment_id. A redirect is not proof of payment.
cancel_urlOptionalstring (url, ≤512)Where the browser returns if the customer abandons checkout. Same registration rule as success_url.

Request

curl -X POST "https://mirrapaysolution.com/api/public/v1/payments" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-8891-attempt-1" \
  -d '{
  "amount_minor": 10000,
  "currency": "USD",
  "description": "Advanced course",
  "reference": "order-8891",
  "success_url": "https://shop.example/payment/success",
  "cancel_url": "https://shop.example/payment/cancel"
}'
Example response
{
  "data": {
    "id": "MR-0440EAF284",
    "reference": "order-8891",
    "status": "open",
    "amount_minor": 42100,
    "currency": "USD",
    "description": "Advanced course",
    "checkout_url": "https://mirrapaysolution.com/pay/abc123",
    "payment_id": null,
    "gross_amount": 42100,
    "fee_amount": null,
    "additional_fee_amount": null,
    "net_amount": null,
    "payment_status": "open",
    "settlement_status": "not_settled",
    "fee_bps": 900,
    "created_at": "2026-09-06T10:12:04.881Z",
    "updated_at": "2026-09-06T10:12:04.881Z",
    "livemode": true
  },
  "request_id": "8f2a…"
}

Response fields

FieldTypeMeaning
data.idOptionalstringThe payment's permanent public identifier (MR-…). Store it: the same value is used for every read, in every state.
data.referenceOptionalstring | nullYour own reference exactly as sent, returned immediately and on every later read. Null when you did not send one.
data.statusOptionalstringLifecycle state: open, pending, requires_action, succeeded, failed, cancelled, expired, refunded, partially_refunded, disputed.
data.amount_minorOptionalintegerAmount requested, in minor units.
data.checkout_urlOptionalstring (url)Hosted checkout to redirect the customer to.
data.gross_amountOptionalintegerAmount requested, in minor units.
data.currencyOptionalstringISO 4217 code.
data.payment_statusOptionalstringSame value as status, kept for compatibility.
data.net_amountOptionalinteger | nullNull until the payment succeeds; then the amount credited to you.
data.fee_bpsOptionalintegerApplicable rate in basis points, resolved from your pricing.
data.created_atOptionalstring (ISO 8601)Creation timestamp.
data.updated_atOptionalstring (ISO 8601)Last time this payment's state changed.
data.livemodeOptionalbooleanTrue when the key used is a live credential.
request_idOptionalstringUnique identifier for this call.

Errors

HTTPCodeMeaning
400invalid_jsonThe body could not be parsed as JSON.
422invalid_requestA field is missing or out of range.
422amount_below_minimumAmount is below the minimum for your account.
422amount_over_capAmount exceeds the ceiling for your account.
422invalid_success_urlsuccess_url is not an absolute https URL, or is too long.
422invalid_cancel_urlcancel_url is not an absolute https URL, or is too long.
422redirect_origin_not_registeredThe URL's website address is not registered for this environment. Add it under Developers → Checkout return URLs.
401invalid_api_keyMissing, malformed or unrecognised key.
401api_key_revokedThe key was revoked.
401api_key_mode_mismatchThe key was presented against the wrong environment.
403insufficient_scopeThe key lacks the required scope.
403sub_account_inactiveThe account is not active.
403live_mode_not_enabledA live key was used while the account is not currently approved to accept live payments. Test keys are unaffected.
409idempotency_key_reuseThe same Idempotency-Key was replayed with a different request body.
409idempotency_in_progressA request with the same Idempotency-Key is still being processed. Retry shortly.
429rate_limitedRate limit exceeded. Retry with backoff.
500internal_errorUnexpected failure. Retry and quote request_id.
GEThttps://mirrapaysolution.com/api/public/v1/payments

List payments

Returns your payments, newest first, for the environment of the key used — every state, including payments not yet paid, cancelled, expired and failed ones. Only payments belonging to the key's own account are ever returned.

Required scope: payments:read

Query parameters

FieldTypeMeaning
limitOptionalinteger (1–100)Maximum rows to return. Defaults to 25.
offsetOptionalinteger (≥0)Rows to skip for paging. Defaults to 0.
statusOptionalstringOptional filter: open, pending, requires_action, succeeded, failed, cancelled, expired, refunded, partially_refunded or disputed.

Request

curl "https://mirrapaysolution.com/api/public/v1/payments?limit=25" \
  -H "Authorization: Bearer YOUR_API_KEY"
Example response
{
  "data": [
    {
      "id": "MR-0440EAF284",
      "reference": "order-8891",
      "status": "succeeded",
      "amount_minor": 42100,
      "description": "Advanced course",
      "currency": "USD",
      "checkout_url": "https://mirrapaysolution.com/pay/abc123",
      "gross_amount": 42100,
      "fee_amount": 3789,
      "additional_fee_amount": 0,
      "net_amount": 38311,
      "payment_status": "succeeded",
      "settlement_status": "pending",
      "fee_bps": 900,
      "created_at": "2026-09-06T10:12:04.881Z",
      "updated_at": "2026-09-06T10:13:22.104Z",
      "succeeded_at": "2026-09-06T10:13:22.104Z",
      "livemode": true
    }
  ],
  "has_more": false,
  "request_id": "8f2a…"
}

Response fields

FieldTypeMeaning
data[].idOptionalstringThe payment's permanent public identifier (MR-…).
data[].referenceOptionalstring | nullYour own reference.
data[].statusOptionalstringLifecycle state of the payment.
data[].amount_minorOptionalintegerAmount requested, in minor units.
data[].currencyOptionalstringISO 4217 code.
data[].gross_amountOptionalintegerAmount the customer paid, in minor units.
data[].fee_amountOptionalinteger | nullMIRRA processing fee withheld. Null before any payment attempt.
data[].additional_fee_amountOptionalinteger | nullAdditional partner fee withheld when one applies, otherwise 0.
data[].net_amountOptionalinteger | nullFinal amount credited to your balance, in minor units. Zero for a payment that was never captured.
data[].payment_statusOptionalstringSame value as status, kept for compatibility.
data[].settlement_statusOptionalstringSettlement state of the credited funds.
data[].succeeded_atOptionalstring | nullSet once the payment succeeded.
has_moreOptionalbooleanTrue when another page may exist at the next offset.
request_idOptionalstringUnique identifier for this call.

Errors

HTTPCodeMeaning
422invalid_status_filterThe status value is not one of the documented lifecycle states.
401invalid_api_keyMissing, malformed or unrecognised key.
401api_key_revokedThe key was revoked.
401api_key_mode_mismatchThe key was presented against the wrong environment.
403insufficient_scopeThe key lacks the required scope.
403sub_account_inactiveThe account is not active.
403live_mode_not_enabledA live key was used while the account is not currently approved to accept live payments. Test keys are unaffected.
409idempotency_key_reuseThe same Idempotency-Key was replayed with a different request body.
409idempotency_in_progressA request with the same Idempotency-Key is still being processed. Retry shortly.
429rate_limitedRate limit exceeded. Retry with backoff.
500internal_errorUnexpected failure. Retry and quote request_id.
GEThttps://mirrapaysolution.com/api/public/v1/payments/{id}

Retrieve a payment

Returns one payment by the MR-… identifier returned at creation. The record stays retrievable for its whole life — before payment, after cancellation, after expiry, after failure and after success — and always carries your own reference. Amounts are populated once the customer has genuinely paid; a payment that was never captured credits zero rather than an estimate.

Required scope: payments:read

Request

curl "https://mirrapaysolution.com/api/public/v1/payments/MR-10241" \
  -H "Authorization: Bearer YOUR_API_KEY"
Example response
{
  "data": {
    "id": "MR-0440EAF284",
    "reference": "order-8891",
    "status": "succeeded",
    "amount_minor": 42100,
    "description": "Advanced course",
    "currency": "USD",
    "checkout_url": "https://mirrapaysolution.com/pay/abc123",
    "payment_id": "3f1c…",
    "gross_amount": 42100,
    "fee_amount": 3789,
    "additional_fee_amount": 0,
    "net_amount": 38311,
    "payment_status": "succeeded",
    "settlement_status": "pending",
    "created_at": "2026-09-06T10:12:04.881Z",
    "updated_at": "2026-09-06T10:13:22.104Z",
    "succeeded_at": "2026-09-06T10:13:22.104Z",
    "livemode": true
  },
  "request_id": "8f2a…"
}

Response fields

FieldTypeMeaning
data.idOptionalstringThe identifier you were given at creation.
data.referenceOptionalstring | nullYour own reference, exactly as sent at creation.
data.statusOptionalstringLifecycle state of the payment.
data.amount_minorOptionalintegerAmount requested, in minor units.
data.payment_idOptionalstring | nullSet once a payment exists for the checkout. For support reconciliation only.
data.gross_amountOptionalintegerAmount the customer paid, in minor units.
data.fee_amountOptionalinteger | nullMIRRA processing fee withheld. Null before any payment attempt.
data.additional_fee_amountOptionalinteger | nullAdditional partner fee when one applies. Null before any payment attempt.
data.net_amountOptionalinteger | nullFinal amount credited to your balance. Null before any attempt, zero when a payment was attempted but never captured.
data.payment_statusOptionalstringSame value as status, kept for compatibility.
data.settlement_statusOptionalstringSettlement state of the credited funds.
data.updated_atOptionalstring (ISO 8601)Last time this payment's state changed.
request_idOptionalstringUnique identifier for this call.

Errors

HTTPCodeMeaning
404not_foundNo payment with that identifier on your account.
401invalid_api_keyMissing, malformed or unrecognised key.
401api_key_revokedThe key was revoked.
401api_key_mode_mismatchThe key was presented against the wrong environment.
403insufficient_scopeThe key lacks the required scope.
403sub_account_inactiveThe account is not active.
403live_mode_not_enabledA live key was used while the account is not currently approved to accept live payments. Test keys are unaffected.
409idempotency_key_reuseThe same Idempotency-Key was replayed with a different request body.
409idempotency_in_progressA request with the same Idempotency-Key is still being processed. Retry shortly.
429rate_limitedRate limit exceeded. Retry with backoff.
500internal_errorUnexpected failure. Retry and quote request_id.

Developers

Security requirements

  • Call the API from your server only. A secret key in browser code, a mobile app or a public repository is compromised the moment it ships.
  • Keep secrets in server environment variables. Never in HTML, JavaScript bundles, analytics events or browser storage.
  • Use test credentials while you build and switch to live credentials only when you are ready to take real money.
  • Rotate a key by creating a new one, deploying it, then revoking the old one. Revocation takes effect immediately.
  • Treat every amount as authoritative only after MIRRA reports the payment as succeeded — never after a browser redirect alone.
  • Log the request_id of failed calls. It is the fastest route to a support answer.
  • Send requests over HTTPS only.

Developers

Not available yet

These are deliberately not documented as working features. If your integration needs one, tell support so it can be prioritised.

Not available yet

Cancel a payment

Cancellation is available in the dashboard only. No API endpoint is published.

Not available yet

Refunds

Refunds are handled by MIRRA operations. No API endpoint is published.

Not available yet

Official SDKs

There is no published MIRRA SDK package. Use the HTTP API directly with your language's standard HTTP client.

Not available yet

API status monitoring

No public monitoring feed is connected, so no uptime figure is shown.

Developers

Getting help

Include the request_id from the failing response, the endpoint, and the time of the call. Write to support@mirrapaysolution.com.

Getting started

Ready to get paid?

Set up your MIRRA Pay account, complete verification and start accepting international payments.

Set up your account · Complete verification · Start receiving payments