MagicPayments Integration Guides

Getting started

AuthSigningCallbacks

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:

ValueSent asUsed for
wallet_uidX-Wallet-UID headerIdentifies the wallet (currency + balance) you transact against.
key_idX-MP-Key-Id headerPublic identifier of your API key.
api_secretnever sentHMAC secret used to sign every request. Keep it server-side only.
cascade_id / gate_idrequest bodySelects the route (terminal) that processes the payment. Each guide tells you which one it needs.
Secrets stay on your server

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

EnvironmentBase URLNotes
Sandbox / Stagehttps://stage.<your-domain>Test terminals; no real money moves.
Productionhttps://<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":

CurrencyDecimal placesAmount fieldMeans
USD / EUR2259925.99
MGA (Ariary)2500000050,000.00
KRW (Won)050000₩50,000
Never hardcode "× 100"

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:

HeaderValue
X-Wallet-UIDyour wallet_uid
X-MP-Key-Idyour key_id
X-MP-Timestampcurrent UTC time in milliseconds
X-MP-Noncea unique random string per request
X-MP-Signaturebase64(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
Sign the exact bytes you send

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.

Python — reference signer
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:

StatusMeaningTerminal?
initiatedAccepted, not yet sent to the provider.no
processingAwaiting the provider / the payer's action.no
successFunds settled.yes
declineRejected for a business reason.yes
errorFailed during processing.yes

Hosted gates wrap the payment in an Invoice, which has its own states: unpaidpayingpaid, 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.

  1. Webhook callback (push) Set workflow_hooks.callback_url on the request. We POST the full payment (or invoice) payload to it on every status change, signed with the same HMAC scheme. Reply 200 to acknowledge; anything else is retried with growing delay.
  2. Status endpoint (pull) Poll /api/payment/status or /api/invoice/status by your own identifier if you missed a callback or need to reconcile.
Verify callback signatures

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

Next steps

Pick the gate you're integrating: