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.
API https://api.mooov.money (mk_test_* and mk_live_*; see /environments/)
Merchant keys select their environment: mk_test_* selects test and
mk_live_* selects live. Connect platform keys (mk_platform_*) use
?environment=test or ?environment=live to select an environment-scoped
merchant grant; omitting the query defaults to live. Invalid, empty, or
repeated environment values return 400 INVALID_ENVIRONMENT. The platform
key itself is not environment-partitioned.
https://sandbox.api.mooov.money is a temporary compatibility alias; the
hostname does not select the request environment. See Environments.
MCP-capable agents and editors can query these endpoint and webhook schemas directly; see Connect your AI agent.
1. Conventions
Authentication
Authenticated payment and customer calls carry 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>
Sign only the URL pathname, without the query string. Send the environment
query on the request URL. Platform payment/customer calls require
Mooov-Merchant; a body merchant_id does not replace that header. Public
Connect and grant-management endpoints have their own requirements in the
Connect protocol.
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, subscription checkouts |
payments:read |
Payment timeline |
customers:read |
Customer lookups |
Newly minted merchant keys default to
["payments:write", "payments:read"].
Idempotency
Every payment mutation below 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 + setup_future_usage: "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_mobile |
string | Depends on enabled method | Customer mobile number in the format confirmed for your enabled payment method. Hosted wallet checkout collects its required customer details. |
payment_method_type |
string | No | Omit for configured routing. Enabled Mooov6 PKR wallet merchants can select "jazzcash_wallet" with flow: "redirect" or "embedded". |
customer_ref |
string | No | Your durable customer handle (platform keys only; live mode only until customer mappings are environment-scoped) |
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 | Must match the authenticated merchant; does not replace Mooov-Merchant |
Payment methods and currencies must be enabled for your merchant and chosen
environment. The PKR wallet selector is restricted to Mooov6; an incompatible
currency returns 400, and a different entity returns 422. Additional
methods require explicit activation during onboarding before use.
Response example for a Mooov3 redirect payment:
{
"payment_id": "pay_…",
"state": "processing",
"provider": {
"provider": "stripe",
"provider_ref": "cs_…",
"status": "pending",
"hosted_url": "https://checkout.stripe.com/c/pay/cs_example"
}
}
flow: "redirect"—provider.hosted_urlis the hosted checkout URL; redirect the browser there.flow: "embedded"—provider.hosted_urlis the Mooov-hosted checkout shell. Follow the returned URL rather than constructing it.provider.checkout_session_idis optional and route-dependent.flow: "server"initiates confirmation. A pending response hasstate: "processing"; confirm completion before fulfilment. Use the completion webhook where delivery is supported. Test Connect payments do not emit platform webhooks: read the signed payment timeline with?environment=testto check the outcome instead. An approved authorization can require capture. Inspectstateandprovider.status, even when HTTP status is200.
Mooov6 payment responses omit provider identity, references, session IDs, and
provider-native error diagnostics. They retain status, an available
hosted_url, and a neutral error when present. Optional fields may be
absent rather than null.
Example provider object excerpt containing a Mooov6 error:
{
"error": {
"category": "risk",
"code": "PAYMENT_DECLINED",
"message": "payment was declined",
"retryable": false
}
}
Error categories are auth, validation, provider, timeout, risk, and
internal. Codes and messages depend on the route and entity; Mooov6 uses
neutral codes. An optional outcome: "unknown" indicates uncertainty that
requires reconciliation before a new payment attempt.
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 200 — state stays
authorized and provider.status / provider.error carry the
failure. Some routes reject a separate capture with the typed
provider_operation_unsupported
error. An already-captured payment returns its current state before this check.
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 |
Where the payment route supports refunds, 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, or the configured route does not support refunds |
refunded_amount in this API response is the amount of this
operation. refunded_total is the cumulative total after the operation;
remaining_amount is the captured balance still available to refund.
An omitted amount or 0 means the full remaining balance, so a
custom partial-refund form must reject zero before calling the API.
For Connect integrations, call this endpoint from your backend using
your platform HMAC credentials, the lodge's Mooov-Merchant, and an
explicit ?environment=live or ?environment=test. The active grant
and key must permit payments:write; refunds:write alone is not
sufficient. Scope the local payment lookup to the authenticated lodge
and enforce your own admin refund permission before signing the request.
Persist a fresh Idempotency-Key once per intentional refund operation.
Retries reuse that key and identical body; another intentional partial
refund gets a new key. A timeout or 5xx is not proof the refund failed:
reconcile an uncertain outcome before starting a new operation. Do not
issue another refund merely because the local ledger is stale.
Use the response's refunded_total and the cumulative totals from
payment.partially_refunded / payment.refunded notifications to update
your ledger monotonically. API and provider events can describe the same
funds, so never add all received amounts together. Webhook
data.refunded_amount is cumulative, unlike this response's
refunded_amount. See refund snapshots
for exact fields, compatibility and out-of-order delivery handling.
Verify full refunds, successive partials, duplicate requests and over-refund rejection against a Test grant before Live use. Test Connect payment events are not delivered to platform destinations; check the API response and signed payment timeline instead.
POST /v1/payment_intents/{payment_id}/void
Voids an authorized (not yet captured) payment.
| Field | Type | Required |
|---|---|---|
reason |
string | No |
Response: { "payment_id": "pay_example", "state": "voided", "provider": { "status": "approved" } }.
A payment that isn't authorized returns
409 payment not voidable in current state. A route without void support
can return the typed 422 provider_operation_unsupported or 502 void not supported by configured provider. Refund availability is also route-specific.
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). Mooov6 timelines omit provider identity and
clear provider-native error codes and messages.
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.
This endpoint is temporarily unavailable in test mode because customer
and provider mappings are not yet environment-scoped. Test requests return
HTTP 422 with the stable test_customer_mappings_unavailable error without looking up a
live customer. Direct tokenized test payments remain available through
POST /v1/payment_intents when customer_ref is omitted.
| 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 | Must match Mooov-Merchant; header still required |
{
"payment_id": "pay_…",
"status": "succeeded",
"state": "captured",
"provider": "stripe",
"provider_ref": "pi_example"
}
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.
In test mode, the endpoint returns test_customer_mappings_unavailable
without reading the shared mapping store, regardless of whether a live
mapping exists.
4. Subscription checkout
POST /v1/subscription_checkouts
Opens an embedded subscription checkout for an enabled recurring-card route.
Requires a Connect platform key, payments:write, Mooov-Merchant, and an
Idempotency-Key. Direct merchant keys receive 422. Customer mappings are
currently live-only: test requests return test_customer_mappings_unavailable.
Confirm recurring checkout enablement during onboarding before integration.
| Field | Type | Required | Notes |
|---|---|---|---|
subscription_id |
string | Yes | Your stable enrollment ID |
customer_ref |
string | Yes | Platform-scoped customer handle |
amount |
int | Yes | Positive recurring amount in minor units |
currency |
string | Yes | Enabled currency |
interval |
string | Yes | day, week, month, or year |
interval_count |
int | No | Defaults to 1; non-negative |
trial_period_days |
int | No | Non-negative |
success_url, cancel_url |
string | Yes | Absolute HTTPS return URLs |
description |
string | No | Checkout description |
metadata |
object | No | String-to-string, as for payment intents |
merchant_id |
string | No | Must match Mooov-Merchant; header still required |
Successful creation returns HTTP 200:
{
"subscription_id": "sub_example",
"status": "pending",
"hosted_url": "https://pay.mooov.money/c/sub_example",
"checkout_session_id": "cs_example"
}
Send the customer to the returned hosted_url. Creation is not payment
completion; use subscription/payment webhooks to track the outcome. Provider
creation errors return 502 with status: "failed" and an error object.
5. 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) |
6. Errors
Error bodies are JSON. The shape depends on the failure boundary:
- Most
/v1/*endpoints:{ "error": "<message>" }, with typed errors like the422 merchant_not_charge_capablebody documented in the error reference. - Platform authentication:
{ "error": "MERCHANT_GRANT_REQUIRED", "message": "merchant has not granted this platform access" }. - Payment execution can return HTTP
200with an unsuccessful state andprovider.error; inspect the response, not just the HTTP status. POST /v1/public/connect/token: OAuth-shaped —{ "error": "invalid_grant", "error_description": "…" }.
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; missing/revoked environment-specific grant (MERCHANT_GRANT_REQUIRED) |
| 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; retain the same Idempotency-Key and identical body for any retry |
A 5xx does not prove that no payment occurred, and an idempotent retry may
replay the stored failure. Reconcile an uncertain payment through webhooks or
the timeline before starting a new operation. Unsupported capabilities need
configuration or integration changes, not repeated retries.
The integrator-facing error code table is in §7 of the Connect protocol.
7. 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.