Dapit Instant Payments Merchant Setup Guide

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.

Estimated time: A developer can complete the full integration in 1–2 hours. If you're using our Hosted Checkout (no coding), you can be testing in 10 minutes.
About the old "I Paid" button. Earlier versions of this guide required the customer to click "I Paid" to trigger payment verification. That step is no longer necessary — our server-side reconciliation runs every ~60 seconds, matches the SEPA Instant credit on the custodial IBAN automatically, and fires your webhook regardless of whether the customer clicks anything. The button is still available on the hosted checkout and via the 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:

Security: Your API key authenticates every request. Never put it in client-side JavaScript, HTML, or mobile app code. All API calls go server → Dapit API → your server.

Step 1 — Verify Your API Key

1
Make a test API call

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"
}
✓ If you see "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.
✗ If you get "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

2
Pick Option A or Option B
Option A: API IntegrationOption B: Hosted Checkout
Developer needed?YesNo
Customer experienceStays on your websiteRedirected to Dapit-branded page
BrandingFully your own designDapit page with your merchant name
Setup time1–2 hours10 minutes
Best forCustom checkout, full controlQuick 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

3
Your server creates a session and gets payment details

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

4
Show the customer how to pay

On your checkout page, display the payment details from Step 3. The customer needs to see:

FieldExample ValueWhat to tell the customer
Beneficiary NameLongemalle Trustees OU"Send payment to this name"
IBANLT913740020058585210"Use this IBAN in your banking app"
Reference NumberDP-260406-XA6HSX"Enter this EXACTLY as the payment reference"
Amount€49.99"Send this exact amount"
BankDapitOptional — helps customer identify the bank
Important: The reference number is how we match the payment to the order. If the customer doesn't include it (or types it wrong), the payment won't be detected automatically. Make the reference number prominent and easy to copy.

The customer then:

  1. Opens their banking app (Wise, Revolut, N26, or any EUR-enabled bank)
  2. Creates a new SEPA transfer to the beneficiary name and IBAN
  3. Enters the reference number in the "Reference" or "Message to recipient" field
  4. 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)

5
Your server receives a POST when payment is confirmed

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 of timestamp + "." + raw_body, keyed on your merchant webhook_secret
  • X-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

Skip this entire section unless you want sub-60-second confirmation in the customer's UI. Webhook reconciliation in Step 5 already handles every payment automatically. The code below adds a faster client-side polling cycle for impatient customers who don't want to wait the full reconciliation interval.
6
Optional: tighter polling for faster client UX

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);
How long does verification take? SEPA Instant transfers settle in 5–20 seconds bank-to-bank. The optional fast-polling loop runs for up to ~7 minutes (60 polls × 7 seconds), so most payments surface in-page within 30 seconds. If the customer never clicks the button — or you don't expose one — the ~60-second reconciliation still catches the credit and fires the webhook from Step 5.
Don't rely on this path alone. The polling cycle is a UX accelerator, not a confirmation source of truth. Always wire your order-fulfillment logic to the webhook in Step 5. If a customer closes the page mid-polling, the webhook still arrives on your server.

Test Accounts for Staging

Use these pre-funded test IBANs to simulate payments in the staging environment. No real money is involved.

Account NameIBANBICCurrencyBalance
Bank Has No Money LT923740020074597273 CNPALT21XXX EUR €100.00
Bank Has Money LT223740020032524104 CNPALT21XXX EUR €10,000.00
Note: Test balances are shared across all staging merchants. Use small amounts (€1–€5) for testing. Balances are reset periodically.

Run a Test Payment

Follow this sequence to test the full payment flow end-to-end:

  1. Create a session — call create_session with amount 1.00 and your webhook URL
  2. Create an invoice — call create_invoice with the session token. Save the IBAN, beneficiary, and reference number.
  3. 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
  4. Wait for the webhook — within ~60 seconds your callback_url should receive a payment.confirmed POST containing your merchant_order_id. This is the source of truth.
  5. Verify via API — call payment_status with the reference number to confirm it shows confirmed
  6. (Optional) If you wired up Step 6's fast-polling flow, also confirm confirm_paid + check_payment return status: "confirmed" within 30 seconds.
✓ If steps 1–5 work, your integration is complete and ready for production. Step 6 is only required if you implemented optional in-page polling.

Integration Checklist

Confirm each of these before requesting production access:

Switch to Production

Once your staging integration tests pass:

  1. Contact your ISO admin — ask them to switch your merchant account from staging to production in MaxaFi
  2. No code changes needed — same API key, same endpoints, same flow. The only difference is payments use real EUR transfers.
  3. Verify the switch — call create_session and check that the response includes "environment": "production"
  4. 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
  5. You're live! — start accepting real payments from your customers
That's it. No card networks, no rolling reserves, no chargebacks. Payments settle instantly and are irrevocable. Welcome to instant payments.

Payment Statuses

StatusMeaningWhat to do
pendingSession created, waiting for customerShow checkout page
invoicedInvoice + reference generatedDisplay payment instructions
pollingReconciliation actively scanning for the creditShow spinner if you exposed the optional polling UI
partialReceived less than the invoice amountShow receipt + remaining balance; customer can top up using the same reference
confirmedFull payment received ✓Show success, fulfill order (the webhook has already fired)
overpaidReceived more than invoice amountOrder confirmed; overpaid amount is auto-refunded (minus €1 fee)
failedReconciliation 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
expiredSession token timed out before the customer started payingCreate a new session and restart checkout
cancelledSession was administratively cancelled before any credit landedCreate a new session if the customer still wants to pay
A late-arriving SEPA credit on a 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

ErrorCauseFix
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 merchantCheck for typos. Ask your admin for a new key if needed.
API key expired (401)Key has a set expiration dateGenerate a new key from the MaxaFi Info tab
Session not found (404)Invalid or expired session tokenCreate a new session. Tokens expire after 60 min by default.
Session expiredCustomer took too longCreate a new session and restart checkout
Amount must be positive (400)Amount is 0 or negativePass a positive number for the amount
timeout status on check_paymentPayment not detected after ~7 minCustomer 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.