Mooov Docs

API reference

Audience: anyone calling the Mooov gateway over HTTPS — platforms on Mooov Connect and merchants with a direct API key. For the consent flow, webhooks deep-dive, and credential ceremony see the Connect protocol; for direct merchant setup see the merchant quickstart.

Production  https://api.mooov.money
Sandbox     https://sandbox.api.mooov.money     (see /sandbox/)

MCP-capable agents and editors can query these endpoint and webhook schemas directly; see Connect your AI agent.

1. Conventions

Authentication

Every /v1/* call carries the HMAC headers below. The signing scheme is identical for merchant keys (mk_test_* / mk_live_*) and platform keys (mk_platform_*):

X-Mooov-Key-Id:    <your key id>
X-Mooov-Timestamp: <RFC3339 UTC>
X-Mooov-Signature: <lowercase hex HMAC-SHA256>
Idempotency-Key:   <your uuid>          (required on writes)
Mooov-Merchant:    merch_<id>           (platform keys only — on-behalf-of)

The signed string is exactly four lines (no trailing newline):

<METHOD>\n<PATH>\n<TIMESTAMP>\n<hex SHA-256 of the raw body, or of "" on GET>

We tolerate ±5 minutes of clock skew. Drop-in signers in Python and TypeScript are in §2 of the Connect protocol.

Scopes

Each key (and each Connect grant) carries a scope list. The wildcard * satisfies any scope.

Scope Gates
payments:write Create / capture / refund / void payments, saved-method charges
payments:read Payment timeline
customers:read Customer lookups

Newly minted merchant keys default to ["payments:write", "payments:read"].

Idempotency

Every write requires an Idempotency-Key header. Replaying the same key with the same body returns the original result; the same key with a different body returns 409. A request still in flight returns 409 request already processing — retry with backoff.

Amounts

All amounts are integer minor units (4200 = £42.00). Currency is an ISO 4217 code, e.g. "GBP".

Payment states

created → processing → authorized → captured → partially_refunded → refunded
                     ↘ failed      ↘ voided

2. Payment intents

POST /v1/payment_intents — create (and usually confirm) a payment

Field Type Required Notes
payment_id string Yes Your stable id, e.g. pay_<uuid>. Used everywhere downstream.
amount int Yes Minor units, > 0
currency string Yes ISO 4217
flow string No ""/"server" (default), "redirect", "embedded"
payment_method string For server flow pm_* or ctoken_*. Required unless using customer_ref + off_session.
success_url string For redirect/embedded Absolute https:// (localhost http:// allowed in sandbox)
cancel_url string For redirect/embedded Same rules
description string No Shown on hosted checkout / statements
customer_email string No Pre-fills hosted checkout
customer_name string No
customer_ref string No Your durable customer handle (platform keys only)
setup_future_usage string No "on_session" or "off_session"; requires customer_ref
metadata object No String→string, max 50 keys; reserved mooov_* keys are dropped
merchant_id string No Platform keys: alternative to the Mooov-Merchant header

Response (all flows share this envelope):

{
  "payment_id": "pay_…",
  "state": "processing",
  "provider": {
    "provider":            "stripe",
    "provider_ref":        "cs_…",
    "status":              "pending",
    "hosted_url":          "https://checkout.stripe.com/c/pay/cs_…",
    "checkout_session_id": "",
    "error":               null
  }
}
"error": {
  "category":         "card_declined",
  "code":             "insufficient_funds",
  "message":          "Your card has insufficient funds.",
  "retryable":        false,
  "provider_code":    "card_declined",
  "provider_message": "…"
}

Both integration shapes are walked through end-to-end in §4 of the Connect protocol.

POST /v1/payment_intents/{payment_id}/capture

Captures a previously authorized payment. Empty body (the empty body is still hashed for the signature and idempotency).

Response: the same envelope as create, with state: "captured". Re-capturing an already-captured payment returns 200 with the current state (idempotent replay). A payment not in authorized returns 409 payment not authorized for capture. If the provider rejects the capture you still get a 200state stays authorized and provider.status / provider.error carry the failure.

POST /v1/payment_intents/{payment_id}/refund

Field Type Required Notes
amount int No Minor units. Omit (or 0) = refund the full remaining amount.
reason string No Stored on the refund for audit

Partial refunds are first-class: refund less than the captured amount and state becomes partially_refunded; keep refunding until remaining_amount is 0 and it becomes refunded.

{
  "payment_id":       "pay_…",
  "state":            "partially_refunded",
  "refunded_amount":  500,
  "refunded_total":   500,
  "remaining_amount": 1500,
  "provider":         { "status": "approved", … }
}
Status When
409 Payment not refundable in its current state / nothing left to refund
422 refund amount exceeds captured amount
502 Provider rejected the refund

POST /v1/payment_intents/{payment_id}/void

Voids an authorized (not yet captured) payment.

Field Type Required
reason string No

Response: { "payment_id": …, "state": "voided", "provider": { … } }. A payment that isn't authorized returns 409 payment not voidable in current state.

GET /v1/payment_intents/{payment_id}/timeline

Returns every provider attempt and published event for the payment, oldest first. Requires payments:read.

{
  "payment_id": "pay_…",
  "timeline": [
    { "kind": "attempt", "operation": "charge",
      "provider": "stripe", "status": "approved",
      "occurred_at": "2026-06-10T12:00:00Z" },
    { "kind": "event", "operation": "payment.captured",
      "status": "published",
      "occurred_at": "2026-06-10T12:00:01Z" }
  ]
}

Entry fields: kind (attempt | event), operation, provider, status, error_code, message, occurred_at. operation on event entries uses internal event names (see the webhook event catalog for the mapping to the names you receive on Connect webhooks).


3. Saved payment methods

POST /v1/charges/saved — charge a saved method

Platform-key only (422 otherwise). Charges a payment method previously saved against one of your customer_refs — the engine behind off-session billing like dues and renewals.

Field Type Required Notes
payment_id string Yes Your stable id for this charge
amount int Yes Minor units, > 0
currency string Yes
customer_ref string Yes The handle you set at save time
payment_method_id string Yes The saved pm_*
off_session bool No Default true
auto_capture bool No Default true
description string No
customer_email string No
customer_name string No
metadata object No String→string
merchant_id string No Alternative to Mooov-Merchant
{
  "payment_id":   "pay_…",
  "status":       "succeeded",
  "state":        "captured",
  "provider":     "stripe",
  "provider_ref": "pi_…",
  "next_action":  null,
  "error":        null
}

status is one of succeeded, requires_action, failed. On requires_action (an off-session charge tripped 3DS), next_action carries what your front-end needs to finish the challenge:

"next_action": {
  "type":                 "use_stripe_sdk",
  "client_secret":        "pi_…_secret_…",
  "connected_account_id": "acct_…",
  "publishable_key":      "pk_…"
}

On failed, error is the same typed provider-error object as payment intents.

GET /v1/customers/by_ref?ref=<customer_ref>

Platform-key only. Requires customers:read. Resolves your durable customer_ref to the underlying PSP customer:

{
  "customer_ref":       "mbr_8f3a",
  "stripe_customer_id": "cus_…",
  "merchant_id":        "merch_…"
}

404 when no mapping exists for that ref on the addressed merchant.


4. Connect grant management

Documented in full in the Connect protocol:

Endpoint Purpose
POST /v1/public/connect/token Exchange a consent-flow auth code for a merchant_id (§3)
POST /v1/grants/{grant_id}/revoke Disconnect a tenant; idempotent; 204 on success (§5.1)

5. Errors

Error bodies are JSON. Two shapes exist:

Cross-cutting statuses:

Status Meaning
400 Malformed body, missing Idempotency-Key, validation failure
401 Missing/invalid signature, stale timestamp, unknown key
403 Key or grant lacks the required scope; revoked grant (GRANT_NOT_FOUND)
404 Resource doesn't exist — also returned when your principal can't see it
409 Idempotency conflict or invalid state transition
422 Typed business errors (merchant_not_charge_capable, refund exceeds captured, …)
5xx Mooov-side or provider-side failure — safe to retry writes with the same Idempotency-Key

The integrator-facing error code table is in §7 of the Connect protocol.

6. Versioning

/v1/* 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.