Skip to content
MOONIPdocs
IntegrationManual
PTEN
Go to dashboard

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

text
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 redirect

Why 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

bash
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.

FieldNotes
amountInteger > 0, smallest unit (cents). 8990 = € 89,90.
currency3–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.
checkoutOptional checkout template slug (1–80). Unknown slug → 404; narrows paymentMethods to the template's methods.
customerName / customerDocument / customerEmail / customerPhoneFlat 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.
customerAlternative nested { name?, document?, email?, phone? }; flattened into the fields above (flat fields win).
expectedPayerDocumentDigits-only expected payer CPF/CNPJ; becomes the payer lock where the provider applies it.
returnUrlWhere to send the customer after they finish (success or failure). http(s) URL without fragment. Canonical field.
cancelUrlWhere to send the customer on abort. Same URL rule; requires returnUrl (else 400).
metadataFree-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.
itemsOptional { name, quantity, unitPrice, totalPrice }[], integers > 0.
webhookUrlOptional per-transaction webhook URL override.
idempotencyKeyBody fallback for the Idempotency-Key header (see above).
countryOptional 2–3 chars; provider-account routing input.
riskTierOptional LOW, MEDIUM or HIGH.
environmentOptional SANDBOX, STAGING, PRODUCTION (default PRODUCTION).
expiresInOptional 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.

json
{
  "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):

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)
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:

bash
curl https://api.moonip.io/v1/payments/b2c3d4e5-f6a7-4890-9bcd-ef0123456789 \
  -H "Authorization: Bearer $MOONIP_API_KEY"
json
{
  "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:

html
<a href="{{ cardRedirectUrl }}">Pay with card</a>

Or do a server-side 302 Found:

js
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

json
{
  "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

text
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:

text
APPROVED  ──▶  CHARGEBACK   issuer raised a chargeback
APPROVED  ──▶  DISPUTE      cardholder opened a dispute
APPROVED  ──▶  REFUNDED     you (or Moonip) issued a refund

Each 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):

NumberOutcome
4111 1111 1111 1111Always APPROVED
4000 0000 0000 0002Always REFUSED — generic decline
4000 0000 0000 0069Always REFUSED — expired card
4000 0000 0000 9995Always REFUSED — insufficient funds
4000 0000 0000 3220Triggers 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