API MagicPay
Esta é a referência da API v1 do MagicPay para integrar pagamentos PIX ao seu checkout. Com ela você cria cobranças, mostra o QR code (ou manda o cliente para a nossa página de pagamento), acompanha o status e recebe webhooks assinados quando o pagamento é confirmado.
Visão geral
| Item | Valor |
|---|---|
| URL base | https://api.amagicpay.tech/api/v1 |
| Formato | JSON (UTF-8) na requisição e na resposta |
| Autenticação | Authorization: Bearer <sua chave de API> |
| Método de pagamento | PIX |
| Valores | inteiros em centavos (9990 = R$ 99,90) |
| Moeda | BRL |
| Datas | ISO 8601 em UTC (2026-09-15T18:30:00.000Z) |
| Ids | cobrança ch_ + 24 caracteres; conta mer_ + 16; entrega de webhook whd_ + 16 |
Regras gerais:
- Toda requisição com corpo usa
Content-Type: application/json. - Toda resposta é JSON e vem com
Cache-Control: no-store. - Campos desconhecidos no corpo são ignorados.
- Nunca use float para dinheiro.
amount,feeenet_amountsão sempre centavos inteiros. - Chamadas à API são de servidor para servidor. Nunca coloque a chave de API em aplicativo, página web ou qualquer código que rode no dispositivo do cliente.
Fluxo típico de integração
- O cliente fecha o pedido na sua loja.
- Seu servidor chama
POST /api/v1/chargescom o valor, os dados do cliente e umIdempotency-Key. - Você mostra o PIX: o
pix.qr_code(copia e cola) e a imagempix.qr_code_image_urlno seu checkout, ou redireciona o cliente parapayment_url. - O cliente paga no app do banco.
- O MagicPay confirma o pagamento na adquirente e envia o webhook
charge.paidpara a sua URL, assinado com o seu segredo. - Seu servidor valida a assinatura, confere o valor e libera o pedido.
Autenticação
A chave de API é gerada no painel do MagicPay, em Integração. Ela aparece uma única vez ao ser gerada: guarde num cofre de segredos ou numa variável de ambiente do seu servidor. Guardamos só um hash, então não conseguimos mostrá-la de novo. Se perder a chave ou suspeitar de vazamento, use Rotacionar: a chave antiga para de funcionar na hora.
O formato é mp_live_ seguido de 40 letras e números (ou mp_test_ numa chave de teste). Envie em toda requisição:
curl https://api.amagicpay.tech/api/v1/me \
-H "Authorization: Bearer mp_live_SUA_CHAVE_AQUI"
A chave só funciona quando:
- a sua conta foi aprovada (cadastro e KYC concluídos e analisados);
- a conta não está suspensa;
- o gateway MagicPay está ativo.
Quando alguma dessas condições falha, a API responde 401 com um código que diz o motivo:
| HTTP | code |
Quando |
|---|---|---|
| 401 | unauthorized |
Cabeçalho Authorization ausente, chave em formato inválido, chave inexistente ou rotacionada, conta desativada |
| 401 | merchant_not_approved |
Conta ainda em cadastro, em análise ou rejeitada |
| 401 | merchant_suspended |
Conta suspensa. Fale com o suporte |
| 401 | gateway_suspended |
O gateway MagicPay está suspenso. Fale com o suporte |
Cobranças
Criar uma cobrança
POST /api/v1/charges
Cria uma cobrança PIX e já devolve o QR code. Responde 201 quando cria e 200 quando é uma repetição com a mesma Idempotency-Key (veja Idempotência).
Cabeçalhos
| Cabeçalho | Obrigatório | Descrição |
|---|---|---|
Authorization |
sim | Bearer <chave> |
Content-Type |
sim | application/json |
Idempotency-Key |
recomendado | Até 128 caracteres. Use um valor único por tentativa de pagamento, por exemplo o id do pedido |
Corpo
| Campo | Tipo | Obrigatório | Regras |
|---|---|---|---|
amount |
inteiro | sim | Centavos. Mínimo 100 (R$ 1,00), máximo 50000000 (R$ 500.000,00) |
customer |
objeto | sim | Dados do pagador. A adquirente exige todos os campos abaixo |
customer.name |
texto | sim | 2 a 120 caracteres |
customer.document |
texto | sim | CPF (11 dígitos) ou CNPJ (14 dígitos), com dígito verificador válido. A pontuação é removida (123.456.789-09 vira 12345678909) |
customer.email |
texto | sim | E-mail válido, até 160 caracteres. Gravado em minúsculo |
customer.phone |
texto | sim | DDD + número, 10 ou 11 dígitos. A pontuação é removida |
description |
texto | não | Até 120 caracteres. Aparece na página de pagamento |
external_id |
texto | não | Até 80 caracteres. O id do pedido no seu sistema. Não precisa ser único: serve para busca |
expires_in |
inteiro | não | Segundos até expirar. Mínimo 300 (5 min), máximo 259200 (72 h). Padrão 3600 (1 h) |
metadata |
objeto | não | Pares livres, até 2 KB em JSON. Devolvido como veio |
success_url |
texto | não | URL https:// para onde a página de pagamento leva o cliente depois de pagar. Se omitida, usa a URL de sucesso configurada no painel (se houver) |
Exemplo
curl -X POST https://api.amagicpay.tech/api/v1/charges \
-H "Authorization: Bearer mp_live_SUA_CHAVE_AQUI" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: pedido-123" \
-d '{
"amount": 9990,
"description": "Chapinha Portátil",
"external_id": "pedido-123",
"customer": {
"name": "Maria Silva",
"document": "12345678909",
"email": "maria@exemplo.com",
"phone": "84999999999"
},
"expires_in": 3600,
"metadata": { "utm_source": "meta" },
"success_url": "https://sualoja.com/obrigado"
}'
Resposta 201 Created:
{
"id": "ch_7k3m9q2x4v6b8n1p5r0t2w4y",
"status": "pending",
"amount": 9990,
"currency": "BRL",
"description": "Chapinha Portátil",
"external_id": "pedido-123",
"acquirer": "podpay",
"customer": {
"name": "Maria Silva",
"document": "12345678909",
"document_type": "cpf",
"email": "maria@exemplo.com",
"phone": "84999999999"
},
"pix": {
"qr_code": "00020126580014br.gov.bcb.pix...",
"qr_code_image_url": "https://pay.amagicpay.tech/pay/ch_7k3m9q2x4v6b8n1p5r0t2w4y/qr",
"expires_at": "2026-09-15T19:30:00.000Z"
},
"payment_url": "https://pay.amagicpay.tech/pay/ch_7k3m9q2x4v6b8n1p5r0t2w4y",
"fee": 599,
"net_amount": 9391,
"metadata": { "utm_source": "meta" },
"paid_at": null,
"expires_at": "2026-09-15T19:30:00.000Z",
"created_at": "2026-09-15T18:30:00.000Z",
"updated_at": "2026-09-15T18:30:01.000Z"
}
Se a adquirente falhar
A cobrança é gravada antes de ir para a adquirente. Se a adquirente recusar ou não responder, a cobrança vira failed, a API responde 502 provider_error e, se você tiver webhook configurado, recebe charge.failed. A cobrança com falha continua visível no painel e em GET /api/v1/charges?external_id=....
Para tentar de novo depois de um 502, use uma nova Idempotency-Key. Repetir a mesma chave devolve a cobrança que falhou (200, status: "failed"), sem nova tentativa na adquirente.
Erros possíveis
| HTTP | code |
Motivo |
|---|---|---|
| 400 | validation_error |
Corpo que não é JSON, campo inválido ou ausente, Idempotency-Key vazia ou maior que 128 caracteres |
| 401 | unauthorized, merchant_not_approved, merchant_suspended, gateway_suspended |
Veja Autenticação |
| 409 | idempotency_conflict |
Idempotency-Key já usada com outro corpo |
| 429 | rate_limited |
Mais de 60 requisições por minuto com a mesma chave |
| 500 | internal_error |
Erro inesperado do nosso lado |
| 502 | provider_error |
A adquirente falhou ao criar o PIX |
| 503 | no_acquirer |
O gateway está sem adquirente ativa. Fale com o suporte |
O objeto cobrança
É o mesmo formato em todas as respostas de cobrança e dentro de data nos webhooks.
| Campo | Tipo | Descrição |
|---|---|---|
id |
texto | Id da cobrança (ch_...) |
status |
texto | pending, paid, expired, refunded ou failed. Veja Status da cobrança |
amount |
inteiro | Valor em centavos |
currency |
texto | Sempre BRL |
description |
texto ou null |
Descrição enviada |
external_id |
texto ou null |
Id do pedido no seu sistema |
acquirer |
texto | Adquirente que processa esta cobrança (podpay, bpx, ou mock em ambiente de teste). Informativo |
customer |
objeto | name, document (só dígitos), document_type (cpf ou cnpj), email, phone (só dígitos) |
pix.qr_code |
texto ou null |
BR Code PIX (copia e cola). null quando a cobrança falhou antes de chegar na adquirente |
pix.qr_code_image_url |
texto | PNG do QR code (320 px), público |
pix.expires_at |
texto | Igual a expires_at |
payment_url |
texto | Página de pagamento pronta, pública |
fee |
inteiro ou null |
Taxa do MagicPay sobre esta cobrança, em centavos, calculada pelo seu plano |
net_amount |
inteiro ou null |
amount - fee. Não desconta a reserva de segurança nem a taxa de saque |
metadata |
objeto | O que você enviou ({} se nada) |
paid_at |
texto ou null |
Quando o pagamento foi confirmado |
expires_at |
texto | Prazo para pagar |
created_at |
texto | Criação |
updated_at |
texto | Última alteração |
No exemplo acima, com um plano de 4,99% + R$ 1,00: fee = 100 + arredondamento(9990 × 4,99%) = 100 + 499 = 599 e net_amount = 9990 - 599 = 9391.
Consultar uma cobrança
GET /api/v1/charges/{id}
curl https://api.amagicpay.tech/api/v1/charges/ch_7k3m9q2x4v6b8n1p5r0t2w4y \
-H "Authorization: Bearer mp_live_SUA_CHAVE_AQUI"
Responde 200 com o objeto cobrança. Responde 404 not_found se o id não existir, estiver malformado ou for de outra conta (nunca revelamos cobranças de terceiros).
Listar cobranças
GET /api/v1/charges
Lista as cobranças da sua conta, das mais novas para as mais antigas.
| Parâmetro | Tipo | Descrição |
|---|---|---|
status |
texto | Filtra por pending, paid, expired, refunded ou failed |
external_id |
texto | Filtra pelo id do pedido (igualdade exata, até 80 caracteres) |
limit |
inteiro | 1 a 100. Padrão 20 |
starting_after |
texto | Id de uma cobrança sua (ch_...). Devolve as cobranças que vêm depois dela na ordenação |
curl "https://api.amagicpay.tech/api/v1/charges?status=paid&limit=50" \
-H "Authorization: Bearer mp_live_SUA_CHAVE_AQUI"
Resposta (cada item de data é um objeto cobrança completo):
{
"data": [
{ "id": "ch_7k3m9q2x4v6b8n1p5r0t2w4y", "status": "paid", "amount": 9990 }
],
"has_more": true
}
Para paginar, repita a chamada com starting_after igual ao id do último item de data enquanto has_more for true:
curl "https://api.amagicpay.tech/api/v1/charges?status=paid&limit=50&starting_after=ch_7k3m9q2x4v6b8n1p5r0t2w4y" \
-H "Authorization: Bearer mp_live_SUA_CHAVE_AQUI"
Um starting_after que não seja uma cobrança da sua conta responde 400 validation_error.
Sincronizar com a adquirente
POST /api/v1/charges/{id}/sync
Consulta a adquirente agora e aplica o resultado (por exemplo, marca como paga). Útil para um botão "Já paguei" no seu checkout ou para conferir um pedido sem esperar o webhook. Não precisa de corpo.
curl -X POST https://api.amagicpay.tech/api/v1/charges/ch_7k3m9q2x4v6b8n1p5r0t2w4y/sync \
-H "Authorization: Bearer mp_live_SUA_CHAVE_AQUI"
- Responde
200com o objeto cobrança já atualizado. - Cada cobrança consulta a adquirente no máximo uma vez a cada 5 segundos. Chamadas dentro desse intervalo devolvem o estado já gravado, sem nova consulta.
- Uma cobrança que falhou antes de chegar na adquirente volta como está.
404not_foundse a cobrança não for sua;502provider_errorse a adquirente não responder.
Não use o sync em laço para descobrir pagamentos: o webhook chega sozinho e as cobranças pendentes também são conferidas periodicamente do nosso lado.
Conta
GET /api/v1/me
Confirma que a chave funciona e mostra para qual ambiente vão as suas cobranças novas.
curl https://api.amagicpay.tech/api/v1/me \
-H "Authorization: Bearer mp_live_SUA_CHAVE_AQUI"
{
"merchant": {
"id": "mer_4f8k2m6q9t3v7x1z",
"name": "Sua Loja",
"webhook_url": "https://sualoja.com/webhooks/pix",
"env": "production"
},
"acquirer": { "kind": "podpay", "env": "production" }
}
env é production ou sandbox. Quando o gateway está sem adquirente ativa, acquirer e merchant.env vêm null e a criação de cobranças responde 503 no_acquirer.
Idempotência
Rede cai e timeouts acontecem. Para não criar dois PIX para o mesmo pedido, envie o cabeçalho Idempotency-Key em POST /api/v1/charges.
| Situação | Resposta |
|---|---|
| Chave nova | 201, cobrança criada |
| Mesma chave e mesmo corpo | 200 com a mesma cobrança, no estado atual (pode já estar paid), e o cabeçalho Idempotent-Replayed: true |
| Mesma chave e corpo diferente | 409 idempotency_conflict |
Detalhes que importam:
- A chave vale por conta e não expira. Use algo único por tentativa de pagamento, como
pedido-123ou um UUID guardado junto do pedido. - A comparação do corpo é feita depois da validação e da normalização: a ordem das chaves do JSON não importa, e
"123.456.789-09"equivale a"12345678909". Mudarexpires_inoumetadataconta como corpo diferente. - Se duas requisições com a mesma chave chegarem ao mesmo tempo, só uma cria a cobrança. A outra recebe
200com a mesma cobrança, que pode ainda estar sempix.qr_code. Nesse caso, consulteGET /api/v1/charges/{id}em seguida. - Depois de um
502provider_error, a cobrança ficafailede a chave fica ligada a ela. Para tentar de novo, use outra chave (por exemplopedido-123-2). - Sem o cabeçalho, cada chamada cria uma cobrança nova.
Status da cobrança
| Status | Significado | Final? |
|---|---|---|
pending |
Aguardando pagamento | não |
paid |
Pagamento confirmado na adquirente | quase: só pode virar refunded |
expired |
O prazo (expires_at) passou sem pagamento confirmado |
não: ainda pode virar paid |
refunded |
Pagamento estornado | sim |
failed |
A adquirente recusou ou falhou ao criar o PIX | sim |
Transições possíveis e o webhook de cada uma:
| De | Para | Webhook |
|---|---|---|
pending |
paid |
charge.paid |
pending |
expired |
charge.expired |
pending |
failed |
charge.failed |
expired |
paid |
charge.paid |
paid |
refunded |
charge.refunded |
Pontos de atenção:
- Pagamento depois de expirar existe. Um PIX pode ser pago no limite do prazo e a confirmação chegar depois. Nesse caso a cobrança vai de
expiredparapaide você recebecharge.paiddepois decharge.expired. Decida no seu sistema o que fazer (entregar o pedido ou pedir a devolução ao suporte). - A expiração é processada em segundo plano: uma cobrança pode continuar
pendingpor alguns segundos depois deexpires_at. - Só marcamos
paiddepois de confirmar o pagamento diretamente na adquirente. Um aviso de pagamento não confirmado nunca muda o status. - A API v1 não tem cancelamento nem estorno. Cobranças não pagas simplesmente expiram. Estornos são feitos pelo suporte e geram
charge.refunded.
Página de pagamento
Toda cobrança tem uma página pública pronta em payment_url (https://pay.amagicpay.tech/pay/{id}). Você pode redirecionar o cliente para ela ou mostrar o PIX dentro do seu próprio checkout.
A página:
- mostra o nome da sua loja, o valor, a descrição, o QR code, o copia e cola com botão "Copiar" e a contagem regressiva até
expires_at; - mostra do cliente apenas o primeiro nome;
- se atualiza sozinha a cada 3 segundos e exibe "Pagamento confirmado" quando a cobrança vira
paid; - se a cobrança tiver
success_url, leva o cliente para ela 3 segundos depois da confirmação; - avisa com clareza quando a cobrança expirou ou falhou;
- funciona bem no celular.
Para montar o PIX no seu checkout, use pix.qr_code (texto do copia e cola) e pix.qr_code_image_url (PNG que pode ir direto numa tag de imagem).
O success_url é só uma conveniência de navegação. Nunca libere um pedido porque o cliente chegou na success_url: qualquer pessoa pode abrir essa URL. Libere pelo webhook charge.paid validado ou pela consulta GET /api/v1/charges/{id}.
Webhooks
Os webhooks avisam o seu servidor quando uma cobrança muda de status, sem você precisar consultar a API.
Configuração
No painel, em Integração:
- Cadastre a URL de webhook (use
https://). - Gere o segredo de webhook. Ele aparece uma única vez: guarde no seu servidor. Sem segredo configurado, as entregas falham.
- Use Enviar webhook de teste para conferir se o seu endpoint recebe e valida a assinatura.
As entregas vão para a URL configurada no momento em que o evento acontece.
Eventos
| Evento | Quando |
|---|---|
charge.paid |
Pagamento confirmado na adquirente (inclusive depois de expirada) |
charge.expired |
O prazo passou sem pagamento |
charge.failed |
A adquirente recusou ou falhou ao criar o PIX |
charge.refunded |
Pagamento estornado |
test |
Disparado por você no painel. Responda 2xx e não aplique efeito em pedidos |
Cada evento é enviado no máximo uma vez por cobrança. As retentativas reenviam a mesma entrega, com o mesmo id.
Formato
POST para a sua URL, com este corpo:
{
"id": "whd_9c2h5k8n3r6t1w4y",
"event": "charge.paid",
"created_at": "2026-09-15T18:34:12.000Z",
"data": {
"id": "ch_7k3m9q2x4v6b8n1p5r0t2w4y",
"status": "paid",
"amount": 9990,
"currency": "BRL",
"description": "Chapinha Portátil",
"external_id": "pedido-123",
"acquirer": "podpay",
"customer": {
"name": "Maria Silva",
"document": "12345678909",
"document_type": "cpf",
"email": "maria@exemplo.com",
"phone": "84999999999"
},
"pix": {
"qr_code": "00020126580014br.gov.bcb.pix...",
"qr_code_image_url": "https://pay.amagicpay.tech/pay/ch_7k3m9q2x4v6b8n1p5r0t2w4y/qr",
"expires_at": "2026-09-15T19:30:00.000Z"
},
"payment_url": "https://pay.amagicpay.tech/pay/ch_7k3m9q2x4v6b8n1p5r0t2w4y",
"fee": 599,
"net_amount": 9391,
"metadata": { "utm_source": "meta" },
"paid_at": "2026-09-15T18:34:10.000Z",
"expires_at": "2026-09-15T19:30:00.000Z",
"created_at": "2026-09-15T18:30:00.000Z",
"updated_at": "2026-09-15T18:34:12.000Z"
}
}
| Campo | Descrição |
|---|---|
id |
Id da entrega (whd_...). Igual em todas as retentativas: use para não processar duas vezes |
event |
Nome do evento |
created_at |
Quando o evento foi gerado |
data |
O objeto cobrança no momento do evento |
Cabeçalhos
| Cabeçalho | Valor |
|---|---|
Content-Type |
application/json |
User-Agent |
MagicPay-Webhooks/1.0 |
X-MagicPay-Event |
Nome do evento, igual a event do corpo |
X-MagicPay-Delivery-Id |
Id da entrega, igual a id do corpo |
X-MagicPay-Timestamp |
Momento do envio desta tentativa, em segundos Unix |
X-MagicPay-Signature |
v1=<assinatura em hex> |
Os nomes dos cabeçalhos são técnicos e iguais para todos os gateways da plataforma.
Assinatura
A assinatura prova que a requisição veio do MagicPay e que o corpo não foi alterado:
assinatura = HMAC-SHA256(segredo, "<X-MagicPay-Timestamp>.<corpo bruto>"), em hex minúsculo
X-MagicPay-Signature: v1=<assinatura>
- segredo: o segredo de webhook exatamente como aparece no painel (64 caracteres hex), usado como texto. Não converta de hex para bytes.
- corpo bruto: o corpo exatamente como foi recebido. Valide antes de fazer o parse do JSON; reserializar o objeto muda espaços e ordem e quebra a assinatura.
- Compare em tempo constante (
crypto.timingSafeEqual), nunca com===. - Rejeite timestamps com mais de 5 minutos de diferença do seu relógio (proteção contra reenvio de requisições capturadas). Mantenha o relógio do servidor sincronizado (NTP).
- Cada tentativa é assinada na hora do envio, com timestamp novo, então as retentativas também passam na janela de 5 minutos.
- O cabeçalho pode trazer mais de uma assinatura separada por vírgula (
v1=abc,v1=def), por exemplo durante uma troca de segredo. Aceite se qualquer uma conferir.
Verificação em Node.js
Exemplo com Express, sem outras dependências:
import crypto from "node:crypto";
import express from "express";
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // segredo de webhook do painel
const TOLERANCE_SECONDS = 5 * 60;
export function verifyWebhook(rawBody, timestampHeader, signatureHeader, secret = WEBHOOK_SECRET) {
if (!secret || !timestampHeader || !signatureHeader) return false;
const timestamp = Number.parseInt(timestampHeader, 10);
if (!Number.isFinite(timestamp)) return false;
const nowSeconds = Math.floor(Date.now() / 1000);
if (Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) return false;
const digest = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const expected = Buffer.from(`v1=${digest}`, "utf8");
return signatureHeader
.split(",")
.map((part) => part.trim())
.some((candidate) => {
const received = Buffer.from(candidate, "utf8");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
// express.raw mantém o corpo como Buffer: a assinatura é conferida sobre o corpo exato.
app.post("/webhooks/pix", express.raw({ type: "application/json", limit: "1mb" }), async (req, res) => {
const rawBody = req.body.toString("utf8");
const ok = verifyWebhook(rawBody, req.get("X-MagicPay-Timestamp"), req.get("X-MagicPay-Signature"));
if (!ok) return res.status(400).json({ error: "assinatura inválida" });
const delivery = JSON.parse(rawBody);
// Responda rápido e processe de forma idempotente pelo id da entrega.
res.status(200).json({ received: true });
if (await jaProcessado(delivery.id)) return;
if (delivery.event === "charge.paid") {
await liberarPedido(delivery.data.external_id, delivery.data.amount);
}
await marcarProcessado(delivery.id);
});
app.listen(3000);
jaProcessado, marcarProcessado e liberarPedido são funções do seu sistema. Em produção, prefira gravar a entrega numa fila ou tabela antes de responder e processar depois.
Com Next.js (App Router), leia o corpo com await request.text(), passe esse texto para verifyWebhook e só então faça JSON.parse.
Entrega e retentativas
- Consideramos entregue qualquer resposta 2xx recebida em até 10 segundos. O conteúdo da resposta é ignorado.
- Redirecionamentos (3xx) não são seguidos e contam como falha. Cadastre a URL final.
- Qualquer outra resposta, timeout ou erro de conexão gera nova tentativa, até 10 tentativas no total:
| Tentativa | Quando |
|---|---|
| 1ª | Logo após o evento (em geral em segundos) |
| 2ª | 1 minuto após a falha anterior |
| 3ª | 5 minutos depois |
| 4ª | 15 minutos depois |
| 5ª | 1 hora depois |
| 6ª | 3 horas depois |
| 7ª | 6 horas depois |
| 8ª | 12 horas depois |
| 9ª | 24 horas depois |
| 10ª | 24 horas depois |
Depois da 10ª falha (cerca de 2 dias e 22 horas após a primeira), a entrega é marcada como falha definitiva. O suporte pode reenviá-la pelo painel, com o mesmo id.
Boas práticas:
- Responda 2xx rápido e processe em segundo plano.
- Seja idempotente: guarde o
idda entrega (ou o par cobrança + evento) e ignore repetições. - Não dependa da ordem de chegada. Um
charge.paidpode chegar depois de umcharge.expired. Usedata.statuse, na dúvida, consulteGET /api/v1/charges/{id}. - Confira o valor: compare
data.amountedata.external_idcom o seu pedido antes de liberar. - Mesmo com webhooks, tenha uma rotina que consulta pela API os pedidos pendentes antigos, para o caso de o seu endpoint ficar fora do ar por muito tempo.
Erros
Toda resposta de erro tem este formato:
{
"error": {
"code": "validation_error",
"message": "Dados inválidos.",
"details": [
{ "path": "customer.document", "message": "document inválido (dígito verificador não confere)" },
{ "path": "amount", "message": "amount precisa ser no mínimo 100 (R$ 1,00)" }
]
}
}
codeé estável: use no seu código.messageé em português e pode mudar: use só para exibir ou registrar.detailsaparece em erros de validação, com o caminho do campo empath.
| HTTP | code |
Significado | O que fazer |
|---|---|---|---|
| 400 | validation_error |
Corpo não é JSON, campo ausente ou inválido, parâmetro de listagem inválido, Idempotency-Key inválida |
Corrija a requisição. Não repita igual |
| 401 | unauthorized |
Chave ausente, malformada, inexistente ou rotacionada, ou conta desativada | Confira o cabeçalho e a chave no painel |
| 401 | merchant_not_approved |
Conta ainda não aprovada | Conclua o cadastro e aguarde a análise |
| 401 | merchant_suspended |
Conta suspensa | Fale com o suporte |
| 401 | gateway_suspended |
Gateway suspenso | Fale com o suporte |
| 404 | not_found |
Cobrança inexistente ou de outra conta, ou rota inexistente | Confira o id e o caminho |
| 409 | idempotency_conflict |
Idempotency-Key já usada com outro corpo |
Use outra chave ou reenvie o corpo original |
| 429 | rate_limited |
Limite de requisições por minuto excedido | Espere os segundos indicados no cabeçalho Retry-After |
| 500 | internal_error |
Erro inesperado | Tente de novo com a mesma Idempotency-Key; se persistir, fale com o suporte |
| 502 | provider_error |
Falha ao falar com a adquirente | Na criação, tente de novo com uma nova Idempotency-Key. No sync, tente mais tarde |
| 503 | no_acquirer |
Gateway sem adquirente ativa | Fale com o suporte |
Um método HTTP não suportado numa rota existente (por exemplo DELETE /api/v1/charges) responde 405 sem corpo.
Limites
| Limite | Valor |
|---|---|
| Requisições por chave de API | 60 por minuto (janela deslizante). Acima disso, 429 com Retry-After em segundos |
| Sync por cobrança | 1 consulta à adquirente a cada 5 segundos |
amount |
100 a 50.000.000 centavos (R$ 1,00 a R$ 500.000,00) |
expires_in |
300 a 259.200 segundos (5 minutos a 72 horas); padrão 3.600 |
description |
120 caracteres |
external_id |
80 caracteres |
customer.name |
2 a 120 caracteres |
customer.email |
160 caracteres |
metadata |
2 KB em JSON |
success_url |
https://, até 2.048 caracteres |
Idempotency-Key |
128 caracteres |
limit na listagem |
1 a 100; padrão 20 |
| Corpo da requisição | 1 MB |
| Tempo de resposta do seu webhook | 10 segundos |
| Tentativas de entrega de webhook | 10 |
Checklist antes de ir para produção
- A chave de API está só no servidor, em variável de ambiente ou cofre de segredos.
-
GET /api/v1/memostra"env": "production". - Toda criação de cobrança envia
Idempotency-Key. - O endpoint de webhook valida a assinatura sobre o corpo bruto, com
timingSafeEquale tolerância de 5 minutos. - O processamento do webhook é idempotente pelo
idda entrega. - O pedido só é liberado com
charge.paidvalidado (ou consulta à API), nunca pelasuccess_url. - O sistema trata
charge.paidrecebido depois decharge.expired. - O valor e o
external_idsão conferidos antes de liberar o pedido. - Respostas
429respeitam oRetry-Aftere respostas5xxsão registradas.