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.
Authentication
Use an API key in the Authorization header on all authenticated requests.
Authorization: Bearer sk_live_<your-key>
Key types
| Prefix | Type | Behavior |
|---|---|---|
| sk_live_ | Live | Real checkouts. Real money. |
| sk_test_ | Test | Test 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.
Errors
Errors follow the format below. The errorMessage field is always human-readable.
{
"response": {
"errorMessage": "Invalid data.",
"errors": [ // present on validation errors
{ "field": "amount", "message": "Required." }
]
}
}
| Status | Meaning |
|---|---|
| 400 | Invalid data — check the fields. |
| 401 | API key missing, invalid, or revoked. |
| 403 | Permission 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. |
| 404 | Resource not found. |
| 409 | State conflict — operation not allowed in the current status. |
| 429 | Rate limit exceeded. Wait and try again. |
| 500 | Internal server error. Try again in a few moments. |
| 503 | Service 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.
Parameters
| Field | Type | Description | |
|---|---|---|---|
| amount | integer | required | Amount in centavos. Minimum: 500 (R$ 5.00). Maximum: 300000 (R$ 3,000.00). |
| payer_tax_number | string | required | Payer's CPF or CNPJ. Accepts a CPF (11 digits) or CNPJ (14 chars, including the new alphanumeric format), with or without mask. |
| description | string | optional | Order description. Maximum 500 characters. Displayed on the payment page. |
| expires_in | integer | optional | Expiration time in seconds. Default: 1200 (20min). Minimum: 300 (5min). Maximum: 1200 (20min). |
| image_url | string | optional | HTTPS URL of the product image. Displayed on the payment page. |
| callback_url | string | optional | HTTPS URL that receives the checkout webhooks. |
| redirect_url | string | optional | URL to redirect the customer after payment. |
| metadata | object | optional | Additional 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());
{
"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
}
}
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.
curl https://depix-backend.vercel.app/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx \ -H "Authorization: Bearer sk_live_<your-key>"
{
"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
| Status | Meaning |
|---|---|
| pending | Awaiting payment. |
| processing | Pix received, processing conversion to DePix. |
| completed | Payment confirmed. DePix in the merchant's wallet. |
| cancelled | Cancelled by the merchant. |
| expired | Payment deadline expired. |
List checkouts
Lists the merchant's checkouts with filters and pagination.
Query params (all optional)
| Parameter | Description |
|---|---|
| status | Filter by status: pending, completed, cancelled, expired. |
| product_id | Filter by product. E.g.: prd_xxx. |
| from | Start date (ISO 8601). E.g.: 2025-06-01T00:00:00Z. |
| to | End date (ISO 8601). |
| q | Search by ID or description. |
| limit | Number of results per page. Default: 50. Maximum: 100. |
| offset | Pagination. Default: 0. |
curl "https://depix-backend.vercel.app/api/checkouts?status=completed&limit=20" \ -H "Authorization: Bearer sk_live_<your-key>"
{
"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.
curl -X POST https://depix-backend.vercel.app/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx/cancel \ -H "Authorization: Bearer sk_live_<your-key>"
{ "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.
Parameters
| Field | Type | Description | |
|---|---|---|---|
| name | string | required | Product name shown in the UI. 2-80 characters. |
| slug | string | optional | URL identifier. If omitted, auto-generated from name. Lowercase letters, numbers, and hyphens. 2-60 characters. Cannot start/end with a hyphen. |
| amount | integer | required | Amount in centavos. Minimum: 500. Maximum: 300000. |
| description | string | optional | Product description. Maximum 500 characters. |
| image_url | string | optional | HTTPS URL of the product image. |
| callback_url | string | optional | HTTPS URL for webhooks. Overrides the merchant default. |
| redirect_url | string | optional | Redirect URL. Overrides the merchant default. |
| metadata | object | optional | Additional data. Maximum 4KB. Included in webhooks for generated checkouts. |
| expires_in | integer | optional | Checkout 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());
{
"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.
Query params (all optional)
| Parameter | Description |
|---|---|
| active | Filter by status: 1 (active) or 0 (inactive). |
| q | Search by name, slug, or description. |
| limit | Number of results. Default: 50. Maximum: 100. |
| offset | Pagination. Default: 0. |
curl "https://depix-backend.vercel.app/api/products?active=1" \ -H "Authorization: Bearer sk_live_<your-key>"
{
"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.
curl https://depix-backend.vercel.app/api/products/prd_xxx \ -H "Authorization: Bearer sk_live_<your-key>"
{
"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.
Parameters (all optional)
| Field | Type | Description |
|---|---|---|
| name | string | New product name. 2-80 characters. |
| slug | string | New URL identifier. Same rules as creation. |
| amount | integer | New amount in centavos. |
| description | string | New description. |
| image_url | string | New image URL. |
| callback_url | string | New webhook URL. |
| redirect_url | string | New redirect URL. |
| metadata | object | New additional data. |
| expires_in | integer | New checkout expiration time. Minimum: 300 (5min). Maximum: 1200 (20min). |
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" }'
{
"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.
curl -X POST https://depix-backend.vercel.app/api/products/prd_xxx/activate \ -H "Authorization: Bearer sk_live_<your-key>"
curl -X POST https://depix-backend.vercel.app/api/products/prd_xxx/deactivate \ -H "Authorization: Bearer sk_live_<your-key>"
{ "success": true }
Feature products on the storefront
Sets the ordered list of products pinned to the top of the merchant's public page (the "storefront"). Pinned products render first, in the given order; every product not in the list is unpinned and falls back to best-sellers ordering. Sending an empty array clears all pins. This request reconciles the whole pinned set in one call (it is not incremental).
Parameters
| Field | Type | Description | |
|---|---|---|---|
| productIds | array<string> | required | Ordered list of product IDs to pin to the top of the storefront. An empty array clears all pins. Maximum 50. All IDs must belong to the merchant; duplicate IDs are rejected. |
Example
curl -X POST https://depix-backend.vercel.app/api/products/featured \ -H "Authorization: Bearer sk_live_<your-key>" \ -H "Content-Type: application/json" \ -d '{ "productIds": ["prd_abc123", "prd_def456"] }'
const res = await fetch("https://depix-backend.vercel.app/api/products/featured", { method: "POST", headers: { "Authorization": "Bearer sk_live_<your-key>", "Content-Type": "application/json", }, body: JSON.stringify({ productIds: ["prd_abc123", "prd_def456"], }), }); const data = await res.json(); console.log(data.featured);
import requests resp = requests.post( "https://depix-backend.vercel.app/api/products/featured", headers={"Authorization": "Bearer sk_live_<your-key>"}, json={ "productIds": ["prd_abc123", "prd_def456"], }, ) data = resp.json() print(data["featured"])
$ch = curl_init("https://depix-backend.vercel.app/api/products/featured"); 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([ "productIds" => ["prd_abc123", "prd_def456"], ]), ]); $response = curl_exec($ch); $data = json_decode($response, true); print_r($data["featured"]);
using var client = new HttpClient(); client.DefaultRequestHeaders.Add("Authorization", "Bearer sk_live_<your-key>"); var payload = new { productIds = new[] { "prd_abc123", "prd_def456" } }; var res = await client.PostAsync( "https://depix-backend.vercel.app/api/products/featured", new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") ); Console.WriteLine(await res.Content.ReadAsStringAsync());
body := `{"productIds":["prd_abc123","prd_def456"]}` req, _ := http.NewRequest("POST", "https://depix-backend.vercel.app/api/products/featured", 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/featured") req = Net::HTTP::Post.new(uri, { "Authorization" => "Bearer sk_live_<your-key>", "Content-Type" => "application/json", }) req.body = { productIds: ["prd_abc123", "prd_def456"] }.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 = """ {"productIds":["prd_abc123","prd_def456"]}"""; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://depix-backend.vercel.app/api/products/featured")) .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());
{
"success": true,
"featured": ["prd_abc123", "prd_def456"]
}
Possible errors: 400 (productIds is not a list, duplicate IDs, or more than 50), 404 (a product does not belong to the merchant), and 403 (no merchant account). See the Errors section for the error response format.
Product checkouts
Lists the checkouts generated from a specific product. Accepts the same filters as the general checkout listing.
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.
Payment links
DePix App generates permanent payment links for products and for the merchant page. These links create checkouts on demand when the customer accesses them.
Link types
| Type | URL | Behavior |
|---|---|---|
| Product | https://pay.depixapp.com/{merchant_slug}/{slug} | Fixed price. The customer sees the product and clicks "Pay with PIX". |
| Merchant | https://pay.depixapp.com/{merchant_slug} | Custom amount. The customer enters the amount and clicks "Pay with PIX". |
/api/merchants/:username/public and /api/products/:id/public (the :username URL segment is in fact the merchant_slug — the parameter name is kept for back-compat). When the merchant updates the business name, the slug regenerates — payment links previously sent to customers stop working and must be resent.
Lifecycle
- When the customer accesses the link and initiates payment, an individual checkout is created automatically.
- From there, the lifecycle is identical to a checkout created via API (status, webhooks, expiration).
- The
callback_urlfollows the chain: product field (if set) → merchant default → null. - The
redirect_urlfollows the same chain.
Public product
Returns the public data of an active product. Does not require authentication.
curl https://depix-backend.vercel.app/api/products/prd_xxx/public
{
"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.
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.
curl https://depix-backend.vercel.app/api/merchants/joao/public
{
"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.
Parameters
| Field | Type | Description | |
|---|---|---|---|
| amount | integer | required | Amount in centavos. Minimum: 500. Maximum: 300000. |
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_urlmust 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— alwaysDePix-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).
# 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
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); });
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 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_livefield returnsfalse. - The generated QR code is not a valid Pix — it cannot be paid with a banking app.
- Use the
/simulate-paymentendpoint to mark the checkout as paid. - Webhooks are sent normally — great for testing your end-to-end integration.
Simulate payment
Marks a test checkout as paid. Only works with sk_test_ keys. Fires the checkout.completed webhook normally.
# 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>"
{ "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.
curl https://depix-backend.vercel.app/api/me \ -H "Authorization: Bearer sk_live_<your-key>"
{
"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.
| Endpoint | Limit | Scope |
|---|---|---|
| POST /api/checkouts | 30 / min | per IP |
| GET /api/checkout-page/:id | 30 / min | per IP (public) |
| GET /api/pay/:id | 60 / min | per IP (public) |
| POST /api/pay/:id/simulate | 5 / min | per IP (public, sandbox only) |
| POST /api/merchants/:username/checkout | 10 / min | per IP (public) |
| POST /api/products/:id/checkout | 10 / min | per IP (public) |
| GET /api/products/:id/public | 30 / min | per IP (public) |
| GET /api/merchants/:username/public | 30 / min | per IP (public) |
| Per merchant (API key) | Configurable | applied 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
429with the message"Muitas requisições. Tente novamente em 1 minuto."— wait ~60s before retrying.