Mooov Docs

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

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)

  1. You generate a signed state token 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:configure
    

    redirect_uri must 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.

  2. 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.

  3. Mooov mints a merchant_grants row and redirects to:

    <redirect_uri>?code=<auth_code>&state=<your state>
    

    auth_code is single-use and expires in 10 minutes.

  4. You verify state against 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:

  1. Email ops@mooov.money with: your product name, redirect URI allowlist (one per environment), webhook URL per environment, and your security contact.
  2. Mooov operator creates a platform_integrators row and mints a sandbox platform key. You receive the secret via the secret-handoff runbook (1Password share, 24h TTL).
  3. 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.