Merchant Setup Guide
This guide walks you through connecting your website to the Dapit Instant Payments platform, testing in the staging environment, and going live with real EUR payments.
confirm_paid API as an optional "verify now" affordance for impatient customers who don't want to wait the full 60 seconds. If you skip it entirely, the system handles everything.
Prerequisites
Before you begin, make sure you have:
- Your API key — provided during merchant onboarding. It looks like a long hex string (e.g.,
a1b2c3d4e5f6...). If you don't have one, ask your ISO admin to generate it from the MaxaFi platform. - A server-side language — PHP, Node.js, Python, Ruby, or any language that can make HTTPS POST requests. All API calls must come from your server, never from the browser.
- A webhook endpoint (recommended) — A URL on your server that can receive POST requests. We'll send payment confirmations here.
Step 1 — Verify Your API Key
Open a terminal and run this curl command (replace YOUR_API_KEY with your actual key):
curl -X POST "https://www.maxafi.com/api/gateway/gateway.php?action=create_session" \
-H "Content-Type: application/json" \
-H "X-Gateway-Key: YOUR_API_KEY" \
-d '{
"amount": 1.00,
"currency": "EUR",
"items": [{"name": "Test Item", "qty": 1, "price": 1.00, "total": 1.00}]
}'
Expected response:
{
"success": true,
"session_id": 1,
"session_token": "a1b2c3d4e5f6...",
"checkout_url": "/checkout/pay.html?session=a1b2c3d4...",
"environment": "stage"
}
"success": true and "environment": "stage", your API key is working and you're connected to the staging environment. Save the session_token — you'll use it in the next steps.
"error": "Missing API key" or a 401 error, double-check your API key. Contact your ISO admin if the key was revoked or expired.
Step 2 — Choose Your Integration Method
| Option A: API Integration | Option B: Hosted Checkout | |
|---|---|---|
| Developer needed? | Yes | No |
| Customer experience | Stays on your website | Redirected to Dapit-branded page |
| Branding | Fully your own design | Dapit page with your merchant name |
| Setup time | 1–2 hours | 10 minutes |
| Best for | Custom checkout, full control | Quick start, no-code merchants |
Option B — Hosted Checkout (no code)
If you don't have a developer, you can skip Steps 3–5 entirely. Just create a session from your server and redirect the customer:
// Create session (from your server), then redirect customer to: https://www.maxafi.com/checkout/pay.html?session=SESSION_TOKEN // We handle the entire payment UI. // When confirmed, we POST to your callback_url.
Then skip ahead to Step 6 (Webhook).
Option A — API Integration
Continue to Step 3 below. You'll build the payment flow into your existing checkout page.
Step 3 — Create a Checkout Session
When a customer clicks "Pay with Instant Payments" on your checkout page, your server makes two API calls:
Call 1: Create Session
// POST to create_session — creates the payment session const session = await fetch('https://www.maxafi.com/api/gateway/gateway.php?action=create_session', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Gateway-Key': 'YOUR_API_KEY' }, body: JSON.stringify({ amount: 49.99, currency: 'EUR', order_id: 'ORD-12345', // YOUR order number — returned in webhook customer_email: 'customer@example.com', customer_name: 'John Smith', callback_url: 'https://yoursite.com/webhooks/dapit', items: [ { name: 'Premium Plan', qty: 1, price: 49.99, total: 49.99 } ] }) }).then(r => r.json()); // Save session.session_token — you need it for all subsequent calls
Call 2: Create Invoice
This generates the reference number and returns the IBAN + beneficiary details:
// POST to create_invoice — generates reference number + payment instructions const invoice = await fetch('https://www.maxafi.com/api/gateway/gateway.php?action=create_invoice', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: session.session_token }) }).then(r => r.json()); // invoice.beneficiary_name → "Longemalle Trustees OU" // invoice.iban → "LT913740020058585210" // invoice.bank_name → "Dapit" // invoice.reference_number → "DP-260406-XA6HSX" // invoice.amount → 49.99
Send these values back to your frontend to display on the checkout page.
Step 4 — Display Payment Instructions
On your checkout page, display the payment details from Step 3. The customer needs to see:
| Field | Example Value | What to tell the customer |
|---|---|---|
| Beneficiary Name | Longemalle Trustees OU | "Send payment to this name" |
| IBAN | LT913740020058585210 | "Use this IBAN in your banking app" |
| Reference Number | DP-260406-XA6HSX | "Enter this EXACTLY as the payment reference" |
| Amount | €49.99 | "Send this exact amount" |
| Bank | Dapit | Optional — helps customer identify the bank |
The customer then:
- Opens their banking app (Wise, Revolut, N26, or any EUR-enabled bank)
- Creates a new SEPA transfer to the beneficiary name and IBAN
- Enters the reference number in the "Reference" or "Message to recipient" field
- Sends the exact EUR amount
That's all the customer has to do. The next step (receiving the webhook on your server) happens automatically — no further action from the customer or your page is required. If you want the customer to see "Payment received" in their browser faster than the ~60-second reconciliation interval, you can optionally add an "I've Sent the Payment" button (covered in Step 6).
Step 5 — Receive Webhook Confirmation (this is all you need)
That's it for the required flow. Our reconciliation runs every ~60 seconds against the custodial IBAN, looking for incoming SEPA Instant credits whose remittance text matches your reference number. When it finds the match, it POSTs to the callback_url you supplied in Step 3 — automatically. You don't have to do anything else. The customer doesn't have to click anything. If they pay, you get the webhook within ~60 seconds of SEPA Instant settlement.
// Your webhook endpoint receives this JSON: // Fires for both exact and overpayments (not for partial — only when fully paid) { "event": "payment.confirmed", "merchant_order_id": "ORD-12345", // YOUR order number "reference_number": "DP-260406-XA6HSX", "invoice_number": "INV-260406-01234", "invoice_amount": 49.99, "amount_paid": 49.99, "amount_remaining": 0.00, "overpaid_amount": 0, "currency": "EUR", "paid_at": "2026-04-06 14:32:10", "session_token": "a1b2c3d4...", "customer_email": "customer@example.com", "customer_name": "John Smith" }
Return HTTP 200 to acknowledge receipt. If we don't receive a 200, the delivery is flagged in our retry queue and the operator dashboard surfaces the failure for redelivery.
Webhook signature
Each webhook POST carries two headers you can use to verify authenticity:
X-MaxaFi-Signature— HMAC-SHA256 oftimestamp + "." + raw_body, keyed on your merchantwebhook_secretX-MaxaFi-Timestamp— Unix timestamp at the moment the webhook was signed
If your webhook_secret is configured, always recompute the HMAC on your side and compare with constant-time string equality before trusting the payload.
Recovery flag (manual + automatic re-matches)
If a payment lands outside the normal reconciliation window — for example, the SEPA credit arrives after the session has already been failed — and our nightly orphan sweep or an operator-driven manual sync later matches it, the webhook still fires, but includes two extra fields so you can branch on it if your idempotency rules require:
{
...standard payment.confirmed fields...,
"recovered": true,
"recovery_method": "cron_orphan_sweep" // or "manual_admin_sync"
}
For most integrations you can treat a recovered webhook identically to a fresh confirmation — the field is informational. Only branch on it if you have strict accounting rules that distinguish "received during checkout" from "received late and reconciled later."
PHP Example
<?php $payload = json_decode(file_get_contents('php://input'), true); if ($payload['event'] === 'payment.confirmed') { $orderId = $payload['merchant_order_id']; $amount = $payload['amount_paid']; // Mark order as paid in your database $db->query("UPDATE orders SET status='paid' WHERE order_id='$orderId'"); // Send confirmation email, trigger shipping, etc. } http_response_code(200); echo json_encode(['received' => true]);
Step 6 — Optional — Speed Up the Customer Experience
If you want the customer to see "✓ Payment received" in their browser within seconds instead of up to a minute, expose an "I've Sent the Payment" button on your page. When the customer taps it, call confirm_paid to start a tighter polling cycle (every 5–7 seconds instead of every 60), then poll check_payment for status. The webhook in Step 5 still fires either way — this just speeds up what the customer sees in the foreground.
6a. Tell our API the customer says they paid
// OPTIONAL: when customer taps "I've Sent the Payment" // Switches our system into a tighter, short-lived polling cycle await fetch('https://www.maxafi.com/api/gateway/gateway.php?action=confirm_paid', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: sessionToken }) });
6b. Poll every 7 seconds until payment is found
// OPTIONAL: only if you want a fast in-page confirmation UX const poll = setInterval(async () => { const check = await fetch('https://www.maxafi.com/api/gateway/gateway.php?action=check_payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: sessionToken }) }).then(r => r.json()); if (check.status === 'confirmed') { clearInterval(poll); // ✅ Payment confirmed! Show success page. } else if (check.status === 'partial') { clearInterval(poll); // ⚠ Customer sent less than the invoice amount. // Show: "We received €X. Remaining: €Y. Send the rest with the same reference." // When they send more: call confirm_paid again, restart polling. } else if (check.status === 'overpaid') { clearInterval(poll); // 💰 Customer sent too much. Order is confirmed. // Refund of (overpaid - €1 fee) will be processed automatically. } else if (check.status === 'timeout') { clearInterval(poll); // ⏰ Payment not found in the fast-polling window. // The ~60-second background reconciliation continues to run; // most payments still arrive — just outside the in-page cycle. } // "pending" / "polling" — keep polling, show a spinner }, 7000);
Test Accounts for Staging
Use these pre-funded test IBANs to simulate payments in the staging environment. No real money is involved.
| Account Name | IBAN | BIC | Currency | Balance |
|---|---|---|---|---|
| Bank Has No Money | LT923740020074597273 |
CNPALT21XXX | EUR | €100.00 |
| Bank Has Money | LT223740020032524104 |
CNPALT21XXX | EUR | €10,000.00 |
Run a Test Payment
Follow this sequence to test the full payment flow end-to-end:
- Create a session — call
create_sessionwith amount1.00and your webhook URL - Create an invoice — call
create_invoicewith the session token. Save the IBAN, beneficiary, and reference number. - Send a test payment — from one of the test accounts above, send €1.00 to the staging custodial IBAN with the reference number as the payment reference
- Wait for the webhook — within ~60 seconds your
callback_urlshould receive apayment.confirmedPOST containing yourmerchant_order_id. This is the source of truth. - Verify via API — call
payment_statuswith the reference number to confirm it showsconfirmed - (Optional) If you wired up Step 6's fast-polling flow, also confirm
confirm_paid+check_paymentreturnstatus: "confirmed"within 30 seconds.
Integration Checklist
Confirm each of these before requesting production access:
create_sessionreturnssuccess: trueandenvironment: "stage"create_invoicereturns IBAN, beneficiary name, and a unique reference number- Your checkout page displays the payment instructions clearly
- The reference number is easy to copy (not buried in small text)
- After a test payment, your webhook endpoint receives
payment.confirmedwithin ~60 seconds - Your webhook handler updates your order database
- Your webhook returns HTTP 200
- If you implemented
X-MaxaFi-Signatureverification, the HMAC matches payment_statusreturnsconfirmedwhen queried by referencelist_transactionsshows the completed test transaction- (Optional) If you wired up Step 6,
check_paymentreturnsconfirmedwithin 30 seconds of the customer tapping "I've Sent the Payment"
Switch to Production
Once your staging integration tests pass:
- Contact your ISO admin — ask them to switch your merchant account from
stagingtoproductionin MaxaFi - No code changes needed — same API key, same endpoints, same flow. The only difference is payments use real EUR transfers.
- Verify the switch — call
create_sessionand check that the response includes"environment": "production" - Run one live test — create a €1.00 session and send a real €1 payment from your own bank account to confirm end-to-end
- You're live! — start accepting real payments from your customers
Payment Statuses
| Status | Meaning | What to do |
|---|---|---|
pending | Session created, waiting for customer | Show checkout page |
invoiced | Invoice + reference generated | Display payment instructions |
polling | Reconciliation actively scanning for the credit | Show spinner if you exposed the optional polling UI |
partial | Received less than the invoice amount | Show receipt + remaining balance; customer can top up using the same reference |
confirmed | Full payment received ✓ | Show success, fulfill order (the webhook has already fired) |
overpaid | Received more than invoice amount | Order confirmed; overpaid amount is auto-refunded (minus €1 fee) |
failed | Reconciliation gave up after the configured wait window (default 1h) | If money does still land later, our nightly orphan sweep re-matches it and fires the webhook with recovered: true |
expired | Session token timed out before the customer started paying | Create a new session and restart checkout |
cancelled | Session was administratively cancelled before any credit landed | Create a new session if the customer still wants to pay |
failed session does not mean lost money. The custodial IBAN holds it, the nightly sweep finds it, and your webhook fires with recovered: true. From your order-fulfillment code's perspective, this looks the same as any other payment.confirmed.
Common Errors
| Error | Cause | Fix |
|---|---|---|
Missing API key (401) | No X-Gateway-Key or X-Api-Key header (and no api_key in POST/GET params) | Add one of the supported credential headers to every API call |
Invalid API key (401) | Key doesn't match any active merchant | Check for typos. Ask your admin for a new key if needed. |
API key expired (401) | Key has a set expiration date | Generate a new key from the MaxaFi Info tab |
Session not found (404) | Invalid or expired session token | Create a new session. Tokens expire after 60 min by default. |
Session expired | Customer took too long | Create a new session and restart checkout |
Amount must be positive (400) | Amount is 0 or negative | Pass a positive number for the amount |
timeout status on check_payment | Payment not detected after ~7 min | Customer may not have sent it, or used wrong reference. Let them retry. |
Frequently Asked Questions
How fast do payments confirm?
SEPA Instant transfers settle bank-to-bank in 5–20 seconds. Our reconciliation runs every ~60 seconds, matches the credit, and fires your webhook automatically. So your server sees payment.confirmed within roughly a minute of the customer pressing Send in their banking app — with no further action from anyone. If you wired up the optional fast-polling flow in Step 6, the customer's browser can show the confirmation within ~30 seconds, but that's purely a UX accelerator; the server-side webhook is unaffected.
What if the customer doesn't include the reference number?
The payment won't be matched on the normal reconciliation pass and the session will eventually move to failed. The money is not lost — it sits on the custodial IBAN, and our nightly orphan sweep plus operator reconciliation tools re-attach it whenever possible. If they succeed, your webhook fires with recovered: true. If they can't (e.g. the reference is totally absent or pointing at a different merchant's session), contact support and we'll reconcile manually.
Can customers pay from any bank?
Yes — any bank that supports SEPA transfers to an IBAN. This includes Wise, Revolut, N26, traditional banks, and any EUR-enabled account worldwide. The customer doesn't need a European bank — they just need to be able to send EUR to an IBAN.
Are there chargebacks?
No. SEPA Credit Transfers are irrevocable once sent. There is no chargeback mechanism. This is the primary advantage over card payments.
What about refunds?
You can request a refund against any confirmed payment, either via the API (create_refund) or from the MaxaFi dashboard. The request is reviewed and the outbound payment is then initiated by Dapit — the merchant is not required (and is not able) to trigger the outbound transfer directly. The destination IBAN is always the original payer's IBAN; you cannot redirect a refund to a different account. Partial refunds are allowed (up to 3 per parent transaction); a €1 processing fee applies per refund. See the Request Refund reference for the full lifecycle and parameters.
Do I need to change anything when switching from staging to production?
No. Same API key, same endpoints, same code. Your ISO admin flips a switch on your merchant account. The only visible change is that create_session will return "environment": "production" instead of "stage".
Where can I see my transactions and settlements?
Log in to MaxaFi, open your merchant account, and go to the Info tab → Instant Payments Gateway section. You'll see a full dashboard with transactions, refunds, and settlement reports.
I need help. Who do I contact?
Contact your ISO admin or reach out through the MaxaFi ticket system. For API issues, include your merchant name, the API endpoint you're calling, and the full error response.