Proudly Canadian. Connected to the world.
MMMAXMERITExchange

MaxMerit Exchange API

One API for fiat-to-crypto, crypto-to-fiat and crypto-to-crypto exchanges settled by a Toronto money services business, plus an embeddable widget, a hosted checkout and custody accounts for approved partners. The endpoints, field names and statuses mirror the Guardarian partner API, so an integration written for one works against the other. Access is by application: sandbox keys are issued once a partner is approved, live keys after onboarding.

Base URL

https://maxmeritexchange.com

Format

JSON, snake_case

Auth

x-api-key header

Quickstart

Get a quote, create a transaction, and send your customer to the hosted checkout. Three calls, no SDK required.

1. Get an estimate

bash
curl "https://maxmeritexchange.com/api/v1/estimate?from_currency=CAD&to_currency=BTC&from_amount=300&payment_method=interac" \
  -H "x-api-key: mm_test_sk_demo_0000000000000000000000000000"

The response is a flat object like Guardarian's: value is what the customer receives, estimated_exchange_rate the net rate, and id lets you lock the price for two minutes.

2. Create the transaction

bash
curl -X POST https://maxmeritexchange.com/api/v1/transaction \
  -H "x-api-key: mm_test_sk_demo_0000000000000000000000000000" \
  -H "content-type: application/json" \
  -d '{
    "estimate_id": "est_8k3mq2p7w9ab",
    "payout_address": "bc1q...",
    "external_partner_link_id": "order_55",
    "redirects_successful": "https://yourapp.com/thanks",
    "customer": { "contact_info": { "email": "alex@example.com" }, "billing_info": { "country_alpha_2": "CA" } }
  }'

3. Redirect, then listen

Send the customer to redirect_url. They pay (or deposit crypto), and every status change is POSTed to your webhook URL. You can also poll GET /api/v1/transactions/:id.

js
const res = await fetch("https://maxmeritexchange.com/api/v1/transaction", {
  method: "POST",
  headers: { "x-api-key": process.env.MM_API_KEY, "content-type": "application/json" },
  body: JSON.stringify({ from_currency: "CAD", to_currency: "ETH", from_amount: 500, deposit_type: "VISA_MC", payout_address: "0x..." }),
});
const tx = await res.json();
window.location.href = tx.redirect_url; // hosted checkout

Authentication

Every partner has two keys, both shown in the admin dashboard when the partner is created.

mm_live_sk_…Secret key. Server to server only, sent as x-api-key (or Authorization: Bearer). Unlocks every endpoint, including transaction listing and custody.
mm_live_pk_…Public widget key. Safe to embed in HTML. Used by the hosted widget and checkout (?partner=). Can quote, create, read and cancel its own transactions, nothing else.

Test keys use the mm_test_ prefix. The seeded demo secret key for local development is mm_test_sk_demo_0000000000000000000000000000 and its widget key is mm_test_pk_demo.

Sandbox and live

Partners start in sandbox mode. Sandbox transactions are real API objects with fake money: a buy moves from waiting_for_deposit to exchanging, sending and finished on its own within about a minute, webhooks fire for every step, and custody balances update. Use PATCH /transactions/:id/checkout with {"simulate":"deposit"} to skip the wait. Switching a partner to live in the admin dashboard issues new live keys; sandbox keys keep working for testing.

Guardarian compatibility

MaxMerit runs its own rails and keeps Guardarian as a backup liquidity provider. To make that invisible to partners, the public API is shaped after Guardarian's: the same paths under /api/v1, the same snake_case field names, the same transaction statuses and the same webhook envelope. Point an existing Guardarian integration at https://maxmeritexchange.com/api/v1 with a MaxMerit key and it works; MaxMerit-only features are additive fields you can ignore.

GuardarianMaxMeritNotes
GET /v1/statusGET /api/v1/statusAdds store, rates and custody status.
GET /v1/currenciesGET /api/v1/currenciesSame shape; /currencies/crypto and /currencies/fiat add prices and limits.
GET /v1/payment-categoriesGET /api/v1/payment-categoriesVISA_MC, SEPA, APPLE_PAY ... plus INTERAC, EFT, CASH.
GET /v1/market-info/min-max-range/{from_to}GET /api/v1/market-info/min-max-range/{from_to}Identical.
GET /v1/estimateGET /api/v1/estimateSame params; response adds id (price lock), fees and provider.
POST /v1/transactionPOST /api/v1/transactionSame body; adds estimate_id, payout_type "custody", custody_customer_ref.
GET /v1/transaction/{id}GET /api/v1/transaction/{id}Same fields (payout_address, output_hash, external_partner_link_id ...).
Webhook { payload, version, requesterType }Same envelopeSigned with X-MM-Signature; adds event, event_id, mode.
calculator/v2?default_side=buy_crypto&default_fiat_currency=EUR.../widget?partner=...&default_side=buy_crypto&default_fiat_currency=CAD...Guardarian calculator parameters are accepted as aliases.

Routing

Settings → Routing chooses the rails: maxmerit (ours only), guardarian (everything upstream) or auto (ours whenever the pair is enabled here, Guardarian as fallback). A Guardarian-routed transaction carries provider: "guardarian" and provider_id (the upstream id), its redirect_url is Guardarian's checkout, and its status is mirrored from Guardarian on every read. Set GUARDARIAN_API_KEY to enable it; without a key, auto routing stays on MaxMerit rails.

Embeddable widget

The hosted widget is the same component that runs on our homepage. One script tag, no build step, resizes itself inside your page.

html
<div id="maxmerit-widget"></div>
<script src="https://maxmeritexchange.com/embed.js"></script>
<script>
  MaxMeritWidget.mount({
    container: "#maxmerit-widget",
    partner: "mm_live_pk_yourkey",   // public widget key
    side: "buy",                      // buy | sell | swap
    fiat: "CAD", crypto: "BTC", amount: 300,
    theme: "light",                   // light | dark
    accent: "gold",                   // gold | navy | blue | green
    directions: "buy,sell",           // optional: hide tabs
    onFinished: (e) => console.log("done", e.transactionId),
  });
</script>

Prefer a plain link or your own iframe? Every option is a query parameter on /widget: /widget?partner=mm_live_pk_yourkey&side=buy&fiat=CAD&crypto=BTC&amount=300&theme=dark&accent=green. Try it live on the configurator.

Events

The iframe posts messages to the parent window: mm:ready, mm:resize (height), mm:checkout_started (estimateId) and mm:finished (transactionId, status). The embed script forwards them to your onEvent callback.

Hosted checkout

Transactions created through the API return a redirect_url on our domain. The checkout collects whatever you did not provide (wallet address or custody choice, email, country), shows payment or deposit instructions, and tracks status live. When the customer finishes we send them to your success_url if you set one. Everything the checkout does is available to you directly through PATCH /api/v1/transactions/:id/checkout if you prefer to build your own screens.

Transaction lifecycle

newCreated
waiting_for_customerWaiting for customer
waiting_for_depositWaiting for deposit
on_holdOn hold (review)
exchangingExchanging
sendingSending
finishedFinished
failedFailed
refundedRefunded
expiredExpired
cancelledCancelled

Buy: waiting_for_deposit → exchanging → sending → finished. Sell and swap are the same once the crypto deposit is detected. waiting_for_customer means the checkout still needs a wallet address. on_hold means an identity check or manual review is pending. expired is set automatically when nothing arrives within an hour of the quote.

API reference

Click an endpoint for parameters and examples. The machine-readable version is at /api/v1/openapi.

Catalog

Currencies, networks, payment methods and countries.

Pricing

Live rates, limits and binding estimates.

Transactions

Create exchanges, send customers to the hosted checkout and track status.

Custody

NakoPay custody accounts and withdrawals.

Webhooks

Signed notifications from MaxMerit to you, and from NakoPay to MaxMerit.

Webhooks

Set a webhook URL on your partner profile and we POST JSON for every event. Verify the signature before trusting the payload.

http
POST /your/webhook HTTP/1.1
Content-Type: application/json
X-MM-Event: transaction.status_changed
X-MM-Delivery: whd_7hq2m9kd3pxz
X-MM-Signature: t=1760200000,v1=5f1c…e9a0

{
  "id": "evt_…", "type": "transaction.status_changed", "created_at": "…", "mode": "live",
  "data": { "transaction": { "id": "tx_…", "status": "finished", "external_id": "order_55", … }, "previous_status": "sending" }
}
js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5 min tolerance
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Events: transaction.created, transaction.status_changed, transaction.finished, transaction.failed, custody.deposit_detected, custody.payout_sent. Respond with any 2xx within 10 seconds. Failed deliveries retry 5 times with exponential backoff and can be replayed from the admin dashboard.

Custody accounts

By default crypto is sent to the customer's own wallet. Partners approved for custody can pass payout_type: "custody" and the crypto is delivered into a custody account operated by the exchange's custody provider instead. Accounts are keyed by your custody_customer_ref (one per customer, asset and network), so a returning customer keeps accumulating in the same account. Balances, deposit addresses and withdrawals are available through the Custody endpoints, and the custody provider notifies the exchange of on-chain deposits and completed payouts through a signed webhook.

bash
# 1. Buy into custody
curl -X POST https://maxmeritexchange.com/api/v1/transactions -H "x-api-key: $KEY" -H "content-type: application/json" \
  -d '{ "from_currency":"CAD","to_currency":"USDT","to_network":"TRX","from_amount":500,"payment_method":"interac",
        "payout_type":"custody","custody_customer_ref":"user_1001","customer":{"email":"alex@example.com","country":"CA"} }'

# 2. Check the balance
curl https://maxmeritexchange.com/api/v1/custody/accounts?customer_ref=user_1001 -H "x-api-key: $KEY"

# 3. Withdraw to any address
curl -X POST https://maxmeritexchange.com/api/v1/custody/accounts/cust_…/payouts -H "x-api-key: $KEY" -H "content-type: application/json" \
  -d '{ "address":"TXyz…", "amount": 250 }'

What the custody provider implements

The exchange talks to its custody provider through a small contract documented in docs/CUSTODY-NAKOPAY.md in the repository: create account, read balance, request payout, and a webhook back to /api/v1/webhooks/nakopay signed with the shared secret. Until the provider credentials are configured, custody runs in sandbox with simulated addresses and balances, so partner integrations can be built and tested end to end today.

Errors and limits

json
{ "error": { "code": "amount_below_minimum", "message": "Minimum is 30 CAD.", "details": { "min": 30, "max": 25000, "currency": "CAD" } } }
400invalid_json, invalid_request
401unauthorized, invalid_api_key, invalid_widget_key, invalid_signature
403partner_suspended, country_restricted, custody_disabled
404transaction_not_found, estimate_not_found, custody_account_not_found
409invalid_transition, transaction_closed, custody_account_frozen
422validation_error, unsupported_currency, unsupported_pair, unsupported_network, unsupported_payment_method, amount_below_minimum, amount_above_maximum, invalid_address, insufficient_balance
429rate_limited: 600 requests/minute per secret key, 120 per widget key per IP, 120 per IP unauthenticated
5xxinternal_error, custody_error, custody_unavailable

Quotes are valid for 2 minutes. Online limits default to 30 to 25,000 CAD per transaction, with identity verification above 1,000 CAD. Partners can request higher limits.

Admin dashboard

Everything above is operated from /admin: live transactions with status overrides and KYC decisions, partners and API keys, coins, networks, fiat currencies and payment methods, fees and spread, live rate feeds, custody accounts and payouts, webhook deliveries with replay, site content, and a full audit log. Sign in with the credentials from ADMIN_EMAIL / ADMIN_PASSWORD.