Merchant quickstart
Audience: a merchant integrating Mooov directly for your own business with an API key from the merchant portal. If you're a platform charging on behalf of many tenants, you want the Connect protocol instead.
Time to first test payment: about fifteen minutes.
1. Mint an API key
In the merchant portal (https://mooov3.mooov.money) go to
Developers → API keys and create a key. You'll get:
- a key id —
mk_test_xxxxxxxx(sandbox) ormk_live_xxxxxxxx(production) - a secret — shown once at creation; store it in your secret manager, never in source control
New keys carry the payments:write and payments:read scopes,
which is everything this quickstart needs.
The environment is baked into the key: mk_test_* keys route to PSP
test mode, mk_live_* keys move real money. Build against the
sandbox first.
2. Sign a request
Mooov uses HMAC request signing — your secret is never sent on the wire. Every request carries:
X-Mooov-Key-Id: mk_test_xxxxxxxx
X-Mooov-Timestamp: 2026-06-10T12:00:00Z (RFC3339 UTC, ±5 min skew)
X-Mooov-Signature: <lowercase hex HMAC-SHA256>
Idempotency-Key: <uuid> (required on writes)
The signature is HMAC-SHA256 over exactly four lines (no trailing newline):
<METHOD>\n<PATH>\n<TIMESTAMP>\n<hex SHA-256 of the raw body, or of "" on GET>
A minimal Node signer:
import { createHmac, createHash, randomUUID } from "node:crypto";
const KEY_ID = process.env.MOOOV_KEY_ID!; // mk_test_…
const SECRET = process.env.MOOOV_SECRET!;
const BASE = "https://sandbox.api.mooov.money";
export async function mooov(method: string, path: string, payload?: unknown) {
const body = payload ? JSON.stringify(payload) : "";
const ts = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
const bodyHash = createHash("sha256").update(body).digest("hex");
const sig = createHmac("sha256", SECRET)
.update(`${method}\n${path}\n${ts}\n${bodyHash}`)
.digest("hex");
return fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
"X-Mooov-Key-Id": KEY_ID,
"X-Mooov-Timestamp": ts,
"X-Mooov-Signature": sig,
...(method !== "GET" ? { "Idempotency-Key": randomUUID() } : {}),
},
body: body || undefined,
});
}
(A Python equivalent is in
§2 of the Connect protocol — drop the
Mooov-Merchant header, which is for platform keys only.)
3. Take your first payment
The fastest path is hosted checkout — Mooov opens a checkout session, you redirect the customer to it, and every payment method, SCA challenge, and receipt is handled for you:
const res = await mooov("POST", "/v1/payment_intents", {
payment_id: `pay_${crypto.randomUUID()}`, // your stable id
amount: 4200, // minor units = £42.00
currency: "GBP",
flow: "redirect",
success_url: "https://your-site.example/checkout/success",
cancel_url: "https://your-site.example/checkout/cancel",
description: "Order #1042",
});
const { provider } = await res.json();
// → send the customer to provider.hosted_url
In the sandbox, pay with the test card 4242 4242 4242 4242 (any
future expiry, any CVC). More test cards — including decline and 3DS
paths — are on the sandbox page. If your account is on
Mooov's high-risk acquiring route, the test cards (and the sandbox
currency, HKD) are different — see §3.2 of the sandbox page.
When the customer completes payment, the payment's state becomes
captured and your webhook receives payment.captured.
4. Hear about it (webhooks)
Register a webhook destination in the portal under Developers → Webhooks. You'll receive signed events:
{
"id": "evt_01HXX…",
"type": "payment.captured",
"occurred_at": "2026-06-10T12:34:56Z",
"merchant_id": "merch_yourbiz",
"data": {
"payment_id": "pay_…",
"state": "captured",
"amount": 4200,
"currency": "GBP"
}
}
Verify the X-Mooov-Signature: t=…,v1=… header before trusting the
body — code samples and the full event list are in the
webhook event catalog. The events you'll handle first:
type |
Meaning |
|---|---|
payment.captured |
Money captured — fulfil the order |
payment.failed |
Charge failed — data.failure_code says why |
payment.refunded / payment.partially_refunded |
A refund settled |
5. Refund a payment
await mooov("POST", `/v1/payment_intents/${paymentId}/refund`, {
amount: 2100, // omit for a full refund
reason: "customer_request",
});
Partial refunds move the payment to partially_refunded; refunding
the rest moves it to refunded. Full request/response shapes — plus
capture, void, and the payment timeline — are in the
API reference.
6. Go live
- Run the sandbox go-live checklist.
- Mint an
mk_live_*key in the portal. - Swap the base URL to
https://api.mooov.moneyand the key pair in your secret manager. Nothing else changes — the API surface is identical.
Questions: integrations@mooov.money.