Mooov Connect protocol
Audience: integrators (platforms) who want to embed Mooov payments in their product and offer payments to many tenants under a single credential, like Stripe Connect.
This document is the integrator-facing surface of Mooov Connect. If you're a merchant integrating Mooov directly for your own business, you don't need this page — use a per-merchant API key instead.
1. What you get
- One platform API key for your whole product. You hold one credential and act on behalf of any tenant who has connected to you.
- A hosted authorization flow (Stripe-Connect-style) so each tenant connects with one click. The connector minted on your behalf appears in our merchant database as a real, addressable merchant.
- A signed webhook stream that fans out events for every connected tenant to a single URL on your side. No per-tenant webhook routing.
- An operator-grade revocation surface on our side. If your key
leaks we revoke the right grants from
ops.mooov.money/platformsand your customers' funds stay safe.
You don't get raw card data, raw bank account numbers, or PSP-level credentials. Those live on Mooov's side and rotate per-merchant.
2. Endpoints, at a glance
Production https://api.mooov.money
Sandbox https://staging.api.mooov.money (Mooov-Test-Mode)
Authorize https://connect.mooov.money/authorize
Every API call uses two headers:
Authorization: Bearer <platform key secret>
X-Mooov-Key-Id: mk_platform_<8-hex>
Mooov-Merchant: merch_<id> (required for on-behalf-of calls)
Idempotency-Key: <your uuid> (required on writes)
Mooov-Merchant is the tenant whose books you're touching. Mooov
verifies a merchant_grants row exists for (your platform_id, that merchant_id) and that the requested action is in the granted
scopes. No grant, no operation; the API returns 403 with
GRANT_NOT_FOUND.
3. Bring a new tenant online (the consent flow)
You generate a signed
statetoken and send the tenant to:https://connect.mooov.money/authorize ?client_id=<your platform slug> &redirect_uri=<your callback URL> &state=<your signed CSRF token> &scopes=payments:write,payments:read,customers:write,webhooks:configureredirect_urimust exact-match one entry in the allowlist you registered with Mooov. Path prefixes and query-string matches are rejected on purpose so a misconfigured tenant can't be phished.The tenant signs into Mooov (or creates an account), sees a consent screen showing your display name and the scopes you asked for, and clicks Connect.
Mooov mints a
merchant_grantsrow and redirects to:<redirect_uri>?code=<auth_code>&state=<your state>auth_codeis single-use and expires in 10 minutes.You verify
stateagainst your CSRF token, then exchange the code at the gateway:curl -X POST https://api.mooov.money/v1/platform/oauth/token \ -H "Authorization: Bearer <platform key secret>" \ -H "X-Mooov-Key-Id: mk_platform_<8-hex>" \ -H "Content-Type: application/json" \ -d '{ "code": "<auth_code from step 3>", "redirect_uri": "<same redirect_uri you used in step 1>" }'The response is the durable identifier you store on your side:
{ "merchant_id": "merch_lodge_001", "entity_id": "mooov3", "granted_scopes": ["payments:write", "payments:read", "customers:write", "webhooks:configure"], "granted_at": "2026-05-18T12:00:00.000Z" }
That merchant_id is what you put in Mooov-Merchant on every
subsequent call you make on this tenant's behalf.
4. Act on a tenant's behalf
Create a payment
curl -X POST https://api.mooov.money/v1/payment_intents \
-H "Authorization: Bearer <platform key secret>" \
-H "X-Mooov-Key-Id: mk_platform_<8-hex>" \
-H "Mooov-Merchant: merch_lodge_001" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 4200,
"currency": "GBP",
"payment_method": { "type": "card" },
"customer": { "email": "guest@example.com" }
}'
List the tenant's recent payments
curl https://api.mooov.money/v1/payment_intents?limit=20 \
-H "Authorization: Bearer <platform key secret>" \
-H "X-Mooov-Key-Id: mk_platform_<8-hex>" \
-H "Mooov-Merchant: merch_lodge_001"
Every /v1/* endpoint follows the same shape: same auth headers, same
Mooov-Merchant header, same idempotency rules. The full reference is
at api.mooov.money/v1.
5. Webhooks
You register one endpoint per environment. Mooov delivers a signed
POST for every event on every tenant that's granted you the
webhooks:configure scope.
POST /your/webhook/endpoint
X-Mooov-Signature: t=1747614000,v1=<hex>
X-Mooov-Delivery: 42 (numeric delivery id, dedupe key)
User-Agent: Mooov-Webhooks/1
Content-Type: application/json
Verify the signature
The signed input is <t>.<raw request body> (literal dot between the
timestamp and the body). Compute HMAC-SHA256 with your webhook
signing secret as the key, hex-encode, and compare in constant time:
import hmac, hashlib, time
def verify(body: bytes, header: str, secret: str, tolerance_s: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > tolerance_s:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(body: Buffer, header: string, secret: string, toleranceSec = 300): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const mac = createHmac("sha256", secret);
mac.update(`${parts.t}.`);
mac.update(body);
const expected = Buffer.from(mac.digest("hex"));
const got = Buffer.from(parts.v1);
return expected.length === got.length && timingSafeEqual(expected, got);
}
Idempotency on your side
Mooov retries non-2xx responses with exponential backoff
(1s, 5s, 30s, 5m, 30m, 2h, 12h). Use X-Mooov-Delivery as the
dedupe key. Distinct deliveries for the same business event share
the same event.id field in the body, so it's also safe to dedupe
on event.id if that's easier for you.
Event shape
{
"id": "evt_01HXX...",
"type": "payment.succeeded",
"created": "2026-05-18T12:34:56.000Z",
"merchant": { "id": "merch_lodge_001", "entity_id": "mooov3" },
"data": { /* event-specific payload */ }
}
created is always RFC3339 UTC with exactly three fractional-second
digits and a Z suffix. We never emit nanoseconds, never omit the
fractional part, never use a numeric offset. Integrators can rely on
this for strict ISO parsers, Postgres timestamp(3) columns, and JSON
Schema format: date-time validators.
The exact data shape is documented per event in the API reference.
6. Errors you'll hit
| Status | error.code |
What it means |
|---|---|---|
| 401 | PLATFORM_KEY_INVALID |
Wrong key id or secret. Check X-Mooov-Key-Id matches the bearer. |
| 401 | PLATFORM_SUSPENDED |
Mooov has suspended your integrator. Check ops.mooov.money/platforms/<your slug> for the reason. |
| 403 | GRANT_NOT_FOUND |
No active merchant_grants row for (your platform_id, Mooov-Merchant). Tenant may have revoked. |
| 403 | SCOPE_NOT_GRANTED |
The action you tried isn't in the grant's scopes. Re-prompt the tenant with the wider scope set. |
| 400 | MOOOV_MERCHANT_REQUIRED |
You forgot the Mooov-Merchant header on an on-behalf-of call. |
| 409 | IDEMPOTENCY_KEY_REUSED |
Same Idempotency-Key with a different request body. Pick a new key. |
| 422 | ROUTING_NO_ELIGIBLE_PROVIDER |
The merchant's routing config rejected every PSP for this request. Operator has to update it. |
When a grant disappears mid-flight (the lodge admin clicked Revoke
in their dashboard, or a Mooov operator did), every subsequent on-
behalf-of call for that merchant returns 403 GRANT_NOT_FOUND. The
correct behaviour on your side is to mark the tenant as
disconnected and surface a "reconnect Mooov" CTA in your UI.
7. Sandbox
For local dev and CI, use the sandbox base URL and ask Mooov for a Mooov-Test-Mode platform key:
Base URL https://staging.api.mooov.money
Authorize https://staging.connect.mooov.money/authorize
Sandbox merchants seed automatically when you exchange the auth code,
so a fresh CI run can spin up merch_test_* tenants without manual
seeding on Mooov's side. Sandbox card numbers + simulated webhook
deliveries are at docs.mooov.money/sandbox.
8. Getting credentials
Mooov mints integrator credentials via an offline ceremony — we don't support self-serve platform registration in v1. To get started:
- Email
ops@mooov.moneywith: your product name, redirect URI allowlist (one per environment), webhook URL per environment, and your security contact. - Mooov operator creates a
platform_integratorsrow and mints a sandbox platform key. You receive the secret via the secret-handoff runbook (1Password share, 24h TTL). - You build against sandbox. When you're ready to go live, repeat the ceremony for prod. Sandbox and prod keys are different secrets and they never share grants.
9. Versioning
Mooov's /v1/* API is stable: additive changes only. Breaking
changes get a new prefix (/v2/*) with a 12-month overlap and a
public deprecation calendar. Webhook event shapes follow the same
contract.
Your platform key never expires unless you (or Mooov) revoke it. Rotate it whenever your runbook says to.