Max SystemsMAXFIdeveloper hub
DocsQuickstart

Quickstart

This guide walks through one test payment: your server creates a payment page, you pay on it with a test card, and your server receives the payment.succeeded webhook and checks its signature. Everything uses a test key (sk_test_…), so no real money moves.

Test mode
sk_test_ keys never reach a bank, and cards are not charged. Test and live mode use the same endpoints, the same payment page, the same webhook format and the same signature; test responses and events carry "livemode": false.

1. Get a test key

You need a MAXFI merchant account. There is no self-service sign-up: send an application on the access request page. When your account is set up, open merchant portal → Developer Hub → API Keys and create a test key pair.

Keep the secret key on your server. Never put it in a web page or a mobile app.

Shell
# Test secret key: merchant portal → Developer Hub → API Keys
export MAXFI_SECRET_KEY="sk_test_…"

2. Create a payment page

Your server sends one request to POST /v2/payments/checkout. amount is in minor units of the currency: 4900 is 49.00 USD.

POST /v2/payments/checkout
curl https://maxfi-api.exezine.az/v2/payments/checkout \
  -H "Authorization: Bearer $MAXFI_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 4900,
    "currency": "USD",
    "description": "Order 42",
    "order_id": "order_42",
    "success_url": "https://yourapp.com/orders/42/thanks",
    "cancel_url": "https://yourapp.com/orders/42",
    "webhook_url": "https://yourapp.com/maxfi/webhook"
  }'

The response contains checkout_url. Send the customer there: redirect them or show the link. The link is valid for 30 minutes (expires_at).

← 200 OK
{
  "id": "cs_4f0c9a2e7b1d4e8f9a6b3c5d7e9f1a2b",
  "checkout_url": "https://maxfi-api.exezine.az/checkout/cs_4f0c9a2e7b1d4e8f9a6b3c5d7e9f1a2b",
  "expires_at": "2026-11-13T09:45:00.000Z",
  "amount": 4900,
  "currency": "USD",
  "status": "open",
  "livemode": false
}

If your server may repeat the request, for example after a timeout, send an Idempotency-Key header: a repeat with the same key and the same body returns the same session instead of creating a second one. Store the checkout_url you received and send the customer only to it.

3. Receive the webhook

When the payment succeeds or fails, MAXFI sends a POST request with a JSON event to the webhook_url from step 2: payment.succeeded or payment.failed. Without webhook_url in the request, events go to the webhook URL saved in the merchant portal. The address must be public and use https; MAXFI does not follow redirects. All event types: Webhooks.

The event has id, type, livemode, created_at and data; data.order_number repeats the order_id you sent.

If your account has a webhook secret, every delivery carries the X-MAXFI-Signature header: sha256= followed by the hex HMAC-SHA256 of the raw request body, computed with that secret. The secret is created when you save the webhook settings in merchant portal → Developer Hub → Webhooks, and it also signs deliveries to a webhook_url passed in the request. Without a secret, deliveries are not signed.

Reply with any 2xx status within 10 seconds. Otherwise MAXFI tries again: up to 8 attempts in total — right away, then 30 s, 2 min, 10 min, 30 min, 1 h, 6 h and 24 h after the previous attempt. The same event can arrive more than once, so process each event id only once.

POST /maxfi/webhook
import crypto from "node:crypto";
import express from "express";

const app = express();

// The signature covers the exact bytes MAXFI sent, so read the raw body.
app.post("/maxfi/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const received = req.get("X-MAXFI-Signature") || "";
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.MAXFI_WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");

  const valid = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!valid) return res.status(401).end();

  const event = JSON.parse(req.body.toString("utf8"));
  if (alreadyProcessed(event.id)) return res.status(200).end();

  switch (event.type) {
    case "payment.succeeded":
      markOrderPaid(event.data.order_number, event.livemode);
      break;
    case "payment.failed":
      markOrderFailed(event.data.order_number, event.data.error_message);
      break;
  }
  res.status(200).end();
});

4. Pay with a test card

Open checkout_url in a browser and pay with card 4242 4242 4242 4242, any future expiry date and any three-digit CVC. The payment succeeds, and payment.succeeded arrives at your webhook URL.

Each test card always behaves the same way. When the result comes right away in the response, your webhook URL also receives payment.succeeded or payment.failed, as for a live payment:

Card numberResult
4242 4242 4242 4242Visa, succeeds
5555 5555 5555 4444Mastercard, succeeds
3782 822463 10005American Express, succeeds
4000 0000 0000 0002Declined: card_declined
4000 0000 0000 9995Declined, insufficient funds: insufficient_funds
4000 0000 0000 0069Declined, expired card: expired_card
4000 0000 0000 0127Declined, incorrect CVC: incorrect_cvc
4000 0000 0000 3220Requires 3-D Secure: you choose the result on the MAXFI test page
4000 0000 0000 3063Requires 3-D Secure; the check fails whichever button you press: 3ds_failed
4000 0000 0000 0119Processing error after 3 seconds: processing_error
4000 0000 0000 9235Blocked as fraud: fraud_detected
4000 0000 0000 0259Fails after 5 seconds (simulated network timeout)

The same list is in merchant portal → Developer Hub → Sandbox.

Steps that a bank or a wallet would show the customer — the 3-D Secure check, confirmation in SberPay, T-Pay or YooMoney, payment by SBP QR code — open a MAXFI test page at maxfi-api.exezine.az/checkout/sandbox/confirm/… with two buttons: confirm or decline. Then you return to the payment page, which shows the result. For a payment created with POST /v2/payments you go to its return_url; without it the test page shows the result itself. Your choice is the result of the payment: it gets its final status, and your webhook URL receives payment.succeeded or payment.failed with "livemode": false, as for a live payment.

SberPay and T-Pay ask for a phone number. These numbers give fixed results (both methods work only for payments in RUB):

Phone numberResult
+7 900 000 00 01The MAXFI test page opens: confirm or cancel the payment. Any number that is not in this table works the same way.
+7 900 000 00 02Declined, the customer cancelled: user_cancelled
+7 900 000 00 03Declined after 5 seconds, no confirmation in time: timeout
+7 900 000 00 04Blocked as fraud: fraud_detected
+7 900 000 00 05Declined, insufficient funds: insufficient_funds
+7 900 000 00 06Declined by the wallet: wallet_declined

YooMoney and SBP always open the test page: for YooMoney the payment page asks only for an email, and the SBP QR code — and the link under it — leads to the same page. Through the API the same numbers work in payment_method_data.sberpay.phone and payment_method_data.tpay.phone, and for YooMoney in payment_method_data.yoomoney.wallet_id.

Apple Pay and Google Pay are not shown on the payment page in test mode: their buttons need your registration with Apple and Google. Test them through POST /v2/payments with payment_method set to apple_pay or google_pay and any non-empty payment_token: the test payment succeeds at once.

Going live

Live keys (sk_live_) work only after MAXFI enables live mode for your account, once onboarding and KYC are complete; until then, requests with a live key are refused. Then replace the test key with the live key on your server. Endpoints, request and response formats and webhooks are the same in both modes, but live payments move real money.

Quickstart — your first test payment · MAXFI Developer Hub