Skip to content
MOONIPdocs
IntegrationManual
PTEN
Go to dashboard

MB WAY

MB WAY is Portugal's mobile wallet. The customer enters their phone number, gets a push notification on their MB WAY app, taps to authorise — funds settle the same day.

#Flow at a glance

text
1. POST /v1/payments  ──────▶  Moonip returns transactionId
2. Customer enters their MB WAY phone number on your checkout
3. Moonip pushes the request to the customer's MB WAY app (5-min window)
4. Customer taps "Pay" in their app
5. Webhook charge.paid arrives at your endpoint

#1. Create the payment

Endpoint POST /v1/payments

bash
curl -X POST https://api.moonip.io/v1/payments \
  -H "Authorization: Bearer $MOONIP_API_KEY" \
  -H "Idempotency-Key: ORD-2034" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 4500,
    "currency": "EUR",
    "paymentMethods": ["MBWAY"],
    "customerName": "Ana Pereira",
    "customerEmail": "ana@example.com",
    "customerPhone": "+351912345678",
    "metadata": { "orderId": "ORD-2034" }
  }'

Idempotency. POST /v1/payments accepts the Idempotency-Key header (preferred) or the body field idempotencyKey as a fallback when the header is absent; send at least one. If you send both, they must match — divergent values are 400, and neither at all is 400 on this route.

FieldNotes
amountCents. 4500 = € 45,00. MB WAY supports up to € 750 per transaction.
currencyMust be "EUR".
paymentMethods["MBWAY"]
customerPhonePortuguese mobile in E.164 (+351XXXXXXXXX). Required for MB WAY. This is the field the platform forwards for delivery.
metadata.customerPhone / metadata.alias / metadata.phoneLegacy per-provider fallbacks some adapters still read — not the contract. A merchant routed to another provider gets no delivery from them. Always send customerPhone.

Response 201 Created

json
{
  "id": "c3d4e5f6-a7b8-4901-9cde-f01234567890",
  "status": "WAITING_PAYMENT",
  "amount": 4500,
  "currency": "EUR",
  "paymentMethods": ["MBWAY"],
  "createdAt": "2026-04-25T15:55:11.000Z"
}

The push to the customer's app is dispatched within seconds.

With the official SDK (same neutral package for every brand — configure it with https://api.moonip.io):

nodepythonphpdotnet
client.payments.create(input, { idempotencyKey })client.payments.create(input, idempotency_key)$client->payments->create($input, $idempotencyKey)client.Payments.CreateAsync(input, idempotencyKey)
client.payments.retrieve(id)client.payments.retrieve(id)$client->payments->retrieve($id)client.Payments.RetrieveAsync(id)

Send customerPhone in input — the SDK passes it through as the wire field customerPhone.

#2. Show a "waiting" screen

There's no QR code or redirect — the customer's phone is buzzing. Show them a loading state with a 5-minute countdown. Poll GET /v1/payments/{id} every 3 seconds for UX only:

js
const poll = async () => {
  const res = await fetch(`/api/payments/${txId}`); // server proxies to Moonip
  const tx = await res.json();
  if (tx.status === 'PAID') return showSuccess();
  if (['REFUSED', 'EXPIRED', 'CANCELLED'].includes(tx.status)) return showFailure(tx.status);
  setTimeout(poll, 3000);
};
poll();

Backend fulfillment must still come from the webhook, never the poll.

#3. Confirm via webhook

json
{
  "id": "evt_00000000000000000000000000000001",
  "object": "event",
  "api_version": "2026-07-23",
  "type": "charge.paid",
  "created_at": "2026-04-25T15:56:02.000Z",
  "data": {
    "object": {
      "id": "ch_c3d4e5f6-a7b8-4901-9cde-f01234567890",
      "object": "charge",
      "status": "paid",
      "amount": 4500,
      "currency": "EUR",
      "payment_method": "mbway",
      "settlement": {}
    }
  }
}

#Lifecycle

text
WAITING_PAYMENT  ──▶  PAID         ✓ fulfill order
                 ──▶  REFUSED      customer rejected on the app
                 ──▶  EXPIRED      no response within 5 minutes
                 ──▶  CANCELLED    customer hit Cancel

#Edge cases & FAQ

Q: The customer mistyped their phone number. A: The push is silently dropped and the charge expires after 5 minutes. Create a new transaction with the corrected number.

Q: Does the customer need a Portuguese bank account? A: Yes. MB WAY only works with Portuguese-issued cards/accounts.

Q: Can I retry the same transaction? A: No — once EXPIRED, the transaction is closed. Create a new one.

Q: What's the maximum amount? A: € 750 per transaction, € 2 500 per day per customer (MB WAY rules — these are not Moonip limits).

Q: Customer says "Paid" but I have WAITING_PAYMENT for an hour. A: That's a Provider/MB WAY clearing-side delay. Contact suporte@moonip.io with the transaction id — we'll reconcile from the settlement file.

#Next

→ Multibanco · API Reference