Pular para o conteúdo
MOONIPdocs
IntegraçãoManual
PTEN
Ir para o painel

Pagamentos com cartão

Aceite Visa, Mastercard, American Express, Elo, Hipercard e as outras grandes bandeiras por uma página de pagamento hospedada pela Moonip — nenhum dado de cartão passa pelos seus servidores, e o 3D Secure é tratado automaticamente.

#O fluxo, de relance

text
1. POST /v1/payments  ──────▶  a Moonip devolve o pagamento: id, status WAITING_PAYMENT, cardRedirectUrl + checkoutUrl
2. Redirecione o cliente (ou abra num iframe) para o cardRedirectUrl
3. O cliente digita os dados do cartão e conclui o desafio 3DS na página segura da Moonip
4. O cliente volta para o seu returnUrl (com sucesso ou com falha)
5. O webhook charge.paid (ou charge.failed) confirma o resultado — confie no webhook, não no redirect

Por que um redirect? Ele mantém os dados do portador do cartão totalmente fora da sua infraestrutura. A sua aplicação — backend ou frontend — nunca vê PAN, CVV nem validade. O processador da Moonip cuida do 3DS, da tokenização e da análise antifraude do nosso lado.

#1. Criar a cobrança

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" }
  }'

Idempotência. O POST /v1/payments aceita o cabeçalho Idempotency-Key (preferido) ou o campo de corpo idempotencyKey como alternativa quando o cabeçalho está ausente; envie pelo menos um. Se enviar os dois, eles precisam ser iguais — valores divergentes são 400, e nenhum dos dois também é 400 nesta rota. Qualquer string não vazia, com escopo por lojista ((merchant, key)): repetir a mesma chave e o mesmo corpo devolve a resposta guardada; a mesma chave com corpo diferente é 409 (idempotency_key_reused). O cabeçalho Idempotency-Replayed não é emitido nesta rota — não monte lógica de retry em cima dele.

CampoObservação
amountInteiro > 0 na menor unidade (centavos). 8990 = € 89,90.
currency3–8 caracteres, padrão "BRL" (BRL, EUR, USD; outras moedas sob pedido).
paymentMethods["CREDIT_CARD"] aqui. A API aceita PIX, CRYPTO, CREDIT_CARD, BOLETO, MBWAY, MULTIBANCO, IDEAL, BANCONTACT, SEPA, BIZUM, SWAP_NOW.
checkoutSlug opcional de template de checkout (1–80). Slug desconhecido → 404; restringe paymentMethods aos métodos do template.
customerName / customerDocument / customerEmail / customerPhoneCampos de cliente planos. customerEmail precisa ser um e-mail; customerPhone é texto livre (E.164 recomendado). customerEmail é muito recomendado — comprovante, sinais de fraude e prova na contestação de chargeback.
customerAlternativa aninhada { name?, document?, email?, phone? }; é achatada nos campos acima (os campos planos vencem).
expectedPayerDocumentCPF/CNPJ esperado do pagador, só dígitos; vira a trava de pagador onde o provedor a aplica.
returnUrlPara onde mandar o cliente quando ele terminar (com sucesso ou com falha). URL http(s) sem fragmento. Campo canônico.
cancelUrlPara onde mandar o cliente se ele desistir. Mesma regra de URL; exige returnUrl (senão 400).
metadataChave/valor livre. metadata.returnUrl / metadata.cancelUrl, se existirem, precisam ser URLs http(s). O metadata.returnUrl ainda direciona o redirect do checkout hospedado, mas o campo canônico é o returnUrl de primeiro nível.
itemsOpcional { name, quantity, unitPrice, totalPrice }[], inteiros > 0.
webhookUrlOverride opcional de webhook por transação.
idempotencyKeyAlternativa de corpo para o cabeçalho Idempotency-Key (acima).
countryOpcional, 2–3 caracteres; entrada de roteamento de conta do provedor.
riskTierOpcional LOW, MEDIUM ou HIGH.
environmentOpcional SANDBOX, STAGING, PRODUCTION (padrão PRODUCTION).
expiresInOpcional, expiração em segundos, 60–604800 (a expiração devolvida pelo provedor vence).

Resposta 201 Created — a resposta de criação já traz os artefatos do método (aqui o cardRedirectUrl) e o checkoutUrl; não é preciso um GET depois para vê-los.

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"
}

O checkoutUrl só existe na criação e na consulta — nas linhas da listagem e na resposta do cancelamento ele não vem.

Com o SDK oficial (o mesmo pacote neutro para qualquer marca — configure-o com 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 devolve o envelope de página { data, total, page, limit }.

#2. Redirecionar o cliente

O cardRedirectUrl vem na resposta de criação (e em todo GET /v1/payments/{id}), então dá para redirecionar na hora:

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"
}

Mande o cliente para o cardRedirectUrl:

html
<a href="{{ cardRedirectUrl }}">Pagar com cartão</a>

Ou faça um 302 Found no servidor:

js
res.redirect(303, transaction.cardRedirectUrl);

Quando o cliente conclui o fluxo (ou desiste), ele volta para o returnUrl que você informou (o checkout hospedado lê o mesmo redirect em metadata.returnUrl) — com ?transactionId=...&status=... no final. Não confie nesses parâmetros de query. Eles são só informativos — espere o webhook antes de liberar o pedido.

#3. Confirmar pelo 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.

#Ciclo de vida

text
WAITING_PAYMENT  ──▶  PROCESSING  ──▶  APPROVED    ✓ libere o pedido
                                  ──▶  REFUSED     o emissor recusou
                                  ──▶  CANCELLED   o cliente desistiu
                                  ──▶  EXPIRED     o link de redirect venceu (24h)

Depois da liquidação (D+1 a D+30, conforme a bandeira e o seu contrato), você também pode ver:

text
APPROVED  ──▶  CHARGEBACK   o emissor abriu um chargeback
APPROVED  ──▶  DISPUTE      o portador do cartão abriu uma disputa
APPROVED  ──▶  REFUNDED     você (ou a Moonip) fez um reembolso

Cada um dispara um webhook, para você sincronizar o estado do pedido.

#Cartões de teste

No sandbox, a página de redirect aceita estes números determinísticos (qualquer validade futura, qualquer CVV):

NúmeroResultado
4111 1111 1111 1111Sempre APPROVED
4000 0000 0000 0002Sempre REFUSED — recusa genérica
4000 0000 0000 0069Sempre REFUSED — cartão vencido
4000 0000 0000 9995Sempre REFUSED — saldo insuficiente
4000 0000 0000 3220Dispara o desafio 3DS e depois APPROVED

Veja a lista completa em Sandbox.

#Perguntas frequentes

P: Dá para manter o cliente no meu domínio? R: Na fase 2 vamos lançar um widget incorporável (iframe + tokenizador) que mantém a sua marca e continua deixando os dados do portador do cartão fora da sua infraestrutura. Por enquanto: redirect hospedado.

P: Vocês aceitam cartão salvo? R: Sim, mas isso exige que o seu lado tenha a própria certificação de conformidade de segurança de dados de cartão. Escreva para suporte@moonip.io para habilitar a Vault API.

P: Qual versão do 3DS é usada? R: 3DS 2.x, com fluxo sem fricção quando o emissor permite. A Autenticação Forte do Cliente (SCA) é obrigatória em EUR — isso é imposto no servidor.

P: E reembolso? R: Reembolso hoje é solicitado pelo painel administrativo, total ou parcial — a API não expõe um endpoint de reembolso utilizável pelo lojista (recusa chamadas com chave de API).

P: Meu cliente foi cobrado, mas o webhook nunca chegou. R: Confira GET /v1/webhooks/deliveries — provavelmente tentamos e o seu endpoint respondeu algo fora de 2xx. Fazemos até 15 retentativas. Depois disso, fale com o suporte para reenviar.

#Próximo

→ MB WAY · Multibanco · Referência da API