Getting started
Authentication, request signing, money units, and the payment lifecycle every integration on this site shares. Read this first.
What you get from us
Before you write any code, your account manager provisions a project and hands you four values:
| Value | Sent as | Used for |
|---|---|---|
wallet_uid | X-Wallet-UID header | Identifies the wallet (currency + balance) you transact against. |
key_id | X-MP-Key-Id header | Public identifier of your API key. |
api_secret | never sent | HMAC secret used to sign every request. Keep it server-side only. |
cascade_id / gate_id | request body | Selects the route (terminal) that processes the payment. Each guide tells you which one it needs. |
The api_secret signs requests and is never transmitted. Card pay-ins and all
payouts must originate from your backend — never from a browser or mobile app.
Base URLs & environments
| Environment | Base URL | Notes |
|---|---|---|
| Sandbox / Stage | https://stage.<your-domain> | Test terminals; no real money moves. |
| Production | https://<your-domain> | Live credentials and live funds. |
Your account manager gives you the exact hostnames. Every path in these guides is relative to the base URL.
Money is always in minor units
Every amount is an integer in the currency's minor units — no decimal point, no separators.
The number of minor units per major unit comes from the currency, not from a fixed "× 100":
| Currency | Decimal places | Amount field | Means |
|---|---|---|---|
| USD / EUR | 2 | 2599 | 25.99 |
| MGA (Ariary) | 2 | 5000000 | 50,000.00 |
| KRW (Won) | 0 | 50000 | ₩50,000 |
KRW has zero decimal places, so 50000 is fifty thousand won, not five hundred.
Read the decimal places per currency and convert accordingly.
Authentication: signing a request
Every authenticated call carries these headers:
| Header | Value |
|---|---|
X-Wallet-UID | your wallet_uid |
X-MP-Key-Id | your key_id |
X-MP-Timestamp | current UTC time in milliseconds |
X-MP-Nonce | a unique random string per request |
X-MP-Signature | base64(HMAC-SHA256(api_secret, canonical_string)) |
The canonical string is eight lines joined by \n, in this exact order:
METHOD
PATH
SORTED_QUERY
SHA256_HEX(body)
TIMESTAMP_MS
NONCE
WALLET_UID
KEY_ID
PATHis the request path only (e.g./api/payment/payin/card), without the host or query string.SORTED_QUERYis the query parameters sorted by key then value and joined with&; empty string when there is no query.SHA256_HEX(body)is the hex SHA-256 of the exact request body bytes you send.- The timestamp must be within 5 minutes of server time, and each nonce may be used only once.
Serialize the body once, sign those bytes, and transmit those same bytes. If you let your HTTP
client re-serialize the JSON after signing, the body hash won't match and you'll get
401 Unauthorized. Use compact separators and ensure_ascii=False.
import hashlib, hmac, base64, json, secrets, time
import httpx
BASE_URL = "https://stage.example-mp.com"
WALLET_UID = "b7b3a53f-2f9e-4f3f-9c68-6f5ab1d03586"
KEY_ID = "mp_key_01HZX..."
API_SECRET = "••••••••" # from your account manager; keep server-side
def signed_post(path: str, payload: dict) -> httpx.Response:
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
ts = str(int(time.time() * 1000))
nonce = secrets.token_hex(16)
canonical = "\n".join([
"POST",
path,
"", # SORTED_QUERY (none here)
hashlib.sha256(body).hexdigest(),
ts,
nonce,
WALLET_UID,
KEY_ID,
])
signature = base64.b64encode(
hmac.new(API_SECRET.encode(), canonical.encode(), hashlib.sha256).digest()
).decode()
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Wallet-UID": WALLET_UID,
"X-MP-Key-Id": KEY_ID,
"X-MP-Timestamp": ts,
"X-MP-Nonce": nonce,
"X-MP-Signature": signature,
}
# Send the SAME bytes we signed — pass content=, not json=.
return httpx.post(BASE_URL + path, content=body, headers=headers)
Every code sample on the other pages assumes this signed_post() helper is in scope.
The payment lifecycle
Whether the payer uses a hosted gate or you post card data directly, the result is a Payment that moves through these states:
| Status | Meaning | Terminal? |
|---|---|---|
initiated | Accepted, not yet sent to the provider. | no |
processing | Awaiting the provider / the payer's action. | no |
success | Funds settled. | yes |
decline | Rejected for a business reason. | yes |
error | Failed during processing. | yes |
Hosted gates wrap the payment in an Invoice, which has its own states: unpaid →
paying → paid, or payment_failed / expired / canceled /
declined_by_payer.
Learn the result two ways
Always treat the callback as the source of truth and use polling as a fallback.
-
Webhook callback (push)
Set
workflow_hooks.callback_urlon the request. WePOSTthe full payment (or invoice) payload to it on every status change, signed with the same HMAC scheme. Reply200to acknowledge; anything else is retried with growing delay. -
Status endpoint (pull)
Poll
/api/payment/statusor/api/invoice/statusby your own identifier if you missed a callback or need to reconcile.
Callbacks carry an X-MP-Signature header over the raw body bytes. Recompute the HMAC
with your api_secret and compare before trusting the payload — this is how you know the
callback really came from us.
Amounts after settlement
Read the settled figure from processing_info.amount_acquired and the net credited to your
wallet from processing_info.amount_credited (after fees). If a provider revises a payment
after it settled, amount_corrected_diff carries the signed delta and you receive a fresh
callback — even when the status itself did not change.
Idempotency & identifiers
- Your
payment_id/invoice_idmust be unique within your project. Reuse is rejected — make it your order reference. - We return our own
payment_uid/invoice_uid. Store both; you can look a payment up by either. - A unique
X-MP-Nonceper request prevents replays; never reuse one.
Next steps
Pick the gate you're integrating:
- Madagascar P2P Gate — MVola mobile money, MGA.
- South Korea P2P Gate — bank transfer, KRW.
- General Card Gate — hosted card form.
- General Card H2H — server-to-server card data.