Introdução
A API do DePix App permite criar e gerenciar checkouts Pix programaticamente. É a mesma API que alimenta o plugin do BTCPay Server e a Área do Lojista.
URL base
https://depix-backend.vercel.app
Formato
Todos os requests e responses usam JSON (Content-Type: application/json). Valores monetários são sempre em centavos (inteiros). Exemplo: R$ 10,00 = 1000.
Autenticação
Use uma API key no header Authorization em todos os requests autenticados.
Authorization: Bearer sk_live_<sua-chave>
Tipos de chave
| Prefixo | Tipo | Comportamento |
|---|---|---|
| sk_live_ | Live | Checkouts reais. Dinheiro de verdade. |
| sk_test_ | Test | Checkouts de teste. Nenhum dinheiro real é movimentado. |
Para gerenciar suas chaves (criar, listar, revogar), acesse a Área do Lojista em depixapp.com/#merchant. Máximo de 5 chaves live e 5 chaves test ativas por conta.
Erros
Erros seguem o formato abaixo. O campo errorMessage é sempre legível para humanos.
{
"response": {
"errorMessage": "Dados inválidos.",
"errors": [ // presente em erros de validação
{ "field": "amount", "message": "Obrigatório." }
]
}
}
| Status | Significado |
|---|---|
| 400 | Dados inválidos — verifique os campos. |
| 401 | API key ausente, inválida ou revogada. |
| 403 | Sem permissão. Conta de lojista não configurada, chave sem acesso ao recurso, ou WhatsApp do lojista não verificado quando o operador exigir verificação ("Verifique seu WhatsApp para criar checkouts."). Verifique seu WhatsApp na área do lojista no DePix App. |
| 404 | Recurso não encontrado. |
| 409 | Conflito de estado — operação não permitida no status atual. |
| 429 | Rate limit excedido. Aguarde e tente novamente. |
| 500 | Erro interno no servidor. Tente novamente em alguns instantes. |
| 503 | Serviço indisponível — plataforma em manutenção ou provedor Pix temporariamente fora do ar. Em links públicos de checkout (/api/merchants/:username/checkout e /api/products/:id/checkout), também retornado quando o lojista do link ainda não verificou o WhatsApp e o operador exigir verificação. |
Criar checkout
Cria um novo checkout Pix. Retorna o QR code e a URL de pagamento para exibir ao seu cliente.
Parâmetros
| Campo | Tipo | Descrição | |
|---|---|---|---|
| amount | integer | obrigatório | Valor em centavos. Mínimo: 500 (R$ 5,00). Máximo: 300000 (R$ 3.000,00). |
| description | string | opcional | Descrição do pedido. Máximo 500 caracteres. Exibida na página de pagamento. |
| expires_in | integer | opcional | Tempo de expiração em segundos. Padrão: 1200 (20min). Mínimo: 300 (5min). Máximo: 1200 (20min). |
| image_url | string | opcional | URL HTTPS da imagem do produto. Exibida na página de pagamento. |
| callback_url | string | opcional | URL HTTPS que recebe os webhooks do checkout. |
| redirect_url | string | opcional | URL para redirecionar o cliente após o pagamento. |
| metadata | object | opcional | Dados adicionais do seu sistema (order_id, user_id, etc.). Máximo 4KB. Devolvido nos webhooks. |
Exemplo
curl -X POST https://depix-backend.vercel.app/api/checkouts \ -H "Authorization: Bearer sk_live_<sua-chave>" \ -H "Content-Type: application/json" \ -d '{ "amount": 2990, "description": "Camiseta tamanho M", "expires_in": 900, "callback_url": "https://minha-loja.com/webhook/depix", "metadata": { "order_id": "ORD-123" } }'
const res = await fetch("https://depix-backend.vercel.app/api/checkouts", { method: "POST", headers: { "Authorization": "Bearer sk_live_<sua-chave>", "Content-Type": "application/json", }, body: JSON.stringify({ amount: 2990, description: "Camiseta tamanho M", expires_in: 900, callback_url: "https://minha-loja.com/webhook/depix", metadata: { order_id: "ORD-123" }, }), }); const data = await res.json(); console.log(data.id, data.payment_url);
import requests resp = requests.post( "https://depix-backend.vercel.app/api/checkouts", headers={"Authorization": "Bearer sk_live_<sua-chave>"}, json={ "amount": 2990, "description": "Camiseta tamanho M", "expires_in": 900, "callback_url": "https://minha-loja.com/webhook/depix", "metadata": {"order_id": "ORD-123"}, }, ) data = resp.json() print(data["id"], data["payment_url"])
$ch = curl_init("https://depix-backend.vercel.app/api/checkouts"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer sk_live_<sua-chave>", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "amount" => 2990, "description" => "Camiseta tamanho M", "expires_in" => 900, "callback_url" => "https://minha-loja.com/webhook/depix", "metadata" => ["order_id" => "ORD-123"], ]), ]); $response = curl_exec($ch); $data = json_decode($response, true); echo $data["id"] . " " . $data["payment_url"];
using var client = new HttpClient(); client.DefaultRequestHeaders.Add("Authorization", "Bearer sk_live_<sua-chave>"); var payload = new { amount = 2990, description = "Camiseta tamanho M", expires_in = 900, callback_url = "https://minha-loja.com/webhook/depix", metadata = new { order_id = "ORD-123" } }; var res = await client.PostAsync( "https://depix-backend.vercel.app/api/checkouts", new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") ); var json = await res.Content.ReadAsStringAsync(); Console.WriteLine(json);
body := `{"amount":2990,"description":"Camiseta tamanho M","expires_in":900,"callback_url":"https://minha-loja.com/webhook/depix","metadata":{"order_id":"ORD-123"}}` req, _ := http.NewRequest("POST", "https://depix-backend.vercel.app/api/checkouts", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer sk_live_<sua-chave>") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() io.Copy(os.Stdout, resp.Body)
require "net/http" require "json" uri = URI("https://depix-backend.vercel.app/api/checkouts") req = Net::HTTP::Post.new(uri, { "Authorization" => "Bearer sk_live_<sua-chave>", "Content-Type" => "application/json", }) req.body = { amount: 2990, description: "Camiseta tamanho M", expires_in: 900, callback_url: "https://minha-loja.com/webhook/depix", metadata: { order_id: "ORD-123" } }.to_json res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } puts JSON.parse(res.body)
HttpClient client = HttpClient.newHttpClient(); String json = """ {"amount":2990,"description":"Camiseta tamanho M","expires_in":900, "callback_url":"https://minha-loja.com/webhook/depix", "metadata":{"order_id":"ORD-123"}}"""; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://depix-backend.vercel.app/api/checkouts")) .header("Authorization", "Bearer sk_live_<sua-chave>") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());
{
"id": "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
"status": "pending",
"amount": 2990,
"description": "Camiseta tamanho M",
"image_url": null,
"expires_at": "2025-06-01T15:30:00.000Z",
"is_live": true,
"payment_url": "https://pay.depixapp.com/chk_01jxxxxxxxxxxxxxxxxxxxxxx",
"pix": {
"qr_code": "00020126580014br.gov.bcb.pix..." // payload EMV para QR code
}
}
payment_url ao seu cliente ou gere um QR code a partir do pix.qr_code. O QR code é compatível com qualquer app de banco.
Consultar checkout
Retorna os detalhes de um checkout específico.
curl https://depix-backend.vercel.app/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx \ -H "Authorization: Bearer sk_live_<sua-chave>"
{
"checkout": {
"id": "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
"status": "completed", // pending | processing | completed | cancelled | expired
"amount": 2990,
"description": "Camiseta tamanho M",
"image_url": null,
"callback_url": "https://minha-loja.com/webhook/depix",
"redirect_url": null,
"metadata": { "order_id": "ORD-123" },
"expires_at": "2025-06-01T15:30:00.000Z",
"is_live": true,
"created_at": "2025-06-01T15:00:00.000Z",
"processing_at": "2025-06-01T15:02:00.000Z",
"completed_at": "2025-06-01T15:22:00.000Z",
"cancelled_at": null,
"blockchain_tx_id": "abc123...def456" // txid Liquid (presente quando completed)
}
}
Status possíveis
| Status | Significado |
|---|---|
| pending | Aguardando pagamento. |
| processing | Pix recebido, processando conversão para DePix. |
| completed | Pagamento confirmado. DePix na carteira do merchant. |
| cancelled | Cancelado pelo merchant. |
| expired | Prazo de pagamento expirou. |
Listar checkouts
Lista os checkouts do merchant com filtros e paginação.
Query params (todos opcionais)
| Parâmetro | Descrição |
|---|---|
| status | Filtrar por status: pending, completed, cancelled, expired. |
| product_id | Filtrar por produto. Ex: prd_xxx. |
| from | Data de início (ISO 8601). Ex: 2025-06-01T00:00:00Z. |
| to | Data de fim (ISO 8601). |
| q | Busca por ID ou descrição. |
| limit | Número de resultados por página. Padrão: 50. Máximo: 100. |
| offset | Paginação. Padrão: 0. |
curl "https://depix-backend.vercel.app/api/checkouts?status=completed&limit=20" \ -H "Authorization: Bearer sk_live_<sua-chave>"
{
"checkouts": [
{
"id": "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
"status": "completed",
"amount": 2990,
"description": "Camiseta tamanho M",
"product_name": "Camiseta Preta", // null se checkout não vier de um produto
"metadata": "{\"order_id\":\"42\"}", // string JSON, null se ausente
"created_at": "2025-06-01T15:00:00.000Z",
"processing_at": "2025-06-01T15:02:14.000Z",
"expires_at": "2025-06-01T15:30:00.000Z",
"is_live": true
}
],
"stats": {
"total": 47,
"pending": 2,
"completed": 40,
"completed_amount": 189500 // centavos — R$ 1.895,00
},
"limit": 20,
"offset": 0
}
Cancelar checkout
Cancela um checkout pendente. Só é possível cancelar checkouts com status pending.
curl -X POST https://depix-backend.vercel.app/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx/cancel \ -H "Authorization: Bearer sk_live_<sua-chave>"
{ "success": true }
Criar produto
Cria um novo produto com valor fixo. Cada produto gera um link de pagamento permanente que pode ser compartilhado com seus clientes.
Parâmetros
| Campo | Tipo | Descrição | |
|---|---|---|---|
| name | string | obrigatório | Nome do produto exibido na UI. 2-80 caracteres. |
| slug | string | opcional | Identificador na URL. Se omitido, é gerado automaticamente a partir do name. Letras minúsculas, números e hífens. 2-60 caracteres. Não pode começar/terminar com hífen. |
| amount | integer | obrigatório | Valor em centavos. Mínimo: 500. Máximo: 300000. |
| description | string | opcional | Descrição do produto. Máximo 500 caracteres. |
| image_url | string | opcional | URL HTTPS da imagem do produto. |
| callback_url | string | opcional | URL HTTPS para webhooks. Sobrescreve o default do merchant. |
| redirect_url | string | opcional | URL de redirecionamento. Sobrescreve o default do merchant. |
| metadata | object | opcional | Dados adicionais. Máximo 4KB. Incluído nos webhooks dos checkouts gerados. |
| expires_in | integer | opcional | Tempo de expiração dos checkouts em segundos. Padrão: 1200 (20min). Mínimo: 300 (5min). Máximo: 1200 (20min). |
Exemplo
curl -X POST https://depix-backend.vercel.app/api/products \ -H "Authorization: Bearer sk_live_<sua-chave>" \ -H "Content-Type: application/json" \ -d '{ "name": "Camiseta M", "slug": "camiseta-m", "amount": 2990, "description": "Camiseta tamanho M" }'
const res = await fetch("https://depix-backend.vercel.app/api/products", { method: "POST", headers: { "Authorization": "Bearer sk_live_<sua-chave>", "Content-Type": "application/json", }, body: JSON.stringify({ name: "Camiseta M", slug: "camiseta-m", amount: 2990, description: "Camiseta tamanho M", }), }); const data = await res.json(); console.log(data.product.payment_url);
import requests resp = requests.post( "https://depix-backend.vercel.app/api/products", headers={"Authorization": "Bearer sk_live_<sua-chave>"}, json={ "name": "Camiseta M", "slug": "camiseta-m", "amount": 2990, "description": "Camiseta tamanho M", }, ) data = resp.json() print(data["product"]["payment_url"])
$ch = curl_init("https://depix-backend.vercel.app/api/products"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer sk_live_<sua-chave>", "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "name" => "Camiseta M", "slug" => "camiseta-m", "amount" => 2990, "description" => "Camiseta tamanho M", ]), ]); $response = curl_exec($ch); $data = json_decode($response, true); echo $data["product"]["payment_url"];
using var client = new HttpClient(); client.DefaultRequestHeaders.Add("Authorization", "Bearer sk_live_<sua-chave>"); var payload = new { name = "Camiseta M", slug = "camiseta-m", amount = 2990, description = "Camiseta tamanho M" }; var res = await client.PostAsync( "https://depix-backend.vercel.app/api/products", new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") ); Console.WriteLine(await res.Content.ReadAsStringAsync());
body := `{"name":"Camiseta M","slug":"camiseta-m","amount":2990,"description":"Camiseta tamanho M"}` req, _ := http.NewRequest("POST", "https://depix-backend.vercel.app/api/products", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer sk_live_<sua-chave>") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() io.Copy(os.Stdout, resp.Body)
require "net/http" require "json" uri = URI("https://depix-backend.vercel.app/api/products") req = Net::HTTP::Post.new(uri, { "Authorization" => "Bearer sk_live_<sua-chave>", "Content-Type" => "application/json", }) req.body = { name: "Camiseta M", slug: "camiseta-m", amount: 2990, description: "Camiseta tamanho M" }.to_json res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } puts JSON.parse(res.body)
HttpClient client = HttpClient.newHttpClient(); String json = """ {"name":"Camiseta M","slug":"camiseta-m","amount":2990,"description":"Camiseta tamanho M"}"""; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://depix-backend.vercel.app/api/products")) .header("Authorization", "Bearer sk_live_<sua-chave>") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());
{
"product": {
"id": "prd_xxx",
"name": "Camiseta M",
"slug": "camiseta-m",
"amount": 2990,
"description": "Camiseta tamanho M",
"image_url": null,
"callback_url": null,
"redirect_url": null,
"metadata": null,
"expires_in": 1200,
"active": true,
"is_live": true,
"payment_url": "https://pay.depixapp.com/joao/camiseta-m"
}
}
Listar produtos
Lista os produtos do merchant com filtros e paginação.
Query params (todos opcionais)
| Parâmetro | Descrição |
|---|---|
| active | Filtrar por status: 1 (ativos) ou 0 (inativos). |
| q | Busca por nome, slug ou descrição. |
| limit | Número de resultados. Padrão: 50. Máximo: 100. |
| offset | Paginação. Padrão: 0. |
curl "https://depix-backend.vercel.app/api/products?active=1" \ -H "Authorization: Bearer sk_live_<sua-chave>"
{
"products": [
{
"id": "prd_xxx",
"name": "Camiseta M",
"slug": "camiseta-m",
"amount": 2990,
"description": "Camiseta tamanho M",
"active": true,
"is_live": true,
"payment_url": "https://pay.depixapp.com/joao/camiseta-m"
}
],
"stats": {
"total": 5,
"active": 4
},
"limit": 50,
"offset": 0
}
Consultar produto
Retorna os detalhes de um produto específico, incluindo estatísticas de checkouts.
curl https://depix-backend.vercel.app/api/products/prd_xxx \ -H "Authorization: Bearer sk_live_<sua-chave>"
{
"product": {
"id": "prd_xxx",
"name": "Camiseta M",
"slug": "camiseta-m",
"amount": 2990,
"description": "Camiseta tamanho M",
"image_url": null,
"callback_url": null,
"redirect_url": null,
"metadata": null,
"expires_in": 1200,
"active": true,
"is_live": true,
"payment_url": "https://pay.depixapp.com/joao/camiseta-m",
"created_at": "2025-06-01T00:00:00.000Z"
}
}
Editar produto
Atualiza um ou mais campos de um produto existente. Envie apenas os campos que deseja alterar.
Parâmetros (todos opcionais)
| Campo | Tipo | Descrição |
|---|---|---|
| name | string | Novo nome do produto. 2-80 caracteres. |
| slug | string | Novo identificador na URL. Mesmas regras da criação. |
| amount | integer | Novo valor em centavos. |
| description | string | Nova descrição. |
| image_url | string | Nova URL de imagem. |
| callback_url | string | Nova URL de webhook. |
| redirect_url | string | Nova URL de redirecionamento. |
| metadata | object | Novos dados adicionais. |
| expires_in | integer | Novo tempo de expiração dos checkouts. Mínimo: 300 (5min). Máximo: 1200 (20min). |
curl -X PATCH https://depix-backend.vercel.app/api/products/prd_xxx \ -H "Authorization: Bearer sk_live_<sua-chave>" \ -H "Content-Type: application/json" \ -d '{ "amount": 3490, "description": "Camiseta tamanho M - Edição Especial" }'
{
"product": {
"id": "prd_xxx",
"name": "Camiseta M",
"slug": "camiseta-m",
"amount": 3490,
"description": "Camiseta tamanho M - Edição Especial",
"active": true,
"is_live": true,
"payment_url": "https://pay.depixapp.com/joao/camiseta-m"
}
}
Ativar / Desativar produto
Ativa ou desativa um produto. Produtos inativos retornam erro 404 quando acessados pelo link de pagamento.
curl -X POST https://depix-backend.vercel.app/api/products/prd_xxx/activate \ -H "Authorization: Bearer sk_live_<sua-chave>"
curl -X POST https://depix-backend.vercel.app/api/products/prd_xxx/deactivate \ -H "Authorization: Bearer sk_live_<sua-chave>"
{ "success": true }
Checkouts do produto
Lista os checkouts gerados a partir de um produto específico. Aceita os mesmos filtros da listagem geral de checkouts.
curl "https://depix-backend.vercel.app/api/products/prd_xxx/checkouts?status=completed" \ -H "Authorization: Bearer sk_live_<sua-chave>"
A resposta segue o mesmo formato da listagem de checkouts.
Links de pagamento
O DePix App gera links de pagamento permanentes para produtos e para a página do merchant. Esses links criam checkouts sob demanda quando o cliente acessa.
Tipos de link
| Tipo | URL | Comportamento |
|---|---|---|
| Produto | https://pay.depixapp.com/{merchant_slug}/{slug} | Valor fixo. O cliente vê o produto e clica "Pagar com PIX". |
| Merchant | https://pay.depixapp.com/{merchant_slug} | Valor livre. O cliente digita o valor e clica "Pagar com PIX". |
/api/merchants/:username/public e /api/products/:id/public (o segmento :username na URL é, na prática, o merchant_slug — o nome do parâmetro foi mantido por compatibilidade). Quando o lojista altera o nome do negócio, a slug é regenerada — links antigos enviados a clientes deixam de funcionar e precisam ser reenviados.
Ciclo de vida
- Quando o cliente acessa o link e inicia o pagamento, um checkout individual é criado automaticamente.
- A partir daí, o ciclo de vida é idêntico a um checkout criado via API (status, webhooks, expiração).
- O
callback_urlsegue a cadeia: campo do produto (se houver) → default do merchant → null. - O
redirect_urlsegue a mesma cadeia.
Produto público
Retorna os dados públicos de um produto ativo. Não requer autenticação.
curl https://depix-backend.vercel.app/api/products/prd_xxx/public
{
"product": {
"id": "prd_xxx",
"name": "Camiseta M",
"slug": "camiseta-m",
"amount": 2990,
"description": "Camiseta tamanho M",
"image_url": null
},
"merchant": {
"name": "Loja do Joao",
"merchant_slug": "joao",
"username": "joao"
}
}
Checkout do produto
Cria um checkout a partir de um produto ativo. Não requer autenticação. O valor é herdado do produto.
curl -X POST https://depix-backend.vercel.app/api/products/prd_xxx/checkout
A resposta segue o mesmo formato do criar checkout (status 201).
Página do merchant
Retorna os dados públicos do merchant. Não requer autenticação.
curl https://depix-backend.vercel.app/api/merchants/joao/public
{
"merchant": {
"name": "Loja do Joao",
"merchant_slug": "joao",
"username": "joao"
}
}
Checkout do merchant
Cria um checkout com valor customizado a partir da página do merchant. Não requer autenticação.
Parâmetros
| Campo | Tipo | Descrição | |
|---|---|---|---|
| amount | integer | obrigatório | Valor em centavos. Mínimo: 500. Máximo: 300000. |
curl -X POST https://depix-backend.vercel.app/api/merchants/joao/checkout \ -H "Content-Type: application/json" \ -d '{ "amount": 5000 }'
A resposta segue o mesmo formato do criar checkout (status 201).
Webhooks
Quando o status de um checkout muda, a API envia um POST para o callback_url que você informou ao criar o checkout (ou configurado no produto/merchant).
Como funciona
- O request é enviado com timeout de 30 segundos.
- Se falhar (resposta não-2xx, timeout ou erro de rede), a API tenta novamente até 5 vezes: após 1 minuto, 10 minutos, 1 hora, 4 horas e 12 horas (6 tentativas no total, cobrindo cerca de 17 horas).
- Sua endpoint deve responder com status 2xx para confirmar recebimento.
- O
callback_urlprecisa ser HTTPS e de acesso público (sem IPs privados).
Headers enviados
X-DePix-Signature— assinatura HMAC-SHA256 (ver seção Verificar assinatura).X-DePix-Event— nome do evento (ex:checkout.completed).X-DePix-Event-Id— identificador único e estável entre tentativas deste evento (ex:evt_abc123…). É a chave recomendada para deduplicação.X-DePix-Delivery-Attempt— número da tentativa atual (1, 2, … até 6). Muda a cada retry; não use para dedupe.User-Agent— sempreDePix-Webhook/1.0.
Entrega at-least-once e idempotência (obrigatório)
Webhooks são entregues com semântica at-least-once — é o padrão do mercado (Stripe, PayPal, Mercado Pago funcionam da mesma forma). Isso significa que o mesmo evento pode chegar ao seu endpoint mais de uma vez, mesmo que tudo esteja funcionando corretamente. Cenários comuns:
- Seu servidor processa o webhook mas responde lentamente — nossa API faz timeout em 30s, marca como falha e tenta novamente; você processa duas vezes.
- Seu servidor responde 200 mas a conexão cai antes da gente ler a resposta — mesma coisa: retry e processamento duplicado.
- O time de operações reenvia manualmente um evento (via comando administrativo) que você já processou.
Para evitar entregar produto duas vezes, creditar saldo em duplicidade, ou disparar acionamentos múltiplos, seu endpoint precisa ser idempotente. A forma mais simples e robusta é deduplicar por X-DePix-Event-Id: guarde os IDs de eventos já processados e ignore silenciosamente qualquer evento cujo ID já esteja na sua tabela.
// Exemplo de dedupe (Node.js, pseudo-código) app.post("/webhook", async (req, res) => { // 1. Valide a assinatura HMAC primeiro (ver seção Verificar assinatura). const eventId = req.headers["x-depix-event-id"]; // 2. Processe E marque como processado dentro da MESMA transação — se // processCheckout falhar, o INSERT também é revertido e o nosso retry // consegue entregar o evento novamente. try { await db.transaction(async (tx) => { await tx.query( "INSERT INTO processed_webhooks (event_id, received_at) VALUES (?, NOW())", [eventId] ); await processCheckout(req.body, tx); }); } catch (err) { if (err.code === "ER_DUP_ENTRY") { // Já processamos antes — responde 200 e ignora. return res.sendStatus(200); } throw err; // Deixa nossa API tentar novamente. } res.sendStatus(200); });
Se preferir não manter uma tabela separada, você também pode deduplicar pelo campo event_id dentro do JSON (data.event_id) — ele carrega o mesmo valor estável do header X-DePix-Event-Id. Não use (data.id, event) como chave de dedupe: um reenvio manual feito pela equipe de operações reusa o mesmo id de checkout e o mesmo nome de evento, então uma chave baseada nessa tupla descartaria silenciosamente o reenvio.
Reduzindo retries
Para evitar entregas duplicadas no caminho feliz, responda o mais rápido possível — alvos comuns ficam abaixo de poucos segundos, para deixar folga para a latência de rede antes do nosso timeout de 30s. O padrão recomendado é: validar a assinatura, responder 200 imediatamente e processar o evento em background (fila, worker, etc.). Isso evita retries causados por timeout do nosso lado.
Eventos
checkout.processing
Disparado quando o pagamento Pix é recebido e a conversão está sendo processada.
{
"event": "checkout.processing",
"data": {
"event_id": "evt_01jxxxxxxxxxxxxxxxxxxxxxx",
"id": "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
"status": "processing",
"amount": 2990,
"processing_at": "2025-06-01T15:02:00.000Z",
"metadata": { "order_id": "ORD-123" }
}
}
checkout.completed
Disparado quando o pagamento é confirmado e o DePix chega na carteira do merchant.
{
"event": "checkout.completed",
"data": {
"event_id": "evt_01jxxxxxxxxxxxxxxxxxxxxxx",
"id": "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
"status": "completed",
"amount": 2990,
"completed_at": "2025-06-01T15:22:00.000Z",
"metadata": { "order_id": "ORD-123" }
}
}
checkout.cancelled
Disparado quando o merchant cancela o checkout via API.
{
"event": "checkout.cancelled",
"data": {
"event_id": "evt_01jxxxxxxxxxxxxxxxxxxxxxx",
"id": "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
"status": "cancelled",
"amount": 2990,
"cancelled_at": "2025-06-01T15:05:00.000Z",
"metadata": { "order_id": "ORD-123" }
}
}
checkout.expired
Disparado quando o checkout expira sem receber pagamento.
{
"event": "checkout.expired",
"data": {
"event_id": "evt_01jxxxxxxxxxxxxxxxxxxxxxx",
"id": "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
"status": "expired",
"amount": 2990,
"expires_at": "2025-06-01T15:30:00.000Z",
"metadata": { "order_id": "ORD-123" }
}
}
Verificar assinatura
Cada webhook vem com um header X-DePix-Signature. Sempre valide a assinatura antes de processar o evento — isso garante que o request veio da API do DePix App e não de terceiros.
Formato do header
X-DePix-Signature: t=1717257600,v1=abc123def456...
- t — timestamp Unix do envio (segundos).
- v1 — assinatura HMAC-SHA256 em hexadecimal.
Como validar
A assinatura é calculada sobre a string timestamp.payload usando o Webhook Secret da sua conta (disponível na Área do Lojista).
# Calcular a assinatura esperada EXPECTED=$(echo -n "${TIMESTAMP}.${RAW_BODY}" | \ openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" | awk '{print $2}') # Comparar com o v1 recebido if [ "$EXPECTED" = "$RECEIVED_V1" ]; then echo "Assinatura válida" fi
import crypto from "node:crypto"; function verifyWebhook(rawBody, sigHeader, secret) { const parts = Object.fromEntries( sigHeader.split(",").map(p => p.split("=", 2)) ); const timestamp = parts["t"]; const received = parts["v1"]; const expected = crypto .createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest("hex"); // Use timingSafeEqual to prevent timing attacks const a = Buffer.from(expected, "hex"); const b = Buffer.from(received, "hex"); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b); } // Exemplo com Express app.post("/webhook/depix", express.raw({ type: "application/json" }), (req, res) => { const sig = req.headers["x-depix-signature"]; if (!verifyWebhook(req.body.toString(), sig, process.env.DEPIX_WEBHOOK_SECRET)) { return res.status(401).send("Assinatura inválida"); } const { event, data } = JSON.parse(req.body); // processa o evento... res.sendStatus(200); });
import hmac, hashlib def verify_webhook(raw_body: str, sig_header: str, secret: str) -> bool: parts = dict(p.split("=", 1) for p in sig_header.split(",")) timestamp = parts["t"] received = parts["v1"] expected = hmac.new( secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, received)
function verifyWebhook(string $rawBody, string $sigHeader, string $secret): bool { $parts = []; foreach (explode(",", $sigHeader) as $pair) { [$k, $v] = explode("=", $pair, 2); $parts[$k] = $v; } $expected = hash_hmac("sha256", $parts["t"] . "." . $rawBody, $secret); return hash_equals($expected, $parts["v1"]); }
static bool VerifyWebhook(string rawBody, string sigHeader, string secret) { var parts = sigHeader.Split(',') .ToDictionary(p => p.Split('=', 2)[0], p => p.Split('=', 2)[1]); using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var expected = Convert.ToHexString( hmac.ComputeHash(Encoding.UTF8.GetBytes($"{parts["t"]}.{rawBody}")) ).ToLower(); return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(parts["v1"]) ); }
func verifyWebhook(rawBody, sigHeader, secret string) bool { parts := make(map[string]string) for _, p := range strings.Split(sigHeader, ",") { kv := strings.SplitN(p, "=", 2) parts[kv[0]] = kv[1] } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(parts["t"] + "." + rawBody)) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(parts["v1"])) }
def verify_webhook(raw_body, sig_header, secret) parts = sig_header.split(",").to_h { |p| p.split("=", 2) } expected = OpenSSL::HMAC.hexdigest("sha256", secret, "#{parts['t']}.#{raw_body}") Rack::Utils.secure_compare(expected, parts["v1"]) end
static boolean verifyWebhook(String rawBody, String sigHeader, String secret) throws Exception { Map<String, String> parts = new HashMap<>(); for (String p : sigHeader.split(",")) { String[] kv = p.split("=", 2); parts.put(kv[0], kv[1]); } Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256")); String expected = HexFormat.of().formatHex( mac.doFinal((parts.get("t") + "." + rawBody).getBytes()) ); return MessageDigest.isEqual(expected.getBytes(), parts.get("v1").getBytes()); }
Sandbox
Use chaves do tipo sk_test_... para testar sem movimentar dinheiro real. Checkouts criados com chave test nunca geram Pix real e ficam isolados dos checkouts de produção.
Diferenças do modo test
- O campo
is_liveretornafalse. - O QR code gerado não é um Pix válido — não pode ser pago num app de banco.
- Use o endpoint
/simulate-paymentpara marcar o checkout como pago. - Webhooks são enviados normalmente — ótimo para testar sua integração de ponta a ponta.
Simular pagamento
Marca um checkout de teste como pago. Só funciona com chaves sk_test_. Dispara o webhook checkout.completed normalmente.
# 1. Crie um checkout de teste curl -X POST https://depix-backend.vercel.app/api/checkouts \ -H "Authorization: Bearer sk_test_<sua-chave>" \ -H "Content-Type: application/json" \ -d '{ "amount": 1000, "callback_url": "https://minha-loja.com/webhook" }' # 2. Simule o pagamento curl -X POST https://depix-backend.vercel.app/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx/simulate-payment \ -H "Authorization: Bearer sk_test_<sua-chave>"
{ "success": true }
Após a simulação, seu callback_url receberá o evento checkout.completed em alguns segundos — exatamente como num pagamento real.
Verificar chave (GET /api/me)
Retorna as informações do merchant autenticado. Útil para verificar se a API key é válida e consultar dados da conta.
curl https://depix-backend.vercel.app/api/me \ -H "Authorization: Bearer sk_live_<sua-chave>"
{
"merchant_id": "mrc_xxx",
"name": "Loja do Joao",
"username": "joao",
"merchant_slug": "joao",
"is_live": true,
"created_at": "2025-06-01T00:00:00.000Z"
}
Rate limits
A API aplica limites de requisições para garantir estabilidade e proteger contra abusos.
| Endpoint | Limite | Escopo |
|---|---|---|
| POST /api/checkouts | 30 / min | por IP |
| GET /api/checkout-page/:id | 30 / min | por IP (público) |
| GET /api/pay/:id | 60 / min | por IP (público) |
| POST /api/pay/:id/simulate | 5 / min | por IP (público, só sandbox) |
| POST /api/merchants/:username/checkout | 10 / min | por IP (público) |
| POST /api/products/:id/checkout | 10 / min | por IP (público) |
| GET /api/products/:id/public | 30 / min | por IP (público) |
| GET /api/merchants/:username/public | 30 / min | por IP (público) |
| Por merchant (API key) | Configurável | aplicado após auth, em cima do limite por IP |
- Para requests autenticados com API key, um rate limit adicional é aplicado por merchant (configurável — fale com o suporte se precisar de aumento).
- Para endpoints públicos (sem auth), o rate limit é aplicado apenas por IP.
- Quando o limite é atingido, a API retorna status
429com a mensagem"Muitas requisições. Tente novamente em 1 minuto."— aguarde ~60s antes de tentar de novo.