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
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 redirectPor 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
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.
| Campo | Observação |
|---|---|
amount | Inteiro > 0 na menor unidade (centavos). 8990 = € 89,90. |
currency | 3–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. |
checkout | Slug opcional de template de checkout (1–80). Slug desconhecido → 404; restringe paymentMethods aos métodos do template. |
customerName / customerDocument / customerEmail / customerPhone | Campos 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. |
customer | Alternativa aninhada { name?, document?, email?, phone? }; é achatada nos campos acima (os campos planos vencem). |
expectedPayerDocument | CPF/CNPJ esperado do pagador, só dígitos; vira a trava de pagador onde o provedor a aplica. |
returnUrl | Para onde mandar o cliente quando ele terminar (com sucesso ou com falha). URL http(s) sem fragmento. Campo canônico. |
cancelUrl | Para onde mandar o cliente se ele desistir. Mesma regra de URL; exige returnUrl (senão 400). |
metadata | Chave/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. |
items | Opcional { name, quantity, unitPrice, totalPrice }[], inteiros > 0. |
webhookUrl | Override opcional de webhook por transação. |
idempotencyKey | Alternativa de corpo para o cabeçalho Idempotency-Key (acima). |
country | Opcional, 2–3 caracteres; entrada de roteamento de conta do provedor. |
riskTier | Opcional LOW, MEDIUM ou HIGH. |
environment | Opcional SANDBOX, STAGING, PRODUCTION (padrão PRODUCTION). |
expiresIn | Opcional, 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.
{
"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):
| 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 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:
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"
}Mande o cliente para o cardRedirectUrl:
<a href="{{ cardRedirectUrl }}">Pagar com cartão</a>Ou faça um 302 Found no servidor:
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
{
"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
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:
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 reembolsoCada 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úmero | Resultado |
|---|---|
4111 1111 1111 1111 | Sempre APPROVED |
4000 0000 0000 0002 | Sempre REFUSED — recusa genérica |
4000 0000 0000 0069 | Sempre REFUSED — cartão vencido |
4000 0000 0000 9995 | Sempre REFUSED — saldo insuficiente |
4000 0000 0000 3220 | Dispara 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.