Skip to content

Getting started in the sandbox

The sandbox behaves exactly like live — same statuses, same signed webhooks, same errors — against mock banks that settle in seconds. Which environment you’re in is decided by the secret you present, not by a different host.

  • Base URL: https://api.mvp-payments.com/v1 (sandbox and live)
  • Full API reference · webhook verification
  • Prefer to click before you code? Run a sandbox payment in the live demo — it shows the exact API calls as you go.

1. Get credentials

Apply at onboarding.mvp-payments.com. Once approved, your account is provisioned with sandbox credentials — an API key, a client id and a client secret, shown once in your dashboard. Live credentials come later, after your go-live review; nothing about your integration changes except the secret.

Every server-to-server call sends all three headers:

HeaderValue
x-api-keyyour API key
x-mvp-client-idyour client id
x-mvp-client-secretyour sandbox secret

Credentials are never accepted in request bodies, and no API key ever belongs in a browser bundle.

One read needs none of them: GET /v1/banks answers without credentials when you name the environment — ?environment=sandbox or ?environment=live — so the bank list can be read from anywhere, including a browser. It is the same list the Bank connections page shows. Send your credentials instead and the environment follows your secret, as on every other call.

2. Create a payment

curl https://api.mvp-payments.com/v1/payments \
  -H "x-api-key: $MVP_API_KEY" \
  -H "x-mvp-client-id: $MVP_CLIENT_ID" \
  -H "x-mvp-client-secret: $MVP_CLIENT_SECRET" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: order-1042-attempt-1" \
  -d '{
    "amount": "12.00",
    "currency": "EUR",
    "beneficiary": { "name": "Your Business Ltd", "iban": "DE89370400440532013000" },
    "remittanceInformationUnstructured": "Order 1042",
    "callbackUrl": "https://your-shop.example/return"
  }'

The response is your payment session:

{
  "mvpId": "aB3dEfGh1JkLmN0pQr",
  "sessionUrl": "https://session.mvp-payments.com/?mvpid=aB3dEfGh1JkLmN0pQr",
  "status": "CREATED",
  "expiresAt": "2026-08-31T12:30:00Z"
}

Send your customer to sessionUrl. In the sandbox the bank picker offers the mock banks — pick one and the journey runs exactly like a real bank, minus the real bank.

Paying in pounds

GBP settles over UK Faster Payments, and a UK account is a six-digit sort code plus an eight-digit account number — never an IBAN, not even a GB one. The rail decides the form: EUR, SEK and RON take an iban and refuse the pair; GBP takes the pair and refuses an iban (400 MVP-4011, with the offending field in errors[].path). Nothing is converted between the two.

curl https://api.mvp-payments.com/v1/payments \
  -H "x-api-key: $MVP_API_KEY" \
  -H "x-mvp-client-id: $MVP_CLIENT_ID" \
  -H "x-mvp-client-secret: $MVP_CLIENT_SECRET" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: order-1043-attempt-1" \
  -d '{
    "amount": "12.00",
    "currency": "GBP",
    "beneficiary": {
      "name": "Your Business Ltd",
      "sortCode": "000000",
      "accountNumber": "00000001"
    },
    "remittanceInformationUnstructured": "Order 1043",
    "callbackUrl": "https://your-shop.example/return"
  }'

Two limits are tighter on this rail than on the others: endToEndId is at most 31 characters and a structured reference at most 18 (400 MVP-4012 above either), and a single payment is capped at £1,000,000.00 (400 MVP-4013). If a UK bank needs the payer’s account before it will initiate, select-bank answers 400 MVP-4005 — that bank cannot serve this payment; on the API-only path retry with debtorSortCode and debtorAccountNumber, or offer another bank.

3. Drive every outcome with magic amounts

The mock banks read the payment amount to decide the outcome, so you can rehearse the unhappy paths deliberately:

AmountOutcome
any other amountSuccess — PROCESSING, then INITIATED about 15 seconds after hand-off
4.04FAILED with failureReason: REJECTED — terminal, no retry offered
4.03FAILED with failureReason: CANCELLED — retryable; the hosted page offers “Try a different bank”
4.05FAILED with failureReason: REJECTED straight from bank selection — the bank refuses the initiation outright, so the payer is never handed to the bank
5.02The bank fails at hand-off — the 502 upstream-error path

The embedded-flow mock bank additionally exercises in-journey credentials: username user, password password, then any method and the one-time code 123456. A wrong code exercises the attempt counter — three misses finalises the payment FAILED.

Real bank sandboxes each have their own screens, test personas and levers. The Bank connections page shows each bank’s sandbox test journey, taken from what the bank itself publishes — and the mock banks’ journeys, built from the same magic amounts as this table.

4. Handle the return

When the payer finishes at the bank we send them back to the callbackUrl you gave at create, with one query parameter added: mvpid, the id the create call returned. Your own query string and fragment come back unchanged, and a mvpid you set yourself is left alone.

https://your-shop.example/return?order=1042&mvpid=aB3dEfGh1JkLmN0pQr

Use it to find the order this browser was paying for, and use nothing else from that URL. It travels through the payer’s browser — which may be a different device from the one that started the payment: a desktop QR journey comes back on the phone, carrying none of your cookies — and anyone can send a browser to your return page with any value on it. So look the id up in the payments you created; if you have no record of it, ignore the request. Having a record is not authorisation either: the id is an identifier the payer can see and share, not a secret, so show a returning browser the outcome and a reference, and order detail only to a session that already owns the order. The outcome comes only from the status call in the next section, or from the signed webhook — never from the URL. A response without the full-tier fields (endToEndId, callbackUrl, timestamps) means the payment is not yours or your credentials were not accepted — the two are deliberately indistinguishable and both answer 200 — so log it and check your credentials before assuming the former.

A return that never arrives — closed tab, dead battery — is cosmetic, never financial: the payment still finalises server-side and the webhook still fires.

5. Watch the status land

Poll the payment — or better, receive the webhook:

curl https://api.mvp-payments.com/v1/payments/aB3dEfGh1JkLmN0pQr \
  -H "x-api-key: $MVP_API_KEY" \
  -H "x-mvp-client-id: $MVP_CLIENT_ID" \
  -H "x-mvp-client-secret: $MVP_CLIENT_SECRET"

Statuses walk CREATED → BANK_SELECTED → PROCESSING → INITIATED | FAILED. INITIATED is final success; FAILED always carries a failureReason. A session nobody completes ends FAILED / EXPIRED — with the webhook fired — so you never poll into the void. Some banks stop reporting at PROCESSING on the success path: check each bank’s statusCeiling from GET /v1/banks before treating PROCESSING as stuck.

Configure your webhook URL in the dashboard and verify every delivery — the verification guide has working Node.js, PHP and Python verifiers, and the dashboard has a signed test-webhook button.

6. Go live

When you’re ready, we enable live payments on your account and you swap the secret. Same base URL, same code, same statuses — that’s the point.

Questions while integrating? Email support@mvp-payments.com with your mvpId or request id.