Developer API

Add FM Merchant to your site

A sandbox-first payments API with hosted checkout, embedded checkout, payment buttons, QR codes and signed webhooks. Get a test key in under a minute — no card details are ever collected by your code.

Getting started

Create a free merchant account, open Dashboard → Developers → Credentials and copy a test key. Every example below works against the sandbox: payments succeed without a card, and no funds move.

  1. 1. Create a test API key and keep it on your server.
  2. 2. Call POST /api/v1/payments to charge.
  3. 3. Or create a payment link and send the customer to the hosted checkout.
  4. 4. Subscribe a webhook so your system hears about payments, refunds and payouts.

Authentication

Send the key as a bearer token. X-API-Key is accepted for clients that cannot set an Authorization header.

Header
Authorization: Bearer fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c
# or
X-API-Key: fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c

A key carries permission scopes: payments:read, payments:write, links:read, links:write, refunds:write and webhooks:read. A key created with no scopes has full access. Requests are limited to 60 per minute per key.

Endpoints

MethodPathScopePurpose
POST/api/v1/paymentspayments:writeCreate a payment
GET/api/v1/paymentspayments:readList payments (status, payment_method, limit)
GET/api/v1/payments/{id}payments:readRetrieve one payment
POST/api/v1/payments/{id}/refundrefunds:writeFull refund
POST/api/v1/payment-linkslinks:writeCreate a hosted checkout link
GET/api/v1/payment-linkslinks:readList payment links
GET/api/v1/transactionspayments:readList transactions
GET/api/v1/webhookswebhooks:readList webhook endpoints and events
GET/api/v1/meany valid keyMerchant profile, register settings and capabilities
GET/api/v1/productscatalog:readList the catalog
POST/api/v1/productscatalog:writeCreate or update a listing
GET/api/v1/customerscustomers:readList customers
GET/api/v1/invoicesinvoices:readList invoices (status, limit)
POST/api/v1/invoicesinvoices:writeCreate an invoice or draft
GET/api/v1/invoices/{id}invoices:readRetrieve an invoice with its payments
POST/api/v1/invoices/{id}/paymentsinvoices:writeTake a full, partial or split payment
POST/api/v1/register-salesregister:writeRing up a complete register sale
GET/api/v1/receipts/{code}payments:readRetrieve one receipt
GET/api/v1/employeesemployees:readList team members and permissions
GET/api/v1/reportsreports:readSales summary (period)

All paths are relative to https://fmmerchant.com.

Creating payments

Amount, currency and method are the only required decisions; fees are computed from the routed provider and returned with the payment. Send an Idempotency-Key header so a retry can never double charge.

cURL
curl -X POST https://fmmerchant.com/api/v1/payments \
  -H "Authorization: Bearer fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c" \
  -H "Idempotency-Key: order-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 49.00,
    "currency": "USD",
    "payment_method": "visa_mastercard",
    "description": "Signature cut and style",
    "customer_name": "Alex Morgan"
  }'

Payment methods: visa_mastercard, debit, flowmoney, crypto, lightning.

Payment status

Fetch one payment by id, or list payments with optional filters.

cURL
curl https://fmmerchant.com/api/v1/payments/PAYMENT_ID \
  -H "Authorization: Bearer fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c"
List payments
GET /api/v1/payments?status=succeeded&payment_method=visa_mastercard&limit=25

Refunds

Refunds are full-value and simulated until live processing is approved. A refund reverses the original ledger entry and its fee, so your available balance returns to where it was before the payment.

cURL
curl -X POST https://fmmerchant.com/api/v1/payments/PAYMENT_ID/refund \
  -H "Authorization: Bearer fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c"

Create a link for a product or service and share its checkout URL. Links can be paused, given an expiry, and are the basis for buttons, embeds and QR codes.

cURL
curl -X POST https://fmmerchant.com/api/v1/payment-links \
  -H "Authorization: Bearer fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c" \
  -H "Content-Type: application/json" \
  -d '{
    "product_name": "Signature cut and style",
    "amount": 45.00,
    "description": "45 minute appointment",
    "reference": "SVC-001"
  }'

Hosted checkout

Send the customer to https://fmmerchant.com/pay/<code>. The page shows your logo, name, accent colour and support details, the product, amount, currency, payment methods, an order summary and a receipt on success.

Embedded checkout

Add ?embed=1 and place the checkout in an iframe.

HTML
<iframe
  src="https://fmmerchant.com/pay/PAYMENT_LINK_CODE?embed=1"
  title="Checkout"
  style="width:100%;max-width:440px;height:720px;border:0;border-radius:16px"
  allow="payment"
></iframe>

Payment buttons

Any link can become a styled button — no JavaScript required.

HTML
<a href="https://fmmerchant.com/pay/PAYMENT_LINK_CODE" target="_blank" rel="noopener"
   style="display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:0 28px;border-radius:999px;background:#00D1FF;color:#04121A;font-weight:700;font-family:system-ui,sans-serif;text-decoration:none">
  Pay $45.00
</a>

QR payments

A QR code is just the hosted checkout URL encoded — no separate API. Generate one for any link from Dashboard → Developers → Integrate, where you can copy the URL and print the code for market stalls, tables or invoices.

Webhooks

Register an https endpoint and choose events. Each delivery carries X-FM-Event, X-FM-Delivery, X-FM-Timestamp and X-FM-Signature.

Events: payment.created, payment.succeeded, payment.failed, payment.settled, payment.refunded, payout.created, payout.completed, payout.failed, merchant.created, merchant.verified.

Signature header
X-FM-Event: payment.succeeded
X-FM-Delivery: <stable delivery id>
X-FM-Timestamp: 1731000000
X-FM-Signature: t=1731000000,v1=<hex hmac-sha256 of "t.rawBody">
Node.js
import crypto from "node:crypto";

// req.body must be the RAW request body, before JSON parsing.
export function verifyFmSignature({ secret, header, rawBody, toleranceSeconds = 300 }) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(parts.t + "." + rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Reject timestamps older than five minutes, compare in constant time, and treat repeat delivery ids as duplicates. Failed deliveries can be retried from the dashboard.

Error handling

Error body
{
  "error": {
    "type": "invalid_request_error",
    "message": "Amount 999999 USD is outside the limits for visa_mastercard (min 0.5, max 50000)"
  }
}
  • 400 — invalid request (amount, method, body)
  • 401 — missing, wrong-mode or revoked key
  • 403 — scope or account restriction (verification)
  • 404 — object not found on your account
  • 409 — already refunded or conflicting state
  • 429 — rate limit (60 requests/minute per key)

Sandbox testing

Sandbox payments always succeed without card details — FM Merchant never asks your code to collect a card number. Test the failure paths with an amount outside the method limits, a revoked key, or a rejected merchant account. Every response includes mode: "test".

cURL
curl "https://fmmerchant.com/api/v1/transactions?limit=25" \
  -H "Authorization: Bearer fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c"

Money and settlement

Every payment posts a balanced double-entry journal to the FlowMoney ledger: the gross amount is captured, the platform fee is separated, and the net moves to your pending balance. Settlement moves it to available; refunds reverse both the net and the fee. FlowMoney is the single ledger — FM Merchant keeps no second balance system.

Live money movement stays disabled until provider approval and production credentials are in place. Test keys keep working either way.