Card Payments
Accept Visa, Mastercard, American Express, Elo, Hipercard, and other major schemes through a Moonip-hosted payment page — no card data ever touches your servers, 3D Secure handled automatically.
#Flow at a glance
1. POST /v1/payments ──────▶ Moonip returns the payment: id, status WAITING_PAYMENT, cardRedirectUrl + checkoutUrl
2. Redirect (or open in iframe) the customer to cardRedirectUrl
3. Customer enters card details + completes 3DS challenge on Moonip's secure page
4. Customer is bounced back to your returnUrl (success or failure)
5. Webhook charge.paid (or charge.failed) confirms the outcome — trust the webhook, not the redirectWhy a redirect? It keeps cardholder data off your infrastructure entirely. Your application — backend or frontend — never sees a PAN, CVV, or expiry. Moonip's processor handles 3DS, tokenisation, and fraud screening on our side.
#1. Create the payment
Endpoint POST /v1/payments
curl -X POST https://api.moonip.io/v1/payments \
-H "Authorization: Bearer $MOONIP_API_KEY" \
-H "Idempotency-Key: ORD-9821" \
-H "Content-Type: application/json" \
-d '{
"amount": 8990,
"currency": "EUR",
"paymentMethods": ["CREDIT_CARD"],
"customerName": "João Costa",
"customerEmail": "joao@example.com",
"customerPhone": "+351912345678",
"returnUrl": "https://shop.example.com/orders/9821/return",
"cancelUrl": "https://shop.example.com/cart",
"metadata": { "orderId": "ORD-9821" }
}'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. Any non-blank string, scoped per merchant ((merchant, key)): replaying the same key and body returns the stored response, the same key with a different body is 409. The Idempotency-Replayed header is not emitted on this route — do not build retry logic on it.
| Field | Notes |
|---|---|
amount | Integer > 0, smallest unit (cents). 8990 = € 89,90. |
currency | 3–8 chars, default "BRL" (BRL, EUR, USD; additional currencies on request). |
paymentMethods | ["CREDIT_CARD"] here. The API accepts PIX, CRYPTO, CREDIT_CARD, BOLETO, MBWAY, MULTIBANCO, IDEAL, BANCONTACT, SEPA, BIZUM, SWAP_NOW. |
checkout | Optional checkout template slug (1–80). Unknown slug → 404; narrows paymentMethods to the template's methods. |
customerName / customerDocument / customerEmail / customerPhone | Flat customer fields. customerEmail must be an e-mail address; customerPhone is free-form (E.164 recommended). customerEmail is strongly recommended — receipts, fraud signals, chargeback dispute proof. |
customer | Alternative nested { name?, document?, email?, phone? }; flattened into the fields above (flat fields win). |
expectedPayerDocument | Digits-only expected payer CPF/CNPJ; becomes the payer lock where the provider applies it. |
returnUrl | Where to send the customer after they finish (success or failure). http(s) URL without fragment. Canonical field. |
cancelUrl | Where to send the customer on abort. Same URL rule; requires returnUrl (else 400). |
metadata | Free-form key/value. metadata.returnUrl / metadata.cancelUrl, if present, must be http(s) URLs. metadata.returnUrl still drives the hosted-checkout redirect, but the canonical field is the top-level returnUrl. |
items | Optional { name, quantity, unitPrice, totalPrice }[], integers > 0. |
webhookUrl | Optional per-transaction webhook URL override. |
idempotencyKey | Body fallback for the Idempotency-Key header (see above). |
country | Optional 2–3 chars; provider-account routing input. |
riskTier | Optional LOW, MEDIUM or HIGH. |
environment | Optional SANDBOX, STAGING, PRODUCTION (default PRODUCTION). |
expiresIn | Optional expiry in seconds, 60–604800 (a provider-returned expiry wins). |
Response 201 Created — the create response already carries the method artefacts (here cardRedirectUrl) and checkoutUrl; you do not need a follow-up GET to see them.
{
"id": "b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"status": "WAITING_PAYMENT",
"amount": 8990,
"currency": "EUR",
"paymentMethods": ["CREDIT_CARD"],
"customerName": "João Costa",
"customerEmail": "joao@example.com",
"customerPhone": "+351912345678",
"cardRedirectUrl": "https://checkout.moonip.io/c/b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"checkoutUrl": "https://checkout.moonip.io/b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"createdAt": "2026-04-25T15:50:11.000Z"
}checkoutUrl is present on create and retrieve only — list rows and the cancel response omit it.
With the official SDK (same neutral package for every brand — configure it with https://api.moonip.io):
| node | python | php | dotnet |
|---|---|---|---|
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) |
client.payments.list(query) | client.payments.list(**query) | $client->payments->list($query) | client.Payments.ListAsync(query) |
payments.list returns the page envelope { data, total, page, limit }.
#2. Redirect the customer
cardRedirectUrl comes in the create response (and on every GET /v1/payments/{id}), so you can redirect straight away:
curl https://api.moonip.io/v1/payments/b2c3d4e5-f6a7-4890-9bcd-ef0123456789 \
-H "Authorization: Bearer $MOONIP_API_KEY"{
"id": "b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"status": "WAITING_PAYMENT",
"amount": 8990,
"currency": "EUR",
"cardRedirectUrl": "https://checkout.moonip.io/c/b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"createdAt": "2026-04-25T15:50:11.000Z"
}Send the customer to cardRedirectUrl:
<a href="{{ cardRedirectUrl }}">Pay with card</a>Or do a server-side 302 Found:
res.redirect(303, transaction.cardRedirectUrl);After the customer completes the flow (or aborts), they bounce back to the returnUrl you supplied (the hosted checkout reads the same redirect from metadata.returnUrl) — with ?transactionId=...&status=... appended. Do not trust those query parameters. They're informational only — wait for the webhook before fulfilling.
#3. Confirm via webhook
{
"id": "evt_00000000000000000000000000000001",
"object": "event",
"api_version": "2026-07-23",
"type": "charge.paid",
"created_at": "2026-04-25T15:52:34.000Z",
"data": {
"object": {
"id": "ch_b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
"object": "charge",
"status": "paid",
"amount": 8990,
"currency": "BRL",
"payment_method": "card",
"settlement": {}
}
}
}The public charge status is paid for successful card payments.
Declines arrive as charge.failed; expiry prefers charge.expired. Read data.object.status for the public outcome. The event does not expose issuer decline reasons.
#Lifecycle
WAITING_PAYMENT ──▶ PROCESSING ──▶ APPROVED ✓ fulfill order
──▶ REFUSED issuer declined
──▶ CANCELLED customer abandoned
──▶ EXPIRED redirect link aged out (24h)After settlement (D+1 to D+30 depending on scheme and your contract), you may also see:
APPROVED ──▶ CHARGEBACK issuer raised a chargeback
APPROVED ──▶ DISPUTE cardholder opened a dispute
APPROVED ──▶ REFUNDED you (or Moonip) issued a refundEach one fires a webhook so you can sync the order state.
#Test cards
In sandbox, the redirect page accepts these deterministic numbers (any future expiry, any CVV):
| Number | Outcome |
|---|---|
4111 1111 1111 1111 | Always APPROVED |
4000 0000 0000 0002 | Always REFUSED — generic decline |
4000 0000 0000 0069 | Always REFUSED — expired card |
4000 0000 0000 9995 | Always REFUSED — insufficient funds |
4000 0000 0000 3220 | Triggers 3DS challenge then APPROVED |
See sandbox for the full list.
#FAQ
Q: Can I keep the customer on my domain? A: Phase 2 we'll ship an embeddable widget (iframe + tokeniser) that keeps your branding while still keeping cardholder data off your infrastructure. For now: hosted redirect.
Q: Do you support saved cards? A: Yes, but it requires your side to hold its own card-data-security compliance attestation. Email suporte@moonip.io to enable the Vault API.
Q: Which 3DS version is used? A: 3DS 2.x with frictionless flow when the issuer permits. Strong Customer Authentication (SCA) is mandatory for EUR — this is enforced server-side.
Q: Refunds? A: Refunds are requested from the admin panel today, full or partial — the API does not expose a merchant-usable refund endpoint (it refuses API-key callers).
Q: My customer was charged but I never got a webhook.
A: Check GET /v1/webhooks/deliveries — likely we tried and your endpoint returned a non-2xx. We retry up to 15 times. After that, contact support to replay.
#Next
→ MB WAY · Multibanco · API Reference