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. Create a test API key and keep it on your server.
- 2. Call POST /api/v1/payments to charge.
- 3. Or create a payment link and send the customer to the hosted checkout.
- 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.
Authorization: Bearer fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c
# or
X-API-Key: fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6cA 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
| Method | Path | Scope | Purpose |
|---|---|---|---|
| POST | /api/v1/payments | payments:write | Create a payment |
| GET | /api/v1/payments | payments:read | List payments (status, payment_method, limit) |
| GET | /api/v1/payments/{id} | payments:read | Retrieve one payment |
| POST | /api/v1/payments/{id}/refund | refunds:write | Full refund |
| POST | /api/v1/payment-links | links:write | Create a hosted checkout link |
| GET | /api/v1/payment-links | links:read | List payment links |
| GET | /api/v1/transactions | payments:read | List transactions |
| GET | /api/v1/webhooks | webhooks:read | List webhook endpoints and events |
| GET | /api/v1/me | any valid key | Merchant profile, register settings and capabilities |
| GET | /api/v1/products | catalog:read | List the catalog |
| POST | /api/v1/products | catalog:write | Create or update a listing |
| GET | /api/v1/customers | customers:read | List customers |
| GET | /api/v1/invoices | invoices:read | List invoices (status, limit) |
| POST | /api/v1/invoices | invoices:write | Create an invoice or draft |
| GET | /api/v1/invoices/{id} | invoices:read | Retrieve an invoice with its payments |
| POST | /api/v1/invoices/{id}/payments | invoices:write | Take a full, partial or split payment |
| POST | /api/v1/register-sales | register:write | Ring up a complete register sale |
| GET | /api/v1/receipts/{code} | payments:read | Retrieve one receipt |
| GET | /api/v1/employees | employees:read | List team members and permissions |
| GET | /api/v1/reports | reports:read | Sales 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 -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 https://fmmerchant.com/api/v1/payments/PAYMENT_ID \
-H "Authorization: Bearer fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c"GET /api/v1/payments?status=succeeded&payment_method=visa_mastercard&limit=25Refunds
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 -X POST https://fmmerchant.com/api/v1/payments/PAYMENT_ID/refund \
-H "Authorization: Bearer fm_test_ab12cd34.9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c"Payment links
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 -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.
<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.
<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.
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">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": {
"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 "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.
