Mobile app API
One backend for the website and the Android app
The Android app uses this same merchant identity, catalog, invoices and ledger — no second database, no second account system, no separate balance to reconcile. Sandbox only until live processing is approved.
Authentication
The Android app signs in. It does not embed a key.
A distributed app must never ship a permanent merchant API key — anyone who unpacks it would have that merchant's account. Instead the person signs in on the phone through the platform login (email and password, or a social provider, plus a verification code). The sign-in returns a short-lived session token, and every call sends it as Authorization: Bearer <session token>. The server resolves the merchant from that session — never from the request body — so a phone can only ever reach its own account.
A session expires and can be revoked by signing out; when a call answers 401 the app shows the sign-in screen and gets a new token. An owner's session carries the owner's permissions. An employee's session carries only the permissions on their team row, translated to the scopes below — a cashier can ring up a sale but cannot read financial reports or issue a refund.
Merchant developer API keys still exist for servers, scripts and integrations: Authorization: Bearer fm_test_<key_id>.<secret> or X-API-Key. Keys are created, scoped, rotated and revoked in the developer dashboard, and the two credential types are independent: revoking a leaked key never signs a person out, and signing out a phone never breaks an integration.
Store the session token in the Android Keystore, refresh it only through sign-in, and never log it. Guest customers never receive any credential.
Endpoints
| Method | Path | Scope | Returns |
|---|---|---|---|
| GET | /api/v1 | public | API root: version, auth methods and every endpoint |
| GET | /api/v1/me | session or key | Merchant profile, register settings, capabilities, current actor |
| GET | /api/v1/products | catalog:read | List the catalog (?active=true, ?limit=) |
| POST | /api/v1/products | catalog:write | Create or update a listing (send id to update) |
| GET | /api/v1/customers | customers:read | Customers built from payments (?search=) |
| GET | /api/v1/invoices | invoices:read | Invoices (?status=open|paid|draft|void|all) |
| POST | /api/v1/invoices | invoices:write | Create an invoice or a draft |
| GET | /api/v1/invoices/{id} | invoices:read | One 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 | The receipt for one payment |
| GET | /api/v1/employees | employees:read | Team members, roles and permission keys |
| GET | /api/v1/reports | reports:read | Sales summary (?period=today|week|month) |
| GET | /api/v1/payments | payments:read | List payments |
| POST | /api/v1/payments | payments:write | Charge a payment |
| GET | /api/v1/payments/{id} | payments:read | One payment's status |
| POST | /api/v1/payments/{id}/refund | refunds:write | Refund a payment |
| GET | /api/v1/payment-links | links:read | Hosted checkout links |
| POST | /api/v1/payment-links | links:write | Create a hosted checkout link |
| GET | /api/v1/identity | identity:read | The merchant's linked FlowMoney identity and service state |
| POST | /api/v1/identity | identity:write | Link an existing handle, claim a new one, or disconnect |
| GET | /api/v1/storefront | storefront:read | The owner's business page, products and share URL |
| POST | /api/v1/storefront | storefront:write | Publish or edit the business page (owner only) |
| GET | /api/v1/transactions | payments:read | Transaction history (?limit=) |
| GET | /api/v1/directory | public | Public business directory (?q=&category=) |
| GET | /api/v1/webhooks | webhooks:read | Webhook endpoints and recent events |
| GET | /api/v1/handoff | public | The Android handoff document, as markdown |
All responses are JSON with mode: "test" and the header X-FM-Mode: test until live processing is approved. Amounts are numbers in the invoice currency (USD), never strings.
Session bootstrap, and a complete sale
GET https://fmmerchant.com/api/v1/me
Authorization: Bearer fm_test_<key_id>.<secret>
200 OK
{
"object": "merchant",
"mode": "test",
"merchant": { "account_id": "FM-8K2M4Q", "display_name": "Aurora Cuts", "handle": "aurora", ... },
"settings": { "tax_rate_percent": 8.25, "tip_enabled": true, ... },
"capabilities": { "register": true, "invoices": true, "card_hardware": false }
}Load /me once at sign-in: it returns the account, the register settings the app must honour (tax rate, tip on/off, discount cap, receipt footer) and which capabilities this account has. The app renders from that response instead of hard-coding tax or branding.
POST https://fmmerchant.com/api/v1/register-sales
Authorization: Bearer fm_test_<key_id>.<secret>
Idempotency-Key: sale-7f3c19
{
"items": [{ "product_id": "67a1…", "name": "Signature cut", "qty": 1, "unit_price": 45 }],
"tip_amount": 5,
"tax_exempt": false,
"payment_method": "visa_mastercard",
"customer_email": "guest@example.com"
}
201 Created
{
"object": "register_sale",
"invoice": { "invoice_number": "AURORA-2026-0042", "total": 53.71, "status": "paid" },
"transaction": { "id": "…", "status": "succeeded", "amount": 53.71 },
"receipt_code": "RCPT-8HK2",
"mode": "test"
}A register sale is one call because the invoice and its payment belong together. The server prices the sale from stored settings, allocates the next invoice number, charges the total and returns the receipt code. The client's own totals are ignored — never displayed as authoritative.
Synchronisation and duplicate protection
The backend is the only ledger. The app keeps a read cache for offline display but never a second balance, and never computes a sale total locally.
- Every write that moves money requires an
Idempotency-Keyof at least 8 characters. Repeating it returns the original payment withidempotent_replay: true— no second charge, no second ledger entry. - Split payments are applied to the invoice balance; an amount above the outstanding balance is refused with 400.
- Refunds reverse the original ledger entry and mark the payment refunded. Refund authority comes from the key's scope and is audited.
- Subscribe the app to signed webhooks for settlement and dispute changes, then re-read
/api/v1/payments/{id}. Polling/reportsis for dashboards, never for payment confirmation. - Employee permissions are enforced server-side on every call; a suspended employee's key stops working immediately.
Errors and limits
- 401 missing, malformed, revoked or mismatched key.
- 403 the key's scopes or the account's status do not permit that action.
- 404 the record exists but not for this merchant — always a 404, never a hint that it exists elsewhere.
- 429 60 requests per minute per key, fixed window.
- Errors are shaped
{ error: { type, message } }and every request is visible in the dashboard's API log.
Not available to the app yet
Live card processing, Tap to Pay and the Cube reader all wait on provider approval and certified hardware. Until then every charged amount is a sandbox authorisation and every response says so. Staff sign in with their own session and can only do what their team permissions allow; a stolen phone session is revocable by signing it out.
