Introduction

The DePix App API lets you create and manage Pix checkouts programmatically. It is the same API that powers the BTCPay Server plugin and the Merchant Dashboard.

Base URL

https://depix-backend.vercel.app

Format

All requests and responses use JSON (Content-Type: application/json). Monetary values are always in centavos (integers). Example: R$ 10.00 = 1000.

To get started, create an account, activate your merchant account in the Merchant Dashboard, and generate an API key.

Authentication

Use an API key in the Authorization header on all authenticated requests.

Header
Authorization: Bearer sk_live_<your-key>

Key types

PrefixTypeBehavior
sk_live_LiveReal checkouts. Real money.
sk_test_TestTest checkouts. No real money is moved.

To manage your keys (create, list, revoke), go to the Merchant Dashboard at depixapp.com/#merchant. Maximum of 5 live keys and 5 test keys active per account.

Production API access

sk_test_ keys are issued automatically once you create your merchant account — start integrating against the sandbox without waiting. sk_live_ keys require manual approval: in the API Keys area click Solicitar acesso (Request access), answer 5 short questions about your integration, and our team reviews. Approvals typically land within a few hours on business days.

Keep your key safe. It is only displayed once at creation time. If you lose it, revoke it and generate a new one.

Errors

Errors follow the format below. The errorMessage field is always human-readable.

Error response
{
  "response": {
    "errorMessage": "Invalid data.",
    "errors": [                         // present on validation errors
      { "field": "amount", "message": "Required." }
    ]
  }
}
StatusMeaning
400Invalid data — check the fields.
401API key missing, invalid, or revoked.
403Permission denied. Merchant account not set up, key lacks access to the resource, or merchant's WhatsApp not verified when the operator requires verification ("Verifique seu WhatsApp para criar checkouts."). Verify your WhatsApp in the merchant area inside the DePix app.
404Resource not found.
409State conflict — operation not allowed in the current status.
429Rate limit exceeded. Wait and try again.
500Internal server error. Try again in a few moments.
503Service unavailable — platform under maintenance or Pix provider temporarily down. Public checkout links (/api/merchants/:username/checkout and /api/products/:id/checkout) also return 503 when the linked merchant has not verified WhatsApp and the operator requires verification.

Create checkout

Creates a new Pix checkout. Returns the QR code and the payment URL to display to your customer.

POST /api/checkouts

Parameters

FieldTypeDescription
amountintegerrequiredAmount in centavos. Minimum: 500 (R$ 5.00). Maximum: 300000 (R$ 3,000.00).
payer_tax_numberstringrequiredPayer's CPF or CNPJ. Accepts a CPF (11 digits) or CNPJ (14 chars, including the new alphanumeric format), with or without mask.
descriptionstringoptionalOrder description. Maximum 500 characters. Displayed on the payment page.
expires_inintegeroptionalExpiration time in seconds. Default: 1200 (20min). Minimum: 300 (5min). Maximum: 1200 (20min).
image_urlstringoptionalHTTPS URL of the product image. Displayed on the payment page.
callback_urlstringoptionalHTTPS URL that receives the checkout webhooks.
redirect_urlstringoptionalURL to redirect the customer after payment.
metadataobjectoptionalAdditional data from your system (order_id, user_id, etc.). Maximum 4KB. Returned in webhooks.

Example

curl -X POST https://depix-backend.vercel.app/api/checkouts \
  -H "Authorization: Bearer sk_live_<your-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2990,
    "payer_tax_number": "529.982.247-25",
    "description": "T-shirt size M",
    "expires_in": 900,
    "callback_url": "https://my-store.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_<your-key>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    amount: 2990,
    payer_tax_number: "529.982.247-25",
    description: "T-shirt size M",
    expires_in: 900,
    callback_url: "https://my-store.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_<your-key>"},
    json={
        "amount": 2990,
        "payer_tax_number": "529.982.247-25",
        "description": "T-shirt size M",
        "expires_in": 900,
        "callback_url": "https://my-store.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_<your-key>",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "amount" => 2990,
        "payer_tax_number" => "529.982.247-25",
        "description" => "T-shirt size M",
        "expires_in" => 900,
        "callback_url" => "https://my-store.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_<your-key>");

var payload = new {
    amount = 2990,
    payer_tax_number = "529.982.247-25",
    description = "T-shirt size M",
    expires_in = 900,
    callback_url = "https://my-store.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,"payer_tax_number":"529.982.247-25","description":"T-shirt size M","expires_in":900,"callback_url":"https://my-store.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_<your-key>")
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_<your-key>",
  "Content-Type" => "application/json",
})
req.body = { amount: 2990, payer_tax_number: "529.982.247-25", description: "T-shirt size M", expires_in: 900,
             callback_url: "https://my-store.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,"payer_tax_number":"529.982.247-25","description":"T-shirt size M","expires_in":900,
     "callback_url":"https://my-store.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_<your-key>")
    .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());
Response — 201 Created
{
  "id":          "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
  "status":      "pending",
  "amount":      2990,
  "description": "T-shirt size 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..."    // EMV payload for QR code
  }
}
💡 Display the payment_url to your customer or generate a QR code from pix.qr_code. The QR code is compatible with any banking app.

Get checkout

Returns the details of a specific checkout.

GET /api/checkouts/:id
curl
curl https://depix-backend.vercel.app/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx \
  -H "Authorization: Bearer sk_live_<your-key>"
Response — 200 OK
{
  "checkout": {
    "id":             "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
    "status":         "completed",     // pending | processing | completed | cancelled | expired
    "amount":         2990,
    "description":    "T-shirt size M",
    "image_url":      null,
    "callback_url":   "https://my-store.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"  // Liquid txid (present when completed)
  }
}

Possible statuses

StatusMeaning
pendingAwaiting payment.
processingPix received, processing conversion to DePix.
completedPayment confirmed. DePix in the merchant's wallet.
cancelledCancelled by the merchant.
expiredPayment deadline expired.

List checkouts

Lists the merchant's checkouts with filters and pagination.

GET /api/checkouts

Query params (all optional)

ParameterDescription
statusFilter by status: pending, completed, cancelled, expired.
product_idFilter by product. E.g.: prd_xxx.
fromStart date (ISO 8601). E.g.: 2025-06-01T00:00:00Z.
toEnd date (ISO 8601).
qSearch by ID or description.
limitNumber of results per page. Default: 50. Maximum: 100.
offsetPagination. Default: 0.
curl
curl "https://depix-backend.vercel.app/api/checkouts?status=completed&limit=20" \
  -H "Authorization: Bearer sk_live_<your-key>"
Response — 200 OK
{
  "checkouts": [
    {
      "id":            "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
      "status":        "completed",
      "amount":        2990,
      "description":   "T-shirt size M",
      "product_name":  "Black T-shirt",    // null if checkout is not linked to a product
      "metadata":      "{\"order_id\":\"42\"}",  // JSON string, null if absent
      "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
}

Cancel checkout

Cancels a pending checkout. Only checkouts with pending status can be cancelled.

POST /api/checkouts/:id/cancel
curl
curl -X POST https://depix-backend.vercel.app/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx/cancel \
  -H "Authorization: Bearer sk_live_<your-key>"
Response — 200 OK
{ "success": true }

Create product

Creates a new product with a fixed price. Each product generates a permanent payment link that can be shared with your customers.

POST /api/products

Parameters

FieldTypeDescription
namestringrequiredProduct name shown in the UI. 2-80 characters.
slugstringoptionalURL identifier. If omitted, auto-generated from name. Lowercase letters, numbers, and hyphens. 2-60 characters. Cannot start/end with a hyphen.
amountintegerrequiredAmount in centavos. Minimum: 500. Maximum: 300000.
descriptionstringoptionalProduct description. Maximum 500 characters.
image_urlstringoptionalHTTPS URL of the product image.
callback_urlstringoptionalHTTPS URL for webhooks. Overrides the merchant default.
redirect_urlstringoptionalRedirect URL. Overrides the merchant default.
metadataobjectoptionalAdditional data. Maximum 4KB. Included in webhooks for generated checkouts.
expires_inintegeroptionalCheckout expiration time in seconds. Default: 1200 (20min). Minimum: 300 (5min). Maximum: 1200 (20min).

Example

curl -X POST https://depix-backend.vercel.app/api/products \
  -H "Authorization: Bearer sk_live_<your-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "T-shirt M",
    "slug": "tshirt-m",
    "amount": 2990,
    "description": "T-shirt size M"
  }'
const res = await fetch("https://depix-backend.vercel.app/api/products", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_live_<your-key>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "T-shirt M",
    slug: "tshirt-m",
    amount: 2990,
    description: "T-shirt size 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_<your-key>"},
    json={
        "name": "T-shirt M",
        "slug": "tshirt-m",
        "amount": 2990,
        "description": "T-shirt size 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_<your-key>",
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "name" => "T-shirt M",
        "slug" => "tshirt-m",
        "amount" => 2990,
        "description" => "T-shirt size 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_<your-key>");

var payload = new { name = "T-shirt M", slug = "tshirt-m", amount = 2990, description = "T-shirt size 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":"T-shirt M","slug":"tshirt-m","amount":2990,"description":"T-shirt size M"}`

req, _ := http.NewRequest("POST", "https://depix-backend.vercel.app/api/products", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer sk_live_<your-key>")
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_<your-key>",
  "Content-Type" => "application/json",
})
req.body = { name: "T-shirt M", slug: "tshirt-m", amount: 2990, description: "T-shirt size 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":"T-shirt M","slug":"tshirt-m","amount":2990,"description":"T-shirt size M"}""";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://depix-backend.vercel.app/api/products"))
    .header("Authorization", "Bearer sk_live_<your-key>")
    .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());
Response — 201 Created
{
  "product": {
    "id":           "prd_xxx",
    "name":         "T-shirt M",
    "slug":         "tshirt-m",
    "amount":       2990,
    "description":  "T-shirt size 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/tshirt-m"
  }
}

List products

Lists the merchant's products with filters and pagination.

GET /api/products

Query params (all optional)

ParameterDescription
activeFilter by status: 1 (active) or 0 (inactive).
qSearch by name, slug, or description.
limitNumber of results. Default: 50. Maximum: 100.
offsetPagination. Default: 0.
curl
curl "https://depix-backend.vercel.app/api/products?active=1" \
  -H "Authorization: Bearer sk_live_<your-key>"
Response — 200 OK
{
  "products": [
    {
      "id":          "prd_xxx",
      "name":        "T-shirt M",
      "slug":        "tshirt-m",
      "amount":      2990,
      "description": "T-shirt size M",
      "active":      true,
      "is_live":     true,
      "position":    0,
      "payment_url": "https://pay.depixapp.com/joao/tshirt-m"
    }
  ],
  "stats": {
    "total":  5,
    "active": 4
  },
  "limit":  50,
  "offset": 0
}

position — integer or null. Display order on the public storefront. null = not pinned (sorted by best-sellers); integer = pinned, shown at the given position (lowest first).

Get product

Returns the details of a specific product, including checkout statistics.

GET /api/products/:id
curl
curl https://depix-backend.vercel.app/api/products/prd_xxx \
  -H "Authorization: Bearer sk_live_<your-key>"
Response — 200 OK
{
  "product": {
    "id":           "prd_xxx",
    "name":         "T-shirt M",
    "slug":         "tshirt-m",
    "amount":       2990,
    "description":  "T-shirt size M",
    "image_url":    null,
    "callback_url": null,
    "redirect_url": null,
    "metadata":     null,
    "expires_in":   1200,
    "active":       true,
    "is_live":      true,
    "position":     0,
    "payment_url":  "https://pay.depixapp.com/joao/tshirt-m",
    "created_at":   "2025-06-01T00:00:00.000Z"
  }
}

position — integer or null. Display order on the public storefront. null = not pinned (sorted by best-sellers); integer = pinned, shown at the given position (lowest first).

Update product

Updates one or more fields of an existing product. Only send the fields you want to change.

PATCH /api/products/:id

Parameters (all optional)

FieldTypeDescription
namestringNew product name. 2-80 characters.
slugstringNew URL identifier. Same rules as creation.
amountintegerNew amount in centavos.
descriptionstringNew description.
image_urlstringNew image URL.
callback_urlstringNew webhook URL.
redirect_urlstringNew redirect URL.
metadataobjectNew additional data.
expires_inintegerNew checkout expiration time. Minimum: 300 (5min). Maximum: 1200 (20min).
curl
curl -X PATCH https://depix-backend.vercel.app/api/products/prd_xxx \
  -H "Authorization: Bearer sk_live_<your-key>" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 3490, "description": "T-shirt size M - Special Edition" }'
Response — 200 OK
{
  "product": {
    "id":           "prd_xxx",
    "name":         "T-shirt M",
    "slug":         "tshirt-m",
    "amount":       3490,
    "description":  "T-shirt size M - Special Edition",
    "active":       true,
    "is_live":      true,
    "payment_url":  "https://pay.depixapp.com/joao/tshirt-m"
  }
}

Activate / Deactivate product

Activates or deactivates a product. Inactive products return a 404 error when accessed via the payment link.

POST /api/products/:id/activate
POST /api/products/:id/deactivate
curl — activate
curl -X POST https://depix-backend.vercel.app/api/products/prd_xxx/activate \
  -H "Authorization: Bearer sk_live_<your-key>"
curl — deactivate
curl -X POST https://depix-backend.vercel.app/api/products/prd_xxx/deactivate \
  -H "Authorization: Bearer sk_live_<your-key>"
Response — 200 OK
{ "success": true }

Product checkouts

Lists the checkouts generated from a specific product. Accepts the same filters as the general checkout listing.

GET /api/products/:id/checkouts
curl
curl "https://depix-backend.vercel.app/api/products/prd_xxx/checkouts?status=completed" \
  -H "Authorization: Bearer sk_live_<your-key>"

The response follows the same format as the checkout listing.

Public product

Returns the public data of an active product. Does not require authentication.

GET /api/products/:id/public
curl
curl https://depix-backend.vercel.app/api/products/prd_xxx/public
Response — 200 OK
{
  "product": {
    "id":          "prd_xxx",
    "name":        "T-shirt M",
    "slug":        "tshirt-m",
    "amount":      2990,
    "description": "T-shirt size M",
    "image_url":   null
  },
  "merchant": {
    "name":          "Loja do Joao",
    "merchant_slug": "joao",
    "username":      "joao"
  }
}

Product checkout

Creates a checkout from an active product. Does not require authentication. The amount is inherited from the product.

POST /api/products/:id/checkout
curl
curl -X POST https://depix-backend.vercel.app/api/products/prd_xxx/checkout

The response follows the same format as create checkout (status 201).

Merchant page

Returns the merchant's public data. Does not require authentication.

GET /api/merchants/:username/public
curl
curl https://depix-backend.vercel.app/api/merchants/joao/public
Response — 200 OK
{
  "merchant": {
    "name":          "Loja do Joao",
    "merchant_slug": "joao",
    "username":      "joao"
  }
}

Merchant checkout

Creates a checkout with a custom amount from the merchant page. Does not require authentication.

POST /api/merchants/:username/checkout

Parameters

FieldTypeDescription
amountintegerrequiredAmount in centavos. Minimum: 500. Maximum: 300000.
curl
curl -X POST https://depix-backend.vercel.app/api/merchants/joao/checkout \
  -H "Content-Type: application/json" \
  -d '{ "amount": 5000 }'

The response follows the same format as create checkout (status 201).

Webhooks

When a checkout's status changes, the API sends a POST to the callback_url you provided when creating the checkout (or configured on the product/merchant).

How it works

  • The request is sent with a 30-second timeout.
  • If it fails (non-2xx response, timeout, or network error), the API retries up to 5 more times: after 1 minute, 10 minutes, 1 hour, 4 hours, and 12 hours (6 attempts total, spanning roughly 17 hours).
  • Your endpoint must respond with a 2xx status to confirm receipt.
  • The callback_url must be HTTPS and publicly accessible (no private IPs).

Request headers

  • X-DePix-Signature — HMAC-SHA256 signature (see Verify signature section).
  • X-DePix-Event — event name (e.g., checkout.completed).
  • X-DePix-Event-Id — unique identifier for this event, stable across retries (e.g., evt_abc123…). This is the recommended dedupe key.
  • X-DePix-Delivery-Attempt — the current attempt number (1, 2, … up to 6). Changes per retry; do not use for dedupe.
  • User-Agent — always DePix-Webhook/1.0.

At-least-once delivery and idempotency (required)

Webhooks are delivered with at-least-once semantics — this is the industry standard (Stripe, PayPal, Mercado Pago all work the same way). It means the same event can reach your endpoint more than once, even when everything is working correctly. Common scenarios:

  • Your server processes the webhook but responds slowly — our API times out at 30s, marks the delivery as failed, and retries; you process the event twice.
  • Your server returns 200 but the connection drops before we read the response — same effect: retry and duplicate processing.
  • Our operations team manually redispatches an event (via an admin command) that you already processed.

To avoid delivering a product twice, double-crediting balance, or triggering duplicate side effects, your endpoint must be idempotent. The simplest and most robust pattern is to deduplicate by X-DePix-Event-Id: store the IDs of events you've already processed and silently ignore any event whose ID is already in your table.

// Dedupe example (Node.js, pseudo-code)
app.post("/webhook", async (req, res) => {
  // 1. Validate the HMAC signature first (see Verify signature section).

  const eventId = req.headers["x-depix-event-id"];

  // 2. Process AND mark-as-processed in a single DB transaction so the row is
  // only persisted if your business logic succeeds. If processCheckout throws,
  // the transaction rolls back and our retry can deliver the event again.
  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") {
      // Already processed — return 200 and ignore.
      return res.sendStatus(200);
    }
    throw err; // Let our API retry.
  }

  res.sendStatus(200);
});

If you'd rather not maintain a separate table, you can also dedupe on the event_id field inside the JSON payload (data.event_id) — it carries the same stable value as the X-DePix-Event-Id header. Do not use (data.id, event) as your dedupe key: an operator-triggered redispatch reuses the same checkout id and event name, so a tuple-based dedupe would silently swallow it.

Reducing retries

To avoid duplicate deliveries on the happy path, respond as fast as possible — common targets are under a few seconds, to leave room for network latency before our 30s timeout. The recommended pattern: validate the signature, return 200 immediately, then process the event in the background (queue, worker, etc.). This avoids retries caused by timeouts on our side.

Events

checkout.processing

Fired when the Pix payment is received and the conversion is being processed.

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

Fired when the payment is confirmed and the DePix arrives in the merchant's wallet.

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

Fired when the merchant cancels the 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

Fired when the checkout expires without receiving payment.

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

Verify signature

Each webhook comes with an X-DePix-Signature header. Always validate the signature before processing the event — this ensures the request came from the DePix App API and not from a third party.

Header format

X-DePix-Signature: t=1717257600,v1=abc123def456...
  • t — Unix timestamp of the dispatch (seconds).
  • v1 — HMAC-SHA256 signature in hexadecimal.

How to validate

The signature is computed over the string timestamp.payload using the Webhook Secret from your account (available in the Merchant Dashboard).

Bash
# Compute the expected signature
EXPECTED=$(echo -n "${TIMESTAMP}.${RAW_BODY}" | \
  openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" | awk '{print $2}')

# Compare with the received v1
if [ "$EXPECTED" = "$RECEIVED_V1" ]; then
  echo "Valid signature"
fi
Node.js
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);
}

// Example with 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("Invalid signature");
  }
  const { event, data } = JSON.parse(req.body);
  // process the event...
  res.sendStatus(200);
});
Python
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)
PHP
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"]);
}
C#
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"])
    );
}
Go
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"]))
}
Ruby
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
Java
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());
}
Read the body as raw bytes (before JSON parsing). Any reformatting will invalidate the signature.

Sandbox

Use sk_test_... keys to test without moving real money. Checkouts created with a test key never generate a real Pix and are isolated from production checkouts.

Test mode differences

  • The is_live field returns false.
  • The generated QR code is not a valid Pix — it cannot be paid with a banking app.
  • Use the /simulate-payment endpoint to mark the checkout as paid.
  • Webhooks are sent normally — great for testing your end-to-end integration.
The sandbox is independent from production. You can create, simulate, and cancel test checkouts without risk.

Simulate payment

Marks a test checkout as paid. Only works with sk_test_ keys. Fires the checkout.completed webhook normally.

POST /api/checkouts/:id/simulate-payment
curl
# 1. Create a test checkout
curl -X POST https://depix-backend.vercel.app/api/checkouts \
  -H "Authorization: Bearer sk_test_<your-key>" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 1000, "callback_url": "https://my-store.com/webhook" }'

# 2. Simulate the payment
curl -X POST https://depix-backend.vercel.app/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx/simulate-payment \
  -H "Authorization: Bearer sk_test_<your-key>"
Response — 200 OK
{ "success": true }

After the simulation, your callback_url will receive the checkout.completed event within seconds — exactly like a real payment.

Verify key (GET /api/me)

Returns the authenticated merchant's information. Useful for verifying if the API key is valid and checking account data.

GET /api/me
curl
curl https://depix-backend.vercel.app/api/me \
  -H "Authorization: Bearer sk_live_<your-key>"
Response — 200 OK
{
  "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

The API enforces request limits to ensure stability and protect against abuse.

EndpointLimitScope
POST /api/checkouts30 / minper IP
GET /api/checkout-page/:id30 / minper IP (public)
GET /api/pay/:id60 / minper IP (public)
POST /api/pay/:id/simulate5 / minper IP (public, sandbox only)
POST /api/merchants/:username/checkout10 / minper IP (public)
POST /api/products/:id/checkout10 / minper IP (public)
GET /api/products/:id/public30 / minper IP (public)
GET /api/merchants/:username/public30 / minper IP (public)
Per merchant (API key)Configurableapplied after auth, on top of the per-IP limit
  • For requests authenticated with an API key, an additional rate limit applies per merchant (configurable — contact support if you need it raised).
  • For public endpoints (no auth), the rate limit is applied only per IP.
  • When the limit is reached, the API returns status 429 with the message "Muitas requisições. Tente novamente em 1 minuto." — wait ~60s before retrying.
If you need higher limits for your integration, contact support.