DePix App API

Introduction

The DePix App API lets you create and manage Pix checkouts programmatically. It is the same API that powers the BTCPay Server plugin, the Merchant Dashboard, and now AI agents — over MCP and the SDK.

Base URL

https://api.depixapp.com

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>

In the curl examples throughout these docs, the key comes from the DEPIX_API_KEY environment variable. Set it once in your shell — export DEPIX_API_KEY=sk_live_... (or your sk_test_ key in sandbox) — instead of pasting the key inline into every command.

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.

Every key is born with explicit scopes (merchant_read, merchant_write, wallet_read, wallet_write) and, for keys with the wallet_write scope, with mandatory spending limits. Keys are immutable after creation — see Key scopes and limits.

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.

Key scopes and limits

Every API key carries an explicit set of scopes that defines what it can do. Scopes are chosen at key creation and cannot be changed afterwards — keys are immutable; to change scopes or limits, revoke and create a new one.

Scopes split along two axes: merchant_* is the merchant side (the gateway — checkouts and products) and wallet_* is the wallet side (the Pix on/off-ramp — deposits and withdrawals).

ScopeGrants
merchant_readAll GETs of the merchant surface: list/get checkouts, products, and GET /api/me.
merchant_writeThe "receiving" side: create/simulate checkouts, the product CRUD, and editing the store's light profile fields (PATCH /api/merchants/me).
wallet_readReading the wallet-side status: GET /api/deposits/:id and GET /api/withdrawals/:id. Never granted by default.
wallet_writeThe "paying" side (moving money): POST /api/deposit, POST /api/withdraw. Never granted by default.
  • No implicit hierarchymerchant_write does not include merchant_read, and the wallet_* scopes include none of them. A key can combine all four: ["merchant_read", "merchant_write", "wallet_read", "wallet_write"].
  • A call without the required scope → 403 with error.code = "insufficient_scope" and details.required_scope (see Errors). The response never echoes the key's own scope list.

wallet_write keys are born with mandatory limits

Every key with the wallet_write scope has its own spending limits, enforced on top of the account limits (account limits always prevail). If you omit the values at creation, the defaults apply: R$ 100.00 per transaction (per_tx_limit_cents = 10000) and R$ 500.00 per day (daily_limit_cents = 50000, a 24-hour rolling window summing the deposits and withdrawals attributed to the key). You may consciously raise the values at creation — but a wallet_write key without limits does not exist.

An operation exceeding a key limit → 400 with error.code = "key_limit_exceeded" and details: { limit: "per_tx" | "daily", limit_cents, used_cents }.

Creating a key with scopes and limits

Credential management is owner-only: POST /api/api-keys accepts only the dashboard JWT (never another API key). Besides the existing fields (type, label, expires_in_days), creation accepts:

FieldTypeDescription
scopesarrayoptionalSubset of ["merchant_read", "merchant_write", "wallet_read", "wallet_write"], no duplicates. Default: ["merchant_read", "merchant_write"] (the behavior keys always had).
per_tx_limit_centsintegeroptionalPer-transaction limit in centavos (minimum 100). With the wallet_write scope, defaults to 10000 (R$ 100.00) when omitted.
daily_limit_centsintegeroptionalDaily limit in centavos, 24-hour rolling window (minimum 100). With the wallet_write scope, defaults to 50000 (R$ 500.00) when omitted.
rate_limit_per_minintegeroptionalAdditional per-key rate limit (1–600 req/min). Omitted = no key-level limit; only the merchant's aggregate budget applies.
Response — 201 Created
{
  "id":                  "a1b2c3d4e5f6...",
  "key":                 "sk_test_...",       // displayed only once
  "prefix":              "sk_test_",
  "label":               "agent-payments",
  "is_live":             false,
  "expires_at":          null,
  "scopes":              ["merchant_read", "merchant_write", "wallet_read", "wallet_write"],
  "per_tx_limit_cents":  10000,
  "daily_limit_cents":   50000,
  "rate_limit_per_min":  30
}
Immutable after creation. There is no endpoint to edit scopes or limits. To change anything on a live key, revoke it and create a new one — mutable permissions on a live credential are attack surface.

Idempotency

The money-moving POSTs accept the optional Idempotency-Key header (1–255 visible ASCII characters). Strongly recommended for agents and for any integration with automatic retries: a retry with the same key returns the original response instead of creating a second QR or a second charge.

Covered endpoints

  • POST /api/deposit
  • POST /api/withdraw
  • POST /api/checkouts

Exact semantics

ScenarioResult
Same key + same bodyReplay of the original response (same status, same body) + Idempotency-Replayed: true header. No new side effects.
Same key + different body422 idempotency_key_reuse — the handler never runs.
Same key on a different endpointIndependent — the uniqueness scope includes the endpoint.
Concurrent request with the same key409 idempotency_in_flight with retry_after: 5 — retry in a few seconds and receive the replay.
  • Uniqueness scope: (identity, endpoint, key) — keys of different merchants never collide.
  • Body comparison: hash of the parsed JSON. Whitespace/formatting never matter; field order does — a semantically equal body with reordered fields → 422. A byte-identical retry always matches.
  • 5xx and 429 responses are never stored — those retries re-execute. Deterministic 4xx (validation, limits) are stored and replayed.
  • 24-hour TTL — after that, the same key counts as new.
  • Sandbox operations (sk_test_) participate normally — agents can train the full flow.
Effectively-once, never exactly-once. The guarantee is at most ONE concurrent execution. If the owning invocation dies between the provider call and persisting the response, a takeover after 60s re-executes the operation. Design your ledger treating a retry as potentially duplicating.

Example

curl
curl -X POST https://api.depixapp.com/api/deposit \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: agent-run-42-deposit-1" \
  -d '{ "amountInCents": 1500, "depixAddress": "lq1qq...", "payer_tax_number": "52998224725" }'

First call: 200 with the QR. Identical retry: 200 with the same body + Idempotency-Replayed: true. Retry with a changed amountInCents:

Response — 422 Unprocessable Entity
{
  "response": { "errorMessage": "Idempotency-Key já utilizada com um corpo diferente." },
  "error": {
    "code": "idempotency_key_reuse",
    "message": "This Idempotency-Key was already used with a different request body.",
    "request_id": "gru1::abcd-1234",
    "docs_url": "https://depixapp.com/docs/en/#errors"
  }
}

Errors

Errors carry a dual envelope: response.errorMessage (legacy Portuguese message, always present and always human-readable) and the error object (structured machine contract, English messages). Branch on error.code — never on message text.

Error response
{
  "response": { "errorMessage": "Muitas requisições. Tente novamente em 1 minuto." },
  "error": {
    "code": "rate_limited",
    "message": "Too many requests for this scope.",
    "request_id": "gru1::iad1::v9x4k-1751476800000-abc123",
    "retry_after": 37,
    "docs_url": "https://depixapp.com/docs/en/#errors",
    "details": { "scope": "deposit" }
  }
}
  • request_id — correlation id, returned on every response (success included) in the X-Request-Id header. Quote it in support requests.
  • retry_after — seconds to wait before retrying; present on every 429, 503 and on 409 idempotency_in_flight. Mirrored in the Retry-After HTTP header.
  • details — optional extras, normative per code: offending field, required scope, limits in centavos (see the table below).
  • Preserved legacy siblings: create-checkout validation errors keep response.errors[] (per-field list) and the blocked-account 403 keeps blocked: true.
  • Legacy handlers outside the agent surface may still respond with only response.errorMessage, without the error object.

Code catalog

Complete, closed catalog. Every row has a stable anchor in the form #error-<code> (e.g. #error-rate_limited).

CodeHTTPWhen
unauthorized401Login-only (JWT) route without a valid token.
invalid_api_key401Token with the sk_ prefix not found, revoked, or expired.
invalid_token401Invalid/expired JWT or missing/malformed Authorization header.
insufficient_scope403The API key lacks the scope required by the operation — details.required_scope.
invalid_password401The account password provided is incorrect.
password_required400The account password is required for this security-sensitive operation.
agent_account_no_password403Agent accounts have no password — authenticate with the agent keypair instead.
oauth_account_not_linked403The Google/GitHub identity is not linked to a DePix account. Link it in the dashboard, then retry.
account_already_linked409The account is already linked to a different OAuth identity. Unlink it first.
workos_identity_in_use409The OAuth identity is already linked to another DePix account.
registration_blocked403The registration cannot proceed.
operator_oauth_failed502The operator identity could not be verified with the provider. Retry.
account_blocked403Blocked account (legacy sibling blocked: true preserved).
merchant_required403The authenticated account has no active merchant profile.
live_access_required403Creating a sk_live_ key without production access approval.
whatsapp_verification_required403Owner's WhatsApp not verified while the operator requires verification.
withdraw_disabled403Withdrawals temporarily disabled (global kill switch).
external_wallet_disabled403Withdrawals to external wallets temporarily disabled.
first_withdraw_tax_number_mismatch403Until the account has one completed withdrawal (status sent), every withdrawal must go to the same CPF/CNPJ that paid the account's first completed deposit. After that first withdrawal, the account withdraws to any Pix key. An account with no completed deposit carrying a document is not locked, and sk_test_ (sandbox) keys are exempt. details.anchor_tax_number carries the expected document, masked.
sandbox_only403simulate-payment called on a live checkout.
validation_error400Invalid input — details.field when applicable; create checkout preserves response.errors[].
tax_number_required400Missing mandatory CPF/CNPJ (payer_tax_number / taxNumber).
amount_out_of_range400Amount outside the endpoint's bounds — details: { min_cents, max_cents } with the bounds of THIS endpoint/mode.
account_limit_exceeded400An account limit was reached. details.limit says which: "receive_cap" — the rolling-window receive cap, with { current_level, cap_cents, used_cents, amount_cents, window_days, resets_at, next_level_requirements, kyc_url }; "first_deposit" — the account has not completed a personal deposit yet; "per_tx" — the legacy per-transaction cap, applied only while the per-level cap is not in force. ("cumulative", the lifetime cap, was removed on 2026-07-31.) None of them depend on the account being verified: verifying unlocks the merchant tools and changes no limit. See Limits.
key_limit_exceeded400API-key spending limit — details: { limit: "per_tx" | "daily", limit_cents, used_cents }.
not_found404Route or resource does not exist — includes resources owned by another account (ownership is never disclosed).
conflict409State conflict: invalid checkout transition, duplicate txid, duplicate slug.
idempotency_in_flight409A request with the same Idempotency-Key is still executing — retry_after: 5.
idempotency_key_reuse422Idempotency-Key reused with a different body.
rate_limited429Per-IP, per-user, or per-key rate limit — retry_after up to 60.
merchant_rate_limited429Merchant aggregate budget exceeded — retry_after up to 60.
payer_velocity_limit429Too many transactions for the same payer CPF/CNPJ in a short period (max 2 per sliding 30-min window; deposits + checkouts combined) — details: { window_minutes, max_per_window }, retry_after up to the remainder of the window.
platform_shutdown503Platform under maintenance (global kill switch) — retry_after: 300.
service_unavailable503Infra dependency unavailable — includes the fail-closed of wallet_* routes via API key when the rate limit cannot be checked — retry_after: 30.
upstream_error502Malformed response or error from the Pix provider.
internal_error500Unexpected internal error. Try again in a few moments.

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: 600000 (R$ 6,000.00). On the depix rail this is the face amount, before the merchant's discount.
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. Must be a real, registered CPF/CNPJ — the payment processor validates beyond the checksum when generating the QR. Required on the pix rail only; on the depix rail it is ignored.
payment_methodstringoptionalpix (default) or depix. With depix the charge is paid directly in DePix on the Liquid network, with no Pix QR — see Receive DePix directly. If the merchant has not enabled direct DePix, creation fails with depix_not_enabled (400).
expected_discount_pctintegeroptionalOn the depix rail only: the discount (0–90) your page showed the customer. If the merchant changed it in the meantime, creation fails with discount_changed (409) carrying the current values, instead of charging a price different from the one displayed.
descriptionstringoptionalOrder description. Maximum 500 characters. Displayed on the payment page.
expires_inintegeroptionalExpiration time in seconds. pix rail: default 1200 (20min), minimum 300 (5min), maximum 1200 (20min). depix rail: default 1800 (30min), minimum 300 (5min), maximum 3600 (1h).
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.
Real, registered CPF/CNPJ. The payment processor validates payer_tax_number beyond the checksum when generating the QR. A checksum-valid but unregistered number fails at creation with the generic error "Error generating QR Code. Please contact an admin." — if you get that error on create, the payer's CPF/CNPJ is almost certainly not a real registered one.

Example

curl -X POST https://api.depixapp.com/api/checkouts \
  -H "Authorization: Bearer $DEPIX_API_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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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
  }
}

Charge directly in DePix: send "payment_method": "depix" to the same endpoint. The response swaps the pix block for a depix block with the address, the exact amount and a payment link — see Receive DePix directly.

Response shape: POST /api/checkouts returns the checkout flat, at the root of the JSON (as above). GET /api/checkouts/:id returns the same object wrapped in { "checkout": { ... } } — see Get checkout.

💡 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://api.depixapp.com/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx \
  -H "Authorization: Bearer $DEPIX_API_KEY"
Response — 200 OK
{
  "checkout": {
    "id":             "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
    "status":         "completed",     // pending | processing | approved | 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",
    "approved_at":    "2025-06-01T15:03:00.000Z",
    "completed_at":   "2025-06-01T15:22:00.000Z",
    "cancelled_at":   null,
    "blockchain_tx_id": "abc123...def456",  // Liquid txid (present when completed)
    "rejection_reasons": []         // array of reasons when the payment was refunded/held
  }
}

Response shape: here the checkout comes wrapped in { "checkout": { ... } }, while POST /api/checkouts returns the object flat at the root of the JSON — mind the difference when parsing both.

While the checkout is pending, the response also includes pix_payload (the Pix QR EMV payload); the field is dropped once the status leaves pending. approved_at is always present (null until approval).

Possible statuses

StatusMeaning
pendingAwaiting payment.
processingPix received, processing conversion to DePix.
approvedPayment approved by the bank, awaiting settlement in DePix.
completedPayment confirmed. DePix in the merchant's wallet.
cancelledCancelled/refunded by the Pix provider.
expiredPayment deadline expired.

Refund reasons (rejection_reasons)

When a checkout's payment is refunded or held by the provider, the rejection_reasons field carries an array with the reasons ([] when the payment was not refused). New codes may appear — handle unknown values generically.

CodeMeaning
PAYER_MISMATCHPayment made with a CPF/CNPJ different from the one provided on the checkout.
PAST_DAILY_LIMITPayer's daily limit exceeded.
BLOCKED_USERUser blocked by the provider.
HIGH_VELOCITYToo many transactions from the payer in a short period.

List checkouts

Lists the merchant's checkouts with filters and pagination.

GET /api/checkouts

Query params (all optional)

ParameterDescription
statusFilter by status: pending, processing, approved, 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://api.depixapp.com/api/checkouts?status=completed&limit=20" \
  -H "Authorization: Bearer $DEPIX_API_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",
      "approved_at":   "2025-06-01T15:03:00.000Z",
      "expires_at":    "2025-06-01T15:30:00.000Z",
      "is_live":       true,
      "payment_method": "depix",        // "pix" or "depix" — the rail this sale settled on
      "depix_discount_pct": 10,       // depix rail only: discount offered, in %
      "depix_due_cents": 2691,       // depix rail only: the amount the payer actually sends
      "rejection_reasons": [],    // array of reasons when the payment was refunded/held
      "delay_until":   null,          // when the money is released, if the sale is held — ISO-8601 WITH offset ("2025-06-15T09:03:00-03:00")
      "vault_hours":   0              // hours this sale was booked to wait (0 = none; null = no decision recorded)
    }
  ],
  "stats": {
    "total":            47,
    "pending":          2,
    "completed":        40,
    "completed_amount":  189500    // centavos — R$ 1,895.00
  },
  "limit":  20,
  "offset": 0
}
Reconciling DePix sales: amount is always the list price. When payment_method is "depix", the amount the customer actually sends is depix_due_cents — the discounted price, moved down by a few cents, which is how we tell which payment belongs to which sale. Summing amount over discounted sales overstates every one of them. Both depix_* fields appear on the depix rail only; a Pix sale does not carry them.
Paid sales that have not landed yet: a sale that has been paid but whose money is not released yet stays in processing. What tells you that it is held is vault_hours (the wait it was booked for at creation); what tells you when the amount lands is delay_until, and only that. Until the provider answers, delay_until is null and there simply is no release date yet — created_at + vault_hours is not that date: the wait runs from the payment, which is later than creation, so that arithmetic always lands early.

vault_hours has three distinct answers: a number greater than zero (it was held), 0 (the policy looked and imposed no wait), and null (no decision was recorded for that row — a sandbox checkout, or one created before/while the mechanism was switched off). null is not zero.

Mind the format of delay_until: it is relayed verbatim from the settlement provider as ISO-8601 with an offset ("2025-06-15T09:03:00-03:00"), unlike created_at, expires_at and processing_at, which are naive UTC ("2025-06-01 15:00:00"). Parse it as a full ISO-8601 instant: a parser that assumes the naive shape and appends a Z produces an invalid date for every real value.

To reconcile "what I have already sold but not yet received", list with ?status=processing and sum amount over the rows that have a delay_until or a vault_hours greater than zero. And remember the list is paginated (limit caps at 100): compare against stats.total, which is counted over the whole filter rather than the page, or walk the remaining pages with offset — summing one page and calling it a total is how a reconciliation silently comes out smaller than reality.

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: 600000.
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).
kindstringoptionalproduct (default) or charge. A charge is a payment link with a due date and late fees, served at pay.depixapp.com/c/{id} — it never appears on the public store, the cart, the vitrine or the default product listing. Immutable once created.
due_datestringchargeRequired when kind=charge. First due date, YYYY-MM-DD. It anchors the recurrence. It may be in the past — a retroactive charge starts already overdue, which is what billing last month looks like.
recurrencestringchargenull (one-time) or weekly, monthly, quarterly, semiannual, yearly. Monthly and above anchor on the due day, clamping to the last day of shorter months (31st → Feb 28/29).
late_fine_bpsintegerchargeOne-time late fine in basis points of the base amount (200 = 2%). Default: 0. Maximum: 2000 (20%).
late_interest_monthly_bpsintegerchargeMonthly interest in basis points (100 = 1% per month), accrued pro-rata per day late. Default: 0. Maximum: 1000 (10% per month).

Example

curl -X POST https://api.depixapp.com/api/products \
  -H "Authorization: Bearer $DEPIX_API_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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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://api.depixapp.com/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
kindFilter by row kind: product (default), charge or all. The default keeps charges out of integrations written before they existed; use charge to list charges (each row carries charge_state).
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://api.depixapp.com/api/products?active=1" \
  -H "Authorization: Bearer $DEPIX_API_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://api.depixapp.com/api/products/prd_xxx \
  -H "Authorization: Bearer $DEPIX_API_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. Minimum: 500. Maximum: 600000.
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://api.depixapp.com/api/products/prd_xxx \
  -H "Authorization: Bearer $DEPIX_API_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://api.depixapp.com/api/products/prd_xxx/activate \
  -H "Authorization: Bearer $DEPIX_API_KEY"
curl — deactivate
curl -X POST https://api.depixapp.com/api/products/prd_xxx/deactivate \
  -H "Authorization: Bearer $DEPIX_API_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://api.depixapp.com/api/products/prd_xxx/checkouts?status=completed" \
  -H "Authorization: Bearer $DEPIX_API_KEY"

The response follows the same format as the checkout listing.

Charges

A charge is a product with kind=charge: same endpoint, same checkout, same webhooks, with three differences — it has a due date, it can carry a late fine and interest, and it lives on a private link that never shows up on your public store. It is the right shape for rent, tuition, an instalment — anything that falls due on a date and can be paid late.

Create it with POST /api/products passing kind: "charge" and due_date; list with GET /api/products?kind=charge. The link comes back in payment_url as https://pay.depixapp.com/c/prd_xxx — addressed by id (not by slug), served with noindex, and its link preview in messaging apps shows your store name and the charge title, never the amount. Every other product endpoint (GET, PATCH, activate/deactivate, product checkouts) works on charges unchanged.

If you have direct DePix settlement enabled, the charge page also offers Pagar com DePix next to Pix — the same flow as the store and product pages. On the DePix rail the amount charged is the day's total (original amount + late fine + interest), with your discount applied to that total, and settlement joins the charge's queue exactly like a Pix payment. That rail asks the payer for no CPF/CNPJ.

How the amount is computed

The amount is not fixed: it is computed when the payer opens the link and generates the QR.

Formula
days_late = calendar days after the due date (America/Sao_Paulo timezone)
fine      = base_amount × late_fine_bps / 10000              // one time only
interest  = base_amount × late_interest_monthly_bps / 10000 × days_late / 30
total     = min(base_amount + fine + interest, 600000)        // per-transaction cap

The due date itself does not count as late — lateness starts the next day. There is no rolling to the next business day: Pix runs 24/7. Interest is linear (pro-rata per day), never compounded. There is no inflation adjustment.

Recurrence and FIFO settlement

With recurrence, a charge becomes a series of due dates anchored on due_date, and the same link works forever. Each payment settles the oldest unpaid cycle; the next visit shows the following one. The cycle each checkout settled is stamped in metadata.charge_cycle, and the webhook carries data.product_id — together they tell you exactly which month was paid.

Two rules decide which cycle a QR bills, and they are worth reading before you integrate:

  • A live QR reserves its cycle. Amount and cycle are frozen when the QR is minted, but the position in the series is resolved at settlement. If two live QRs were priced as the same cycle and both got paid, the second payer would carry the fine and interest of a cycle that was not late. So an un-expired QR counts toward the position: the next QR is priced for the cycle it will actually settle. Expired QRs do not count.
  • The position never moves backwards. What counts is a cycle that ever settled. A reversal (MED, payer-mismatch refund) therefore does not make the link re-bill a cycle that is still paid, nor apply the wrong cycle's late fees — and it does not re-bill the reversed cycle on its own. That is the merchant's decision: you receive checkout.cancelled, you see the sale cancelled, and you decide.

charge_state

Present on every row of GET /api/products?kind=charge and in the 201 response of a charge checkout.

Example — a charge 10 days late
{
  "settled":              false,      // true = one-time charge already paid (other fields absent)
  "cycle_due_date":       "2026-08-05",  // current cycle (oldest unpaid)
  "days_late":            10,
  "base_cents":           250000,
  "fine_cents":           5000,
  "interest_cents":       833,
  "total_today_cents":    255833,   // what a QR created now charges
  "capped":               false,      // true = base + fees exceeded the cap and was clamped
  "status":               "late",      // late | due_today | upcoming
  "open_past_due_cycles": 1,         // > 1 = cycles piled up
  "in_flight":            false       // a paid Pix for this charge is settling
}

Charge-specific errors

CodeHTTPWhen
charge_already_paid409One-time charge already settled — there is no cycle left to pay.
charge_payment_in_progress409A paid Pix for this charge is still settling. Issuing another QR now would mean paying twice.
charge_payment_pending409An un-expired QR already reserves the only cycle left (one-time charge). This is not "already paid": nobody has paid anything yet.
Late fees are your configuration and your responsibility. DePix App applies the formula you define and only validates sanity bounds (fine up to 20%, interest up to 10% per month). We give no legal advice on what your contract may charge.

Public product

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

GET /api/products/:id/public
curl
curl https://api.depixapp.com/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

Parameters

FieldTypeDescription
payer_tax_numberstringrequiredCPF or CNPJ of whoever pays the Pix, with or without punctuation. Must be a real, registered CPF/CNPJ — the payment processor validates beyond the checksum when generating the QR. Required on the pix rail only.
payment_methodstringoptionalpix (default) or depix — see Receive DePix directly. The depix rail takes no CPF/CNPJ.
expected_discount_pctintegeroptionalOn the depix rail only: the discount (0–90) your page displayed. Different from the current one? discount_changed (409).
curl
curl -X POST https://api.depixapp.com/api/products/prd_xxx/checkout \
  -H "Content-Type: application/json" \
  -d '{ "payer_tax_number": "52998224725" }'

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

🛈 Errors here are written for the payer. The caller is an anonymous third party, not the merchant. A refusal only keeps its detailed message and its error.details when it describes the payer, what the payer typed, or the thing being paid — which covers every code listed above. Refusals that belong to the merchant's account (receiving limits, onboarding gates, account configuration) answer a fixed neutral message with error.details omitted. The error.code is always the real one. If your integration needs the numbers, make the authenticated merchant call at POST /api/checkouts.

Merchant page

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

GET /api/merchants/:username/public
curl
curl https://api.depixapp.com/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: 600000.
payer_tax_numberstringrequiredCPF or CNPJ of whoever pays the Pix, with or without punctuation. Must be a real, registered CPF/CNPJ — the payment processor validates beyond the checksum when generating the QR. Required on the pix rail only.
payment_methodstringoptionalpix (default) or depix — see Receive DePix directly. The depix rail takes no CPF/CNPJ.
expected_discount_pctintegeroptionalOn the depix rail only: the discount (0–90) your page displayed. Different from the current one? discount_changed (409).
curl
curl -X POST https://api.depixapp.com/api/merchants/joao/checkout \
  -H "Content-Type: application/json" \
  -d '{ "amount": 5000, "payer_tax_number": "52998224725" }'

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

🛈 Errors here are written for the payer. Same rule as the product checkout: refusals belonging to the merchant's account answer a neutral message with no error.details. The full numbers are only returned on the authenticated call, at POST /api/checkouts.

Receive DePix directly (Liquid)

Every checkout can be charged on one of two rails, picked with the payment_method field. The default (pix) is the usual Pix QR. The alternative (depix) charges directly in DePix: the payer sends DePix wallet to wallet on the Liquid network, to an address dedicated to that merchant, and DePix App watches the network to confirm the payment. There is no Pix QR, no payer tax number is asked for, and the money lands straight in the merchant's wallet.

A network payment cannot be undone. Unlike Pix, a wrong transfer cannot be cancelled or returned by us. Always use the exact amount the API returns and send DePix only — any other coin sent to that address is lost. That is why the payment page hands over amount and address ready to use, with nobody typing anything.

How the merchant enables it

Direct receiving is switched on by the account owner inside DePix App, under Receive with DePix (password confirmation). On activation the app creates a dedicated address for these receipts — separate from the address that receives Pix settlements — and the merchant picks a discount from 0% to 90% for whoever pays on this rail. There is no API-key endpoint to enable the rail or to change the discount: it is always the owner, logged in, with a password.

Until the merchant enables it, any creation with payment_method: "depix" answers depix_not_enabled (400) — the Pix rail keeps working normally.

Create a DePix charge

The same endpoints as always (POST /api/checkouts, product checkout and merchant checkout), only the rail changes.

curl
curl -X POST https://api.depixapp.com/api/checkouts \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 9990,
    "payment_method": "depix",
    "description": "Order #124",
    "expires_in": 1800,
    "expected_discount_pct": 10
  }'
JavaScript
const res = await fetch("https://api.depixapp.com/api/checkouts", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_live_<your-key>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    amount: 9990,
    payment_method: "depix",
    description: "Order #124",
    expires_in: 1800,
    expected_discount_pct: 10,
  }),
});
const data = await res.json();
console.log(data.depix.amount, data.depix.uri);
Python
import requests

resp = requests.post(
    "https://api.depixapp.com/api/checkouts",
    headers={"Authorization": "Bearer sk_live_<your-key>"},
    json={
        "amount": 9990,
        "payment_method": "depix",
        "description": "Order #124",
        "expires_in": 1800,
        "expected_discount_pct": 10,
    },
)
data = resp.json()
print(data["depix"]["amount"], data["depix"]["uri"])
Response — 201 Created
{
  "id":             "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
  "status":         "pending",
  "amount":         9990,
  "description":    "Order #124",
  "image_url":      null,
  "expires_at":     "2026-07-29 12:30:00",
  "is_live":        true,
  "payment_url":    "https://pay.depixapp.com/chk_01jxxxxxxxxxxxxxxxxxxxxxx",
  "payment_method": "depix",
  "depix": {
    "address":               "lq1qqw8re6vg9dqfazzsx4h9pkq6trxfmk8n0h0ykr7v9k8xn7pdrjq...",
    "amount_cents":          8991,
    "amount":                "89.91",
    "asset_id":              "02f22f8d9c76ab41661a2729e4752e2c5d1a263012141b86ea98af5472df5189",
    "uri":                   "liquidnetwork:lq1qqw8re6...?amount=89.91&assetid=02f22f8d...&depixid=chk_01jxxx...",
    "discount_pct":          10,
    "original_amount_cents": 9990,
    "detected":              false
  }
}

A DePix checkout carries no pix block — it simply does not exist on this rail. A Pix checkout, in turn, carries no depix block. Always read payment_method before reading the payment payload.

The depix object

FieldTypeDescription
addressstringConfidential Liquid address (lq1…) dedicated to that merchant's direct receipts. In test mode it is a fake address: never send anything to it.
amount_centsintegerExact amount to send, in centavos. It is the face amount minus the merchant's discount and minus an adjustment of up to 99 centavos (always downwards) that makes this value unique among the merchant's open charges.
amountstringThe same amount in the format a wallet signs ("89.91"). Display and transmit it exactly like that, never rounded.
asset_idstringIdentifier of DePix on the Liquid network. Sending any other coin to that address loses the money.
uristringPayment link with address, amount, coin and this checkout's id already embedded (liquidnetwork:…?amount=…&assetid=…&depixid=…). This is what you hand to a wallet — so nobody types an amount by hand. Any BIP21 wallet ignores depixid; the DePix App uses it to re-read the checkout and confirm the address and status before offering to pay, so a URI written by someone else is refused rather than paid. The link never carries the store name: a name inside the URI is a name in the payer's clipboard, which anyone can write. null in test mode.
discount_pctintegerThe merchant's discount applied on this rail, from 0 to 90.
original_amount_centsintegerFace amount, before the discount and the centavo adjustment — the checkout's own amount.
detectedbooleantrue once a matching payment has shown up on the network but is not confirmed yet. It only feeds the "received, confirming" screen; the status stays pending and no webhook fires yet.

Status flow

StatusWhen it happensWebhook
pendingAwaiting payment. depix.detected turns true as soon as the transaction shows up on the network (seconds).
approvedFirst confirmation on the network (~1 minute) with the amount matched to this charge. The money is already in the merchant's wallet — this is the safe point to release the order.checkout.approved
completedSecond confirmation. Terminal.checkout.completed
expiredTerminal, fired 15 minutes after expires_at: that is the grace window so a payment broadcast in the last seconds can still confirm and be credited.checkout.expired

The processing and cancelled statuses are not used on this rail. While the countdown shows 0 and the status is still pending, keep polling: that is the grace window above, not a stuck charge.

Why does the amount have odd centavos?

The payment is identified by its exact amount. So that two open charges of the same merchant never share a value, the API may shave a few centavos — the amount can vary by up to R$ 0.99 downwards, always in the payer's favour. That is why the charged amount (depix.amount_cents) can be a few centavos below the face amount minus the discount. Always charge and reconcile by the amount the API returned, never by an amount you recomputed.

One practical consequence: a payment for a different amount is not credited automatically — it becomes an unmatched receipt (the merchant sees it in the app and gets the checkout.unmatched_payment webhook). The money is in the merchant's wallet; only the automatic link to the order did not happen.

Webhooks and reconciliation

In the checkout.* events of this rail, amount is still the face value, and what was actually paid comes in amount_received (with discount_pct and payment_method next to it). Release the order by amount_received. If you reconcile by reading the charge instead of listening for the webhook, the depix block on GET /api/checkouts/{id} stays there after paymentdepix.amount_cents is the amount that arrived (uri comes back null, since there is nothing left to pay). Receipts that match no charge produce the checkout.unmatched_payment event, sent to the merchant's default_callback_url.

Errors
depix_not_enabled          400   merchant does not accept direct DePix receiving
depix_busy                 409   no unique amount available right now — offer Pix
discount_changed           409   the discount changed (details carries the current values)
depix_address_unsupported  400   receiving address is not a confidential lq1... address
depix_address_conflict     400   the address must be dedicated to this receiving rail
invalid_blinding_key       400   the view key does not match the address

The last three only show up in the activation flow performed by the account owner in the app; an API-key integration never meets them.

In test mode (sk_test_) the DePix rail issues no payable destination: the address is a placeholder, uri comes back null and there is no QR. Complete a test charge with simulate payment, just like on the Pix rail.

Deposit & Withdraw (wallet_* scopes)

Besides receiving through checkouts, an API key with the wallet_* scopes moves the "paying" side of the account: it generates personal-deposit Pix QRs (BRL → DePix on-ramp) and creates DePix → Pix withdrawals (off-ramp). It is the same surface the human UI uses — account limits, delays and verification apply by construction.

SDK-first flows. Deposit and withdraw were designed to be consumed by the official SDK, which mirrors the human UX: sdk.deposit(...) generates the QR and tracks settlement; sdk.withdraw(...) quotes, builds the transaction, signs client-side and tracks settlement. The REST below is the complete contract the SDK itself consumes — fully documented for anyone who prefers to integrate directly.

Fees

Fees are deducted from the amount — what the payer sends is not what the destination receives. Plan around the net figure:

FlowFeeR$ 100,00 becomes
Deposit (BRL → DePix)2% + R$ 0,99R$ 97,01 in DePix
Withdrawal up to R$ 100,001% + R$ 1,00R$ 98,00 in the Pix key
Withdrawal above R$ 100,002%

The two withdrawal tiers meet without a step: at exactly R$ 100,00 both rules cost R$ 2,00. Rates can change — the canonical, always-current table is the fee panel on depixapp.com; treat the values above as illustrative of the shape, not as a contract.

The DePix asset on Liquid

DePix is an issued asset on Liquid mainnet, 8 decimals, pegged 1:1 to BRL. Its asset id is:

02f22f8d9c76ab41661a2729e4752e2c5d1a263012141b86ea98af5472df5189

You need it whenever you build a Liquid transaction yourself instead of letting the SDK do it — in particular the withdrawal fee output, which must pay fee_cents to fee_address as an explicit (unblinded) DePix output in the same transaction. Paying the wrong asset, or paying it blinded, makes the withdrawal fail and can lose the funds.

The deposit flow (on-ramp)

  • 1. POST /api/deposit → Pix QR (qrCopyPaste) + id.
  • 2. The account owner pays the QR in a banking app.
  • 3. Track it by polling GET /api/deposits/:id (5–15s) and/or via the deposit.* webhooks until the terminal status depix_sent — the DePix arrived at the given Liquid address.

Personal deposits count towards account verification. On-ramp alternative: create a checkout against yourself — note that checkout payments do not count towards verification.

The withdrawal flow (off-ramp)

  • 1. POST /api/withdraw → quote with depositAddress (the provider's Liquid address).
  • 2. Send the DePix to the depositAddress from your wallet — signing is always client-side; the API never touches private keys or holds funds.
  • 3. Track via GET /api/withdrawals/:id and/or the withdraw.* webhooks until sent — the Pix arrived at the destination key.

Limits

Two layers, always combined (AND):

  • Account limits — inherited from the owner's account and always prevailing. They cover first deposit, per-transaction amount, a delay on the first deposits, and deposit/withdrawal hard caps. The values depend on the account's verification level and can change — do not hard-code them. On exceed → 400 account_limit_exceeded, with details carrying the current limit_cents/used_cents.
  • Key limits — the spending limits the owner set when creating the wallet_write key (per_tx_limit_cents, daily_limit_cents; see Scopes and limits), visible via GET /api/api-keys. On exceed → 400 key_limit_exceeded (details.limit = per_tx | daily).

Synthetic sandbox (sk_test_)

With a sk_test_ key, deposit and withdraw respond with synthetic payloads marked "sandbox": true: unpayable SANDBOX-…-DO-NOT-PAY strings, sandbox_* ids, zero money, zero provider calls, zero rows created. All account gates and the key's per-transaction limit are still checked — the sandbox teaches the real limits. See Sandbox.

Create deposit

Generates a personal-deposit Pix QR. When the Pix is paid, the DePix is delivered to the given Liquid address. Requires the wallet_write scope. Accepts Idempotency-Key.

POST /api/deposit

Parameters

FieldTypeDescription
amountInCentsintegerrequiredAmount in centavos. Minimum: 500 (R$ 5.00). Maximum: 600000 (R$ 6,000.00). Account and key limits may restrict it further.
depixAddressstringrequiredLiquid address that receives the DePix once the Pix settles.
payer_tax_numberstringrequiredCPF or CNPJ of whoever pays the Pix, with or without punctuation. Must be a real, registered CPF/CNPJ — the payment processor validates beyond the checksum when generating the QR.
Real, registered CPF/CNPJ. The payment processor validates payer_tax_number beyond the checksum when generating the QR. A checksum-valid but unregistered number fails at creation with the generic error "Error generating QR Code. Please contact an admin." — if you get that error on create, the payer's CPF/CNPJ is almost certainly not a real registered one.

Example

curl -X POST https://api.depixapp.com/api/deposit \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dep-order-42" \
  -d '{
    "amountInCents": 5000,
    "depixAddress": "lq1qq...",
    "payer_tax_number": "529.982.247-25"
  }'
const res = await fetch("https://api.depixapp.com/api/deposit", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_live_<your-key>",
    "Content-Type": "application/json",
    "Idempotency-Key": "dep-order-42",
  },
  body: JSON.stringify({
    amountInCents: 5000,
    depixAddress: "lq1qq...",
    payer_tax_number: "529.982.247-25",
  }),
});
const data = await res.json();
console.log(data.response.qrCopyPaste, data.response.id);
import requests

resp = requests.post(
    "https://api.depixapp.com/api/deposit",
    headers={
        "Authorization": "Bearer sk_live_<your-key>",
        "Idempotency-Key": "dep-order-42",
    },
    json={
        "amountInCents": 5000,
        "depixAddress": "lq1qq...",
        "payer_tax_number": "529.982.247-25",
    },
)
data = resp.json()
print(data["response"]["qrCopyPaste"], data["response"]["id"])
$ch = curl_init("https://api.depixapp.com/api/deposit");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_live_<your-key>",
        "Content-Type: application/json",
        "Idempotency-Key: dep-order-42",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "amountInCents" => 5000,
        "depixAddress" => "lq1qq...",
        "payer_tax_number" => "529.982.247-25",
    ]),
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
echo $data["response"]["qrCopyPaste"];
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer sk_live_<your-key>");
client.DefaultRequestHeaders.Add("Idempotency-Key", "dep-order-42");

var payload = new {
    amountInCents = 5000,
    depixAddress = "lq1qq...",
    payer_tax_number = "529.982.247-25"
};

var res = await client.PostAsync(
    "https://api.depixapp.com/api/deposit",
    new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
);
var json = await res.Content.ReadAsStringAsync();
Console.WriteLine(json);
body := `{"amountInCents":5000,"depixAddress":"lq1qq...","payer_tax_number":"529.982.247-25"}`

req, _ := http.NewRequest("POST", "https://api.depixapp.com/api/deposit", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer sk_live_<your-key>")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "dep-order-42")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
require "net/http"
require "json"

uri = URI("https://api.depixapp.com/api/deposit")
req = Net::HTTP::Post.new(uri, {
  "Authorization" => "Bearer sk_live_<your-key>",
  "Content-Type" => "application/json",
  "Idempotency-Key" => "dep-order-42",
})
req.body = { amountInCents: 5000, depixAddress: "lq1qq...",
             payer_tax_number: "529.982.247-25" }.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 = """
    {"amountInCents":5000,"depixAddress":"lq1qq...","payer_tax_number":"529.982.247-25"}""";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.depixapp.com/api/deposit"))
    .header("Authorization", "Bearer sk_live_<your-key>")
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "dep-order-42")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Response — 200 OK
{
  "async": false,
  "response": {
    "qrCopyPaste": "00020126580014br.gov.bcb.pix...",   // EMV payload for the QR code
    "qrImageUrl":  "https://depix.eulen.app/qr/qr-id-456.png",
    "id":          "qr-id-456"                          // use it in GET /api/deposits/:id
  }
}
Response — 200 OK (sandbox, sk_test_)
{
  "async": false,
  "response": {
    "qrCopyPaste": "SANDBOX-DEPIX-TEST-MODE-DO-NOT-PAY-a1b2c3d4e5f60708",
    "qrImageUrl":  null,
    "id":          "sandbox_3uw_a1b2c3d4e5f60708",   // sandbox_<amount36>_<hex> — 5000 → 3uw
    "sandbox":     true
  }
}
Provider rejections arrive with HTTP 400. If the Pix provider refuses the operation, the response is 400 with error.code = "validation_error" and the provider message preserved in response.errorMessage (same behavior as POST /api/withdraw). Program against the HTTP status: 2xx = QR issued, 4xx = rejected. The human-readable message stays in response.errorMessage.

Deposit status

Reads the status of a deposit created via POST /api/deposit. Ownership is enforced: another account's id → 404. Poll every 5–15 seconds until a terminal status — depix_sent is the terminal success.

GET /api/deposits/:id
curl
curl https://api.depixapp.com/api/deposits/qr-id-456 \
  -H "Authorization: Bearer $DEPIX_API_KEY"
Response — 200 OK
{
  "id":           "qr-id-456",
  "type":         "deposit",
  "amount_cents": 5000,
  "status":       "depix_sent",
  "created_at":   "2026-07-01 12:00:00",
  "updated_at":   "2026-07-01 12:34:56",
  "rejection_reasons": []   // always present; [] when the deposit was not refused
}

Possible statuses

StatusTerminalMeaning
pendingQR generated; Pix not paid yet.
under_reviewPix paid; payment in pre-settlement review.
pending_pix2faPix paid; waiting for the payer to complete the Pix 2FA.
approvedPix approved by the provider; DePix not yet sent.
delayedSettlement held by the delay policy (new accounts/high values).
will_refundRefund flow started; the deposit will be refunded.
depix_sentyesSuccess: DePix delivered to the target Liquid address.
refundedyesDeposit refunded to the payer.
canceledyesCanceled by the provider.
erroryesProcessing error at the provider.
expiredyesQR expired unpaid.

Refund reasons (rejection_reasons)

The response always carries rejection_reasons as an array — [] when the deposit was not refused. A polling agent can therefore read the field unconditionally, without existence checks. It is populated when the payment was refunded or held by the provider (typically on the will_refund, refunded and error statuses). New codes may appear — display unknown values as-is. Withdrawals have no rejection_reasons.

CodeMeaning
PAYER_MISMATCHPayment made with a CPF/CNPJ different from the one provided on the deposit.
PAST_DAILY_LIMITPayer's daily limit exceeded.
BLOCKED_USERUser blocked by the provider.
HIGH_VELOCITYToo many transactions from the payer in a short period.
Sandbox: with sk_test_, a sandbox_* id always returns the fixed synthetic response { "id": "sandbox_3uw_a1b2c3d4e5f60708", "type": "deposit", "amount_cents": 5000, "status": "depix_sent", "created_at": "2026-01-01 00:00:00", "updated_at": "2026-01-01 00:00:00", "sandbox": true, "rejection_reasons": [] } — same shape as the live response (includes amount_cents, decoded from the id, plus deterministic timestamps) so you can exercise the full polling loop in test mode. amount_cents is null only for legacy ids with no embedded amount. Any other id via sk_test_404; sandbox_* ids via a live key or JWT → 404.

Create withdrawal

Quotes a DePix → Pix withdrawal: the response carries the provider's Liquid address you send the DePix to. After broadcasting the transaction, track the status. Requires the wallet_write scope. Accepts Idempotency-Key.

POST /api/withdraw

Parameters

Send exactly one of depositAmountInCents ("you send" mode) and payoutAmountInCents ("you receive" mode) — the same two modes of the human UI.

FieldTypeDescription
pixKeystringrequiredDestination Pix key (email, phone, CPF/CNPJ or random key).
depositAmountInCentsintegerone of the two"You send" mode: how much DePix you deliver, in centavos. Minimum: 500. Maximum: 600000 (R$ 6,000.00). Mutually exclusive with payoutAmountInCents.
payoutAmountInCentsintegerone of the two"You receive" mode: how much the destination key receives, in centavos. Minimum: 500. Maximum: 588000 (fee-adjusted). Mutually exclusive with depositAmountInCents.
taxNumberstringrequiredCPF or CNPJ of the destination Pix key holder.

Example

curl -X POST https://api.depixapp.com/api/withdraw \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-order-42" \
  -d '{
    "pixKey": "someone@example.com",
    "depositAmountInCents": 10000,
    "taxNumber": "529.982.247-25"
  }'
const res = await fetch("https://api.depixapp.com/api/withdraw", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_live_<your-key>",
    "Content-Type": "application/json",
    "Idempotency-Key": "wd-order-42",
  },
  body: JSON.stringify({
    pixKey: "someone@example.com",
    depositAmountInCents: 10000,
    taxNumber: "529.982.247-25",
  }),
});
const data = await res.json();
console.log(data.response.withdrawalId, data.response.depositAddress);
import requests

resp = requests.post(
    "https://api.depixapp.com/api/withdraw",
    headers={
        "Authorization": "Bearer sk_live_<your-key>",
        "Idempotency-Key": "wd-order-42",
    },
    json={
        "pixKey": "someone@example.com",
        "depositAmountInCents": 10000,
        "taxNumber": "529.982.247-25",
    },
)
data = resp.json()
print(data["response"]["withdrawalId"], data["response"]["depositAddress"])
$ch = curl_init("https://api.depixapp.com/api/withdraw");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_live_<your-key>",
        "Content-Type: application/json",
        "Idempotency-Key: wd-order-42",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "pixKey" => "someone@example.com",
        "depositAmountInCents" => 10000,
        "taxNumber" => "529.982.247-25",
    ]),
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
echo $data["response"]["depositAddress"];
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer sk_live_<your-key>");
client.DefaultRequestHeaders.Add("Idempotency-Key", "wd-order-42");

var payload = new {
    pixKey = "someone@example.com",
    depositAmountInCents = 10000,
    taxNumber = "529.982.247-25"
};

var res = await client.PostAsync(
    "https://api.depixapp.com/api/withdraw",
    new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
);
var json = await res.Content.ReadAsStringAsync();
Console.WriteLine(json);
body := `{"pixKey":"someone@example.com","depositAmountInCents":10000,"taxNumber":"529.982.247-25"}`

req, _ := http.NewRequest("POST", "https://api.depixapp.com/api/withdraw", strings.NewReader(body))
req.Header.Set("Authorization", "Bearer sk_live_<your-key>")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "wd-order-42")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
require "net/http"
require "json"

uri = URI("https://api.depixapp.com/api/withdraw")
req = Net::HTTP::Post.new(uri, {
  "Authorization" => "Bearer sk_live_<your-key>",
  "Content-Type" => "application/json",
  "Idempotency-Key" => "wd-order-42",
})
req.body = { pixKey: "someone@example.com", depositAmountInCents: 10000,
             taxNumber: "529.982.247-25" }.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 = """
    {"pixKey":"someone@example.com","depositAmountInCents":10000,"taxNumber":"529.982.247-25"}""";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.depixapp.com/api/withdraw"))
    .header("Authorization", "Bearer sk_live_<your-key>")
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "wd-order-42")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Response — 200 OK
{
  "response": {
    "withdrawalId":              "wd-123",           // use it in the status GET
    "depositAddress":            "lq1qq2v9wxkyz...", // send the withdrawal DePix here
    "depositAmountInCents":      9900,               // what the provider receives
    "payoutAmountInCents":       9700,               // what the Pix key receives
    "totalDepositAmountInCents": 10000,              // gross wallet outflow (provider + fee)
    "split":                     { "address": "ex1qfee...", "amountCentavos": 100 },
    "fee_cents":                 100,                // platform fee — MANDATORY in the same transaction
    "fee_address":               "ex1qfee..."        // fee address (non-confidential form)
  }
}

The API-key response includes fee_cents and fee_address: the platform fee your Liquid transaction must pay as a second explicit output (unblinded, DePix asset) to fee_address, in the same transaction as the main output to depositAddress. Pay fee_address exactly as given — it comes in the non-confidential form (ex1...) on purpose: a confidential/blinded output cannot be verified and counts as an unpaid fee. The fee is verified automatically on the Liquid transaction that pays the withdrawal.

Warning: Sending the withdrawal funds without including, in the same transaction, the fee amount will result in: a failed withdrawal and loss of funds.
Response — 200 OK (sandbox, sk_test_)
{
  "response": {
    "withdrawalId":         "sandbox_0011223344556677",
    "depositAddress":       "SANDBOX-LIQUID-ADDRESS-DO-NOT-PAY",
    "depositAmountInCents": 10000,
    "payoutAmountInCents":  9800,                // fixed synthetic 2% rate; the real rate varies
    "fee_cents":            100,                 // deterministic synthetic 1% fee
    "fee_address":          "SANDBOX-LIQUID-FEE-ADDRESS-DO-NOT-PAY",
    "sandbox":              true
  }
}

Withdrawal status

Reads the status of a withdrawal created via POST /api/withdraw. Ownership is enforced: another account's id → 404. Poll every 5–15 seconds until a terminal status — sent is the terminal success.

GET /api/withdrawals/:id
curl
curl https://api.depixapp.com/api/withdrawals/wd-123 \
  -H "Authorization: Bearer $DEPIX_API_KEY"
Response — 200 OK
{
  "id":           "wd-123",
  "type":         "withdraw",
  "amount_cents": 10000,                // sending side (depositAmountInCents)
  "status":       "sent",
  "created_at":   "2026-07-01 10:00:00",
  "updated_at":   "2026-07-01 10:00:00",
  "liquid_txid":  "abab...ab"           // present once the Liquid transfer is detected on-chain
}

Possible statuses

StatusTerminalMeaning
unsentCreated; the DePix has not reached the provider yet.
sendingDePix received; Pix payout in flight.
sentyesSuccess: Pix delivered to the destination key.
refundedyesRefunded.
cancelledyesCancelled.
erroryesProcessing error at the provider.
expiredyesThe DePix never arrived — swept by the cron.
Sandbox: with sk_test_, a sandbox_* id always returns { "id": "sandbox_7ps_…", "type": "withdraw", "amount_cents": 10000, "status": "confirmed", "created_at": "2026-01-01 00:00:00", "updated_at": "2026-01-01 00:00:00", "sandbox": true } — a fixed synthetic state, sandbox-only (status: "confirmed" is outside the live enum); amount_cents is decoded from the id (null for legacy ids that carry no amount). Any other id via sk_test_404; sandbox_* ids via a live key or JWT → 404.

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). Deposits and withdrawals created via API key also fire webhooks (deposit.*/withdraw.* events) to the merchant's default_callback_url — see Events.

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",
    "product_id":     null,
    "status":         "processing",
    "amount":         2990,
    "processing_at":  "2025-06-01T15:02:00.000Z",
    "metadata":       { "order_id": "ORD-123" }
  }
}

checkout.approved

Fired when the payment is approved by the bank and is awaiting final settlement in DePix.

{
  "event": "checkout.approved",
  "data": {
    "event_id":     "evt_01jxxxxxxxxxxxxxxxxxxxxxx",
    "id":           "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
    "product_id":   null,
    "status":       "approved",
    "amount":       2990,
    "approved_at":  "2025-06-01T15:05: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",
    "product_id":    null,
    "status":        "completed",
    "amount":        2990,
    "completed_at":  "2025-06-01T15:22:00.000Z",
    "metadata":      { "order_id": "ORD-123" }
  }
}

checkout.cancelled

Fired when the checkout payment is cancelled, charged back, or refunded by the Pix provider.

{
  "event": "checkout.cancelled",
  "data": {
    "event_id":      "evt_01jxxxxxxxxxxxxxxxxxxxxxx",
    "id":            "chk_01jxxxxxxxxxxxxxxxxxxxxxx",
    "product_id":    null,
    "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",
    "product_id":  null,
    "status":      "expired",
    "amount":      2990,
    "expires_at":  "2025-06-01T15:30:00.000Z",
    "metadata":    { "order_id": "ORD-123" }
  }
}

checkout.unmatched_payment

Direct DePix rail only: a payment landed on the merchant's address that matches no charge (an amount different from the one quoted, a payment after the grace window, or a second payment for an already settled charge). The money is in the merchant's wallet — only the automatic link to the order did not happen, and reconciling it is a human decision. Since there is no checkout, delivery goes to the merchant's default_callback_url.

{
  "event": "checkout.unmatched_payment",
  "data": {
    "event_id":     "evt_01jxxxxxxxxxxxxxxxxxxxxxx",
    "id":           "dout_9f8e7d6c5b4a",
    "type":         "depix_output",
    "status":       "unattributed",
    "amount_cents": 8997,
    "txid":         "abab…",
    "vout":         1,
    "first_seen_at": "2026-07-29 12:07:00",
    "reason":       "no_matching_checkout"
  }
}

status is either unattributed (no open charge with that exact amount) or duplicate (a second payment for an already settled charge). first_seen_at is when the payment was seen on the merchant's address — possibly a few minutes before this notification — and it is the same time the app shows. reason spells out why it was not credited (no_matching_checkout, duplicate_payment, ambiguous_candidates, value_not_whole_cents, max_attributions_per_tx, transition_lost); reconcile on id, not on that string.

The checkout.* events of the direct DePix rail also carry payment_method, amount_received (what was actually paid, in centavos) and discount_pct. The amount field is still the face value — release the order by amount_received.

deposit.* and withdraw.* events

Deposits and withdrawals created via API key (wallet_write scope) fire one event per real status transition, named 1:1 after the raw status: deposit.<status> / withdraw.<status>. Human operations (dashboard/SPA) never dispatch. Delivery goes to the merchant's default_callback_url — without it configured, nothing is sent. Sandbox operations (sk_test_) create no rows and therefore never fire webhooks.

Creating a deposit or withdrawal does not emit a webhook — the first event you receive is the next status change. The initial-state events (deposit.pending, withdraw.unsent) therefore only appear on the uncommon reversal back to that state.

EventFired when
deposit.pendingDeposit awaiting Pix payment (initial state).
deposit.under_reviewPix paid; payment in pre-settlement review.
deposit.pending_pix2faWaiting for the payer to complete the Pix 2FA.
deposit.approvedApproved by the provider; DePix not yet sent.
deposit.delayedSettlement held by the delay policy.
deposit.will_refundRefund flow started.
deposit.depix_sentTerminal success: DePix delivered to the target address.
deposit.refundedRefunded to the payer (terminal).
deposit.canceledCanceled by the provider (terminal).
deposit.errorProcessing error at the provider (terminal).
deposit.expiredQR expired unpaid (terminal).
withdraw.unsentWithdrawal awaiting the DePix transfer (initial state).
withdraw.sendingDePix received; Pix payout in flight.
withdraw.sentTerminal success: Pix delivered to the destination key.
withdraw.refundedRefunded (terminal).
withdraw.cancelledCancelled (terminal).
withdraw.errorProcessing error at the provider (terminal).
withdraw.expiredThe DePix never arrived — swept by the cron (terminal).

The payload uses the same English shape as the status GETs, plus event_id — the dedupe key (same value as the X-DePix-Event-Id header, stable across retries). deposit.* payloads always carry rejection_reasons (array, [] when the deposit was not refused) — populated on deposit.refunded, deposit.will_refund and deposit.error; empty on every other event. withdraw.* payloads do not have this field:

{
  "event": "deposit.depix_sent",
  "data": {
    "id":           "qr-id-456",
    "type":         "deposit",
    "amount_cents": 5000,
    "status":       "depix_sent",
    "created_at":   "2026-07-01 12:00:00",
    "updated_at":   "2026-07-01 12:34:56",
    "rejection_reasons": [],   // e.g. ["PAYER_MISMATCH"] on deposit.refunded
    "event_id":     "evt_9f8e7d6c5b4a"
  }
}
{
  "event": "withdraw.sent",
  "data": {
    "id":           "wd-123",
    "type":         "withdraw",
    "amount_cents": 10000,
    "status":       "sent",
    "created_at":   "2026-07-01 10:00:00",
    "updated_at":   "2026-07-01 10:00:00",
    "liquid_txid":  "abab...ab",
    "event_id":     "evt_1a2b3c4d5e6f"
  }
}

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.

Deposit and withdrawal in test mode

  • POST /api/deposit and POST /api/withdraw with sk_test_ respond with synthetic payloads marked "sandbox": true: unpayable SANDBOX-…-DO-NOT-PAY strings, sandbox_* ids, a fixed synthetic 2% quote on withdrawals.
  • Zero money and zero rows: no Pix provider call, nothing written — the account's economic counters are untouched.
  • Real validations and limits are exercised: the account gates and the key's per-transaction limit run normally. The daily limit never accrues (nothing is stored).
  • GET /api/deposits/:id and GET /api/withdrawals/:id with a sandbox_* id return a fixed synthetic status (depix_sent / confirmed) to train the polling loop.
  • No rows → sandbox operations never fire deposit.*/withdraw.* webhooks. To test webhooks end to end, use a checkout + simulate-payment.
  • A sk_test_ key never reads or writes live data: any id that is not sandbox_*404.
The sandbox is independent from production. You can create and simulate 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://api.depixapp.com/api/checkouts \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 1000, "payer_tax_number": "529.982.247-25", "callback_url": "https://my-store.com/webhook" }'

# 2. Simulate the payment
curl -X POST https://api.depixapp.com/api/checkouts/chk_01jxxxxxxxxxxxxxxxxxxxxxx/simulate-payment \
  -H "Authorization: Bearer $DEPIX_API_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://api.depixapp.com/api/me \
  -H "Authorization: Bearer $DEPIX_API_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"
}

Edit store profile (PATCH /api/merchants/me)

Partially updates the authenticated merchant's profile. Send only the fields you want to change. Accepts both the dashboard JWT and an API key with the merchant_write scope.

PATCH /api/merchants/me
By API key, only these 5 light fields. liquid_address (redirects money), cnpj, and the account password are not editable by a key — sending them on an API-key-authenticated request returns 400 with error.code = "validation_error" and details.field naming the rejected field. split_address is never editable through this endpoint (admin only). These sensitive fields only change via the web dashboard, by the account owner (the Liquid address also requires the password).

Parameters (all optional — send only what changes)

FieldTypeDescription
business_namestringNew business name (2–100 chars). Changing it rotates the public merchant_slug and retires the old one — any payment link or checkout URL built on the old slug will return 404. Rename with that in mind.
websitestringNew store website (normalized to https://). Sending null or empty clears the field.
logo_urlstringNew HTTPS logo URL. null or empty clears the field.
default_callback_urlstringNew default HTTPS webhook endpoint for deposit.* / withdraw.* events. null or empty clears the field.
default_redirect_urlstringNew default HTTPS post-payment redirect for the store's customers. null or empty clears the field.
Every PATCH made by an API key emails the owner, naming the changed fields and the key id (compensating control). Read the current profile with GET /api/me.
curl
curl -X PATCH https://api.depixapp.com/api/merchants/me \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "default_redirect_url": "https://shop.example.com/thanks" }'
Response — 200 OK
{
  "success":        true,
  "merchant_slug":  "loja-do-joao"       // changes only if business_name changed
}
Response — 400 (owner-only field, via API key)
{
  "response": { "errorMessage": "Este campo só pode ser alterado pelo dono da conta no painel web." },
  "error": {
    "code":        "validation_error",
    "message":     "This field can only be changed by the account owner in the web dashboard.",
    "request_id":  "gru1::abcd-1234",
    "docs_url":    "https://depixapp.com/docs/en/#errors",
    "details":     { "field": "liquid_address" }
  }
}

Key audit log

Every write operation performed with an API key is audited: action, amount, resource, request_id, IP, sandbox flag and idempotent replays — including authenticated denials (e.g. insufficient_scope, account_blocked, with the action suffixed *.denied:<code>). The owner queries each key's history with the endpoint below. Retention: 90 days. GETs and 429 responses are not logged.

GET /api/api-keys/:id/audit
JWT only. This endpoint accepts only the dashboard JWT (owner login) — never an API key. A key cannot see its own audit log.

Query params (all optional)

ParameterDescription
limitResults per page. Default: 50. Minimum: 1. Maximum: 100.
offsetPagination. Default: 0.
curl
curl "https://api.depixapp.com/api/api-keys/a1b2c3d4e5f6/audit?limit=50&offset=0" \
  -H "Authorization: Bearer <dashboard-jwt>"
Response — 200 OK
{
  "audit": [
    {
      "id":           "aud_01jxxxxxxxxxxxxxxxxxxxxxx",
      "action":       "withdraw.create",
      "method":       "POST",
      "path":         "/api/withdraw",
      "status_code":  200,
      "amount_cents": 10000,
      "resource_id":  "wd-123",
      "is_sandbox":   0,
      "is_replay":    0,       // 1 = idempotent replay (handler did not run)
      "ip":           "203.0.113.9",
      "request_id":   "gru1::iad1::v9x4k-1751476800000-abc123",
      "created_at":   "2026-07-02 14:03:11"
    }
  ],
  "total": 123
}

Rate limits

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

EndpointLimitScope
POST /api/checkouts30 / minper IP
POST /api/checkouts/:id/simulate-payment60 / min per IP · 30 / min per keyauthenticated (merchant_write scope)
POST /api/products60 / min per IP · 30 / min per keyauthenticated (merchant_write scope)
POST /api/products/featured60 / min per IP · 30 / min per keyauthenticated (merchant_write scope)
PATCH /api/products/:id60 / min per IP · 30 / min per keyauthenticated (merchant_write scope)
POST /api/products/:id/deactivate60 / min per IP · 30 / min per keyauthenticated (merchant_write scope)
POST /api/products/:id/activate60 / min per IP · 30 / min per keyauthenticated (merchant_write scope)
POST /api/deposit20 / min per IP · 2 / min per keyauthenticated (wallet_write scope)
POST /api/withdraw20 / min per IP · 2 / min per keyauthenticated (wallet_write scope)
GET /api/deposits/:id60 / min per IP · 30 / min per keyauthenticated (wallet_read scope)
GET /api/withdrawals/:id60 / min per IP · 30 / min per keyauthenticated (wallet_read scope)
GET /api/api-keys/:id/audit60 / min per IP · 30 / min per userauthenticated (JWT)
PATCH /api/merchants/me30 / min per IP · 10 / min per keyauthenticated (merchant_write scope)
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 / min per IP · 60 / min per merchantpublic
POST /api/products/:id/checkout10 / min per IP · 60 / min per merchantpublic
GET /api/products/:id/public30 / minper IP (public)
GET /api/merchants/:username/public30 / minper IP (public)
Per merchant (API key)30 / min (default) — configurableshared across all of the key's endpoints; raisable via support
Per key (rate_limit_per_min)Configurable at key creationadditional per-API-key check (1–600 req/min)
  • For requests authenticated with an API key, an additional rate limit applies per merchant (default 30 req/min, shared across all endpoints — contact support if you need it raised) and, when set at creation, per key (rate_limit_per_min).
  • Authenticated writes reachable via API key — create/simulate checkout and the product CRUD — carry a per-endpoint limit (60/min per IP · 30/min per key), listed in the table above. Authenticated reads on the "receiving" side (list checkouts and products, GET /api/me, etc.) have no per-endpoint limit of their own — they are bounded by the per-merchant budget (default 30 req/min, configurable). The JWT/SPA path is limited too, not just the API-key path.
  • On the deposit/withdraw routes, the per-user counter runs per key — each API key gets its own budget, never competing with the owner in the SPA.
  • For public endpoints (no auth), the rate limit is applied only per IP.
  • When the limit is reached, the API returns 429 with error.code = "rate_limited" (or "merchant_rate_limited"), the error.retry_after field and the Retry-After header — wait the indicated seconds before retrying.
  • On wallet_* routes authenticated with an API key, an infrastructure failure in the rate-limit check responds 503 service_unavailable with retry_after (fail-closed) instead of letting the request through.
  • Per-payer velocity: at most 2 QRs per payer CPF/CNPJ inside a sliding 30-minute window (deposits and checkouts count together). From the 3rd on, the API returns 429 with error.code = "payer_velocity_limit", details: { window_minutes, max_per_window } and the Retry-After header telling the seconds until a slot frees up.
If you need higher limits for your integration, contact support.

Open a ticket

Opens a support ticket. The /api/tickets endpoints work for both human users (the dashboard JWT) and AI agents (an sk_live_/sk_test_ key) — no scope is required. A signed sk_ request behaves exactly like the JWT one. Each session or key only ever sees the tickets it created; another principal's tickets are invisible.

Support replies within 1 business day. This is a human queue, not a realtime channel — poll GET /api/tickets/{id} every few minutes, not seconds. For an agent, polling that endpoint is how it reads the answer.
POST /api/tickets

Parameters

FieldTypeDescription
subjectstringrequiredShort summary. 4–120 characters.
bodystringrequiredThe message. 1–4000 characters.
categorystringoptionalOne of bug, question, account, payment, other. Default other.

Ticket fields

FieldValuesDescription
statusawaiting_reply · answered · closedawaiting_reply = support's turn to reply (never auto-closes). answered = support replied and is waiting on you (auto-closes after 2 business days with no user reply). closed = terminal, though an auto-closed ticket reopens if you reply within 7 days.
opener_typehuman · agentWho opened the ticket.
closed_reasonnull · user · admin · autoWhy it closed. null while open.
categorybug · question · account · payment · otherSet at creation.
curl
curl -X POST https://api.depixapp.com/api/tickets \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Withdrawal stuck as pending",
    "category": "payment",
    "body": "Withdrawal wtd_123 has been pending for 2 hours."
  }'
Response — 201 Created
{
  "ticket": {
    "id": "tkt_ab12cd34ef",
    "opener_type": "human",
    "status": "awaiting_reply",
    "subject": "Withdrawal stuck as pending",
    "category": "payment",
    "created_at": "2026-07-22 12:00:00",
    "last_activity_at": "2026-07-22 12:00:00",
    "closed_reason": null,
    "closed_at": null
  }
}
Errors
validation_error   400   bad field (details.field; legacy response.errors[] sibling)
ticket_open_cap    429   too many open tickets (details.max_open)
unauthorized       401   missing / invalid credential

The open-ticket cap is 5 simultaneously open tickets (1 for suspended accounts); error.details.max_open carries the ceiling. Validation errors also expose the legacy response.errors[] sibling alongside error.details.field.

List your tickets

Lists the tickets created by the calling principal (this JWT session or this API key), most-recent activity first. Paginated.

GET /api/tickets

Query parameters

FieldTypeDescription
limitintegeroptionalPage size. Default 50.
offsetintegeroptionalRows to skip. Default 0.
curl
curl "https://api.depixapp.com/api/tickets?limit=50&offset=0" \
  -H "Authorization: Bearer $DEPIX_API_KEY"
Response — 200 OK
{
  "tickets": [
    {
      "id": "tkt_ab12cd34ef",
      "opener_type": "human",
      "status": "awaiting_reply",
      "subject": "Withdrawal stuck as pending",
      "category": "payment",
      "created_at": "2026-07-22 12:00:00",
      "last_activity_at": "2026-07-22 12:00:00",
      "closed_reason": null,
      "closed_at": null
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Ticket detail & messages

Returns one ticket plus its full message thread, oldest first. Polling this endpoint is how you (or an agent) read a support reply.

GET /api/tickets/{id}
curl
curl https://api.depixapp.com/api/tickets/tkt_ab12cd34ef \
  -H "Authorization: Bearer $DEPIX_API_KEY"
Response — 200 OK
{
  "ticket": {
    "id": "tkt_ab12cd34ef",
    "opener_type": "human",
    "status": "answered",
    "subject": "Withdrawal stuck as pending",
    "category": "payment",
    "created_at": "2026-07-22 12:00:00",
    "last_activity_at": "2026-07-22 13:15:00",
    "closed_reason": null,
    "closed_at": null
  },
  "messages": [
    { "id": "tmsg_1", "sender": "user",  "body": "Withdrawal wtd_123 has been pending for 2 hours.", "created_at": "2026-07-22 12:00:00" },
    { "id": "tmsg_2", "sender": "admin", "body": "It settled just now — can you confirm?", "created_at": "2026-07-22 13:15:00" }
  ]
}

A message's sender is one of user, admin or system.

Errors
not_found   404   no such ticket — or it belongs to another principal

A ticket you do not own returns the same 404 not_found as one that does not exist — ownership is never disclosed.

Post a reply

Appends a user message to the thread. Replying to an answered ticket flips it back to awaiting_reply. Replying to an auto-closed ticket within 7 days reopens it. Tickets closed by a user or an admin are terminal — a reply there fails with 409 ticket_closed.

POST /api/tickets/{id}/messages

Parameters

FieldTypeDescription
bodystringrequiredThe reply. 1–4000 characters.
curl
curl -X POST https://api.depixapp.com/api/tickets/tkt_ab12cd34ef/messages \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Confirmed, the funds arrived. Thanks!" }'
Response — 201 Created
{
  "message": { "id": "tmsg_3", "sender": "user", "body": "Confirmed, the funds arrived. Thanks!", "created_at": "2026-07-22 13:20:00" },
  "ticket": {
    "id": "tkt_ab12cd34ef",
    "opener_type": "human",
    "status": "awaiting_reply",
    "subject": "Withdrawal stuck as pending",
    "category": "payment",
    "created_at": "2026-07-22 12:00:00",
    "last_activity_at": "2026-07-22 13:20:00",
    "closed_reason": null,
    "closed_at": null
  }
}
Errors
validation_error   400   bad body (details.field)
not_found          404   no such ticket (or not yours)
ticket_closed      409   ticket was closed by a user or an admin (terminal)

Attach a file

Uploads one file (image, PDF, log or JSON) to the support team — ideal for a screenshot or a diagnostics file when reporting a bug. The bytes go in file_b64 (base64, no data: URI prefix), up to ~3 MB. The file is forwarded to support, not stored or served back — the response records only its name and type. Attaching counts as a reply: an answered ticket returns to awaiting_reply, and a ticket auto-closed within 7 days reopens.

POST /api/tickets/{id}/attachments

Parameters

FieldTypeDescription
filenamestringrequiredFilename shown to support. 1–200 characters.
content_typestringrequiredOne of: image/png, image/jpeg, image/webp, application/pdf, text/plain, application/json.
file_b64stringrequiredThe file bytes, base64-encoded (no data: URI prefix). Max ~3 MB decoded.
captionstringoptionalShort note shown with the file. Max 400 characters.
curl
curl -X POST https://api.depixapp.com/api/tickets/tkt_ab12cd34ef/attachments \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "checkout-error.png", "content_type": "image/png", "file_b64": "iVBORw0KGgo…" }'
Response — 201 Created
{
  "message": {
    "id": "tmsg_5",
    "sender": "user",
    "body": "checkout-error.png",
    "created_at": "2026-07-22 14:05:00",
    "attachment": { "name": "checkout-error.png", "mime": "image/png" }
  },
  "ticket": {
    "id": "tkt_ab12cd34ef",
    "opener_type": "human",
    "status": "awaiting_reply",
    "subject": "Withdrawal stuck as pending",
    "category": "payment",
    "created_at": "2026-07-22 12:00:00",
    "last_activity_at": "2026-07-22 14:05:00",
    "closed_reason": null,
    "closed_at": null
  }
}
Errors
validation_error        400   bad content_type/file_b64 (details.field)
attachment_too_large    413   file above ~3 MB
not_found               404   no such ticket (or not yours)
ticket_closed           409   ticket was closed by a user or an admin (terminal)
attachment_unavailable  503   no support channel right now — retry or use text

Close a ticket

Closes the ticket yourself, setting closed_reason to user. A user-closed ticket is terminal — to continue the conversation you open a new ticket.

POST /api/tickets/{id}/close
curl
curl -X POST https://api.depixapp.com/api/tickets/tkt_ab12cd34ef/close \
  -H "Authorization: Bearer $DEPIX_API_KEY"
Response — 200 OK
{
  "ticket": {
    "id": "tkt_ab12cd34ef",
    "opener_type": "human",
    "status": "closed",
    "subject": "Withdrawal stuck as pending",
    "category": "payment",
    "created_at": "2026-07-22 12:00:00",
    "last_activity_at": "2026-07-22 13:25:00",
    "closed_reason": "user",
    "closed_at": "2026-07-22 13:25:00",
    "rating": null,
    "rated_at": null
  }
}
Errors
not_found       404   no such ticket (or not yours)
ticket_closed   409   already closed

Rate the support

Scores the support on an already closed ticket from 0 to 10 (0 = worst, 10 = best). Rating is write-once: a second call returns 409 already_rated, so the first score is never silently overwritten.

Rating only records the datapoint — it does not reopen the ticket, does not count as a message, and does not change last_activity_at.

POST /api/tickets/{id}/rating
FieldTypeRequiredDescription
ratingintegeryesWhole number from 0 to 10. Out-of-range, fractional or non-numeric values return 400 validation_error.
curl
curl -X POST https://api.depixapp.com/api/tickets/tkt_ab12cd34ef/rating \
  -H "Authorization: Bearer $DEPIX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rating": 10}'
Response — 200 OK
{
  "ticket": {
    "id": "tkt_ab12cd34ef",
    "opener_type": "human",
    "status": "closed",
    "subject": "Withdrawal stuck as pending",
    "category": "payment",
    "created_at": "2026-07-22 12:00:00",
    "last_activity_at": "2026-07-22 13:25:00",
    "closed_reason": "admin",
    "closed_at": "2026-07-22 13:25:00",
    "rating": 10,
    "rated_at": "2026-07-22 13:31:00"
  }
}
Errors
validation_error  400   rating missing, outside 0–10, or not a whole number
not_found         404   no such ticket (or not yours)
ticket_open       409   the ticket is still open
already_rated     409   this ticket has already been rated

Agent accounts

An agent account is a merchant that an AI agent creates and runs on its own — no email, WhatsApp or captcha. It is provisioned and driven entirely through the /api/agents/* endpoints below. Most agents reach them through the DePix SDK (the DepixAgent class), which signs every request for you.

Signed-request authentication

The agent endpoints do not use a static API key. Every request is signed with the account's Ed25519 key (generated and kept locally by the agent). Send these headers on each call:

HeaderValue
x-agent-public-key64-hex raw Ed25519 public key
x-agent-signature128-hex Ed25519 signature of the canonical string
x-agent-nonceUnique per request — single-use, valid ~11 min
x-agent-timestampUnix seconds, within ±300s of server time
Canonical string — newline-joined, then Ed25519-signed
depix-agent-auth:v1
api.depixapp.com
<METHOD>
<path-without-query>
<timestamp>
<nonce>
<sha256hex(raw-request-body)>
The SDK builds and signs this for you — DepixAgent.create() generates the keypair and every agent.* call is signed automatically. The reference below is for building your own client.

Onboarding & graduation

A new agent starts with a sk_test_ sandbox key and a wallet-only starter sk_live_ key capped at R$100/tx and R$500/day. The account graduates — and can then mint full sk_live_ keys — once it is verified. For an agent, verifying the account means proving a domain over DNS: it is the same domain that unlocks receiving from third parties (checkouts, merchant_* scopes), so one unlocks the other.

The 24h delay (inter_deposit_delay_hours) applied to deposits 2–5 holds the settlement (the DePix payout) of those deposits — it does not block creating the next deposit, which can be created right away.

Deposit position is counted per account, not per channel: a Pix checkout counts exactly like a personal QR. And the delay is exact — no other rule extends the 24h on those deposits. Deposit 1 settles immediately; from deposit 6 on, the receive cap and the instant lane govern. A checkout paid over the DePix rail (Liquid) is outside all of these rules, since it never touches the Pix rail.

Signed-request errors
agent_invalid_signature   401   signature does not verify
agent_signature_expired   401   timestamp outside ±300s
agent_replay_detected     401   nonce already used
agent_unknown_key         401   key not registered (authed routes)
account_suspended         403   account paused (mutating routes)
agents_disabled           503   agent onboarding kill-switch is on

Register an agent

Creates an agent account — a merchant, a Liquid receive address and the starter keys. Requires an operator token (op_…) that a human obtains once by connecting an identity (GitHub/Google) in the dashboard — the anti-abuse anchor.

POST /api/agents/register

Parameters

FieldTypeDescription
namestringrequiredDisplay name. 2–100 characters.
operator_tokenstringrequiredThe op_… token from the human operator.
operator_emailstringrequiredNotification email (never a login).
liquid_addressstringrequiredWallet receive address. Immutable after register.
usernamestringoptionalLowercase handle. Defaults to agent_<pubkey-prefix>.
default_callback_urlstringoptionalHTTPS webhook URL.
refstringoptionalReferral username, for attribution only.
curl
curl -X POST https://api.depixapp.com/api/agents/register \
  -H "x-agent-public-key: <64hex>" \
  -H "x-agent-signature: <128hex>" \
  -H "x-agent-nonce: <unique>" -H "x-agent-timestamp: <unix>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Agent",
    "operator_token": "op_...",
    "operator_email": "me@example.com",
    "liquid_address": "lq1..."
  }'
Response — 201 Created
{
  "response": {
    "agent":    { "username": "agent_9f3c2a1b0d", "public_key": "<64hex>", "account_type": "agent" },
    "merchant": { "id": "mrc_...", "merchant_slug": "agent_9f3c2a1b0d", "liquid_address": "lq1...", "webhook_secret": "whsec_..." },
    "keys": {
      "test":         { "id": "...", "key": "sk_test_...", "scopes": "merchant_read merchant_write wallet_read wallet_write" },
      "live_starter": { "id": "...", "key": "sk_live_...", "scopes": "wallet_read wallet_write", "per_tx_limit_cents": 10000, "daily_limit_cents": 50000, "starter": true }
    },
    "graduation": {
      "requires": "domain_proof",                                  // what is missing: prove a domain
      "verify_domain_endpoint": "POST /api/agents/verify-domain",  // where to prove it
      "allowed_tlds_endpoint": "GET /api/agents/domain-tlds"       // accepted TLDs — check first
    },
    "pacing": {
      "first_deposit_max_cents": 2000,
      "unverified_per_tx_max_cents": 10000,
      "inter_deposit_delay_hours": 24,   // delays settlement of deposits 2–5 — does not block creating the next one
      "payer_velocity": { "max_per_window": 2, "window_minutes": 30 },
      "verified_per_tx_deposit_max_cents": 600000,
      "verified_per_tx_withdraw_max_cents": 600000
    }
  }
}

The key plaintexts and webhook_secret are returned only once.

Errors
validation_error       400   bad field (details.field)
invalid_operator_token 401   operator token not valid
operator_token_revoked 403
agent_pubkey_exists    409   this key already has an account
username_taken         409
agents_disabled        503

Create a key

Mints a new API key for the agent's account. Live keys require graduation; merchant_* scopes require a verified domain.

POST /api/agents/keys

Parameters

FieldTypeDescription
livebooleanoptionalDefault false. true mints sk_live_ (requires graduation).
scopesstring[]optionalSubset of merchant_read, merchant_write, wallet_read, wallet_write. Default ["merchant_read","merchant_write"].
labelstringoptionalUp to 100 characters.
per_tx_limit_centsintegeroptionalMin 100. Mandatory when the key has wallet_write (defaults to 10000).
daily_limit_centsintegeroptionalMin 100. Defaults to 50000 with wallet_write.
Response — 201 Created
{
  "response": {
    "id": "...", "key": "sk_live_...", "prefix": "sk_live_",
    "is_live": true, "scopes": "wallet_read wallet_write",
    "per_tx_limit_cents": 10000, "daily_limit_cents": 50000
  }
}

The key plaintext is returned once. Max 5 active keys per kind (live / test).

Errors
validation_error   400   bad scopes / label / limits, or 5-key cap reached
graduation_pending 403   live:true before graduation
domain_required    403   merchant_* scope without a verified domain

Revoke a key

Revokes one of the account's keys. Idempotent — revoking an already-revoked key still succeeds.

POST /api/agents/keys/revoke

Parameters

FieldTypeDescription
idstringrequiredThe key id to revoke. Must belong to this agent.
Response — 200 OK
{ "response": { "id": "...", "revoked": true } }

not_found (404) when the key is not owned by this agent's merchant.

Account status

Reads the account state and graduation progress. Stays available even while suspended, so an agent can read the reason.

Important asymmetry: a suspension of your account keeps this read available (200, with the reason field). The global agent kill-switch, however, answers 503 agents_disabled even for this read — treat 503 agents_disabled as a platform-level pause, not as your own suspension.

GET /api/agents/status
Response — 200 OK
{
  "response": {
    "account_status": "active",          // active | suspended
    "graduated": false,
    "graduation": { "blocked_on": "domain_proof" },  // "domain_proof": prove a domain (your move)
                                                     // "gate_review": verified, waiting on our review
                                                     // null: already graduated
    "keys": [
      { "id": "...", "prefix": "sk_test_", "is_live": false, "starter": false, "scopes": "...", "revoked_at": null }
    ]
    // "reason": "..."  — present only when suspended
  }
}

Webhook delivery logs

Read-only audit of the deliveries of checkout.*, deposit.* and withdraw.* events to this account's callback URLs — what was delivered, retried or failed. The list returns the 50 most recent attempts (newest first) without bodies; fetch one log by id for the request/response payloads. A log owned by another account answers 404.

GET /api/agents/webhook-logs
GET /api/agents/webhook-logs/:id
curl
curl https://api.depixapp.com/api/agents/webhook-logs \
  -H "x-agent-public-key: <64hex>" \
  -H "x-agent-signature: <128hex>" \
  -H "x-agent-nonce: <unique>" -H "x-agent-timestamp: <unix>"
Response — 200 OK (list)
{
  "logs": [
    { "id": "...", "checkout_id": null, "event": "deposit.depix_sent", "url": "https://...", "status_code": 200, "error": null, "attempt": 1, "sent_at": "..." }
  ]
}
Response — 200 OK (detail: adds the bodies)
{
  "log": {
    "id": "...", "event": "checkout.completed", "url": "https://...", "status_code": 200,
    "request_body": "{...}",   // the signed payload sent (X-DePix-Signature covers these bytes)
    "response_body": "{...}",  // what the receiver answered
    "error": null, "attempt": 1, "next_retry_at": null, "sent_at": "..."
  }
}
Errors
merchant_required  403   no ACTIVE merchant profile (e.g. account suspended)
not_found          404   log does not exist or belongs to another account

Verify a domain

Two-phase DNS TXT proof. Unlocks receiving from third parties (checkouts / merchant_* scopes). The domain is normalized to its registrable root (e.g. shop.acme.com.bracme.com.br).

POST /api/agents/verify-domain

Parameters

FieldTypeDescription
domainstringrequiredThe domain to verify. Its TLD must be in the allowlist.
confirmbooleanoptionalOmit for phase 1 (get the token). Send true for phase 2 (check the TXT record).
Phase 1 (no confirm) — 200 OK
{
  "record_name":  "_depix-verify.acme.com.br",
  "record_value": "depix-verify=<32-hex token>"
}
Phase 2 (confirm: true, after adding the TXT record) — 200 OK
{ "verified_domain": "acme.com.br", "verified": true }  // verified: the account is now verified — graduation follows
Errors
validation_error      400   details.field: "domain"
domain_tld_not_allowed 422   details.allowed_tlds: [...]
domain_free_host      422   vercel.app / netlify.app / github.io / ... denied
domain_txt_not_found  422   TXT missing/mismatch — retry after DNS propagates

Domain TLD allowlist

Public. Returns the TLD suffixes accepted by domain verification.

GET /api/agents/domain-tlds
curl
curl https://api.depixapp.com/api/agents/domain-tlds
Response — 200 OK
{ "allowed_tlds": [".com", ".net", ".org", ".io", ".ai", ".dev", ".app", ".com.br", ".br", ".store", ".shop", "..."] }

MCP gateway

The DePix MCP gateway is a hosted Model Context Protocol server that lets any MCP client — Claude, Cursor, ChatGPT — receive Pix without custody: create checkouts and products, and read payment status. It is a thin, stateless client in front of this same REST API; it holds no keys and never moves money.

POST https://mcp.depixapp.com/mcp

Transport is MCP Streamable HTTP (stateless). Package @depixapp/mcp also runs locally over stdio (npx -y @depixapp/mcp).

The gateway is the receive side (no custody). To hold, sign and move funds — deposits, withdrawals, conversions — use the Wallet SDK, which ships its own MCP server (depix-wallet-mcp, stdio) exposing the agent's wallet as tools. Two servers, different blast radius: this gateway cannot move funds, that one can. Instead.

Connect a client

Authenticate with a DePix API key in the Authorization header — sk_test_ for sandbox, sk_live_ for production. The key is forwarded verbatim to the API per request and never stored.

Claude Code (remote HTTP)
claude mcp add --transport http depix https://mcp.depixapp.com/mcp \
  --header "Authorization: Bearer sk_test_YOUR_KEY"
Cursor — ~/.cursor/mcp.json
{
  "mcpServers": {
    "depix": {
      "url": "https://mcp.depixapp.com/mcp",
      "headers": { "Authorization": "Bearer sk_test_YOUR_KEY" }
    }
  }
}
Claude Desktop (local stdio) — claude_desktop_config.json
{
  "mcpServers": {
    "depix": {
      "command": "npx",
      "args": ["-y", "@depixapp/mcp"],
      "env": { "DEPIX_API_KEY": "sk_test_YOUR_KEY" }
    }
  }
}

claude.ai / ChatGPT connect over OAuth (no custom header). Sign in through the connector, then link that login to your DePix account in the dashboard. OAuth sessions are capped to read + merchant scopes and can never move money — use an sk_ key for that. Test any connection with the get_account tool.

Scopes

Each tool needs a scope on the key: merchant_read, merchant_write, wallet_read. A call missing a scope returns insufficient_scope naming what it needs.

Tools

Sixteen tools. All amounts are in centavos. The gateway never creates deposits or withdrawals — that is the Wallet SDK's job.

ToolScopeDoes
create_checkoutmerchant_writeCreate a Pix checkout (needs payer_tax_number).
get_checkoutmerchant_readFetch one checkout by id.
list_checkoutsmerchant_readList/filter checkouts.
wait_for_checkoutmerchant_readServer-side poll until terminal; streams progress.
simulate_checkout_paymentmerchant_writeSandbox-only: mark a checkout paid.
create_productmerchant_writeCreate a reusable payment link.
list_productsmerchant_readList/filter products.
get_productmerchant_readFetch one product + aggregates.
update_productmerchant_writePatch product fields.
activate_product / deactivate_productmerchant_writeToggle a product on/off.
set_featured_productsmerchant_writePin products on the storefront.
list_product_checkoutsmerchant_readCheckouts for a product.
get_accountmerchant_readVerify the key / connection.
get_deposit_statuswallet_readRead a deposit's status.
get_withdrawal_statuswallet_readRead a withdrawal's status.

Source and full reference: github.com/depixapp/depix-mcp (@depixapp/mcp).

Wallet SDK

@depixapp/sdk is a non-custodial Liquid wallet for Node — the seed is generated and encrypted locally, every signature happens on the agent's side, and the backend never holds a key. It ships two classes:

ClassPurpose
DepixWalletMove money: deposit, withdraw, convert, send, balances.
DepixAgentSelf-onboarding: register an account and manage API keys (drives the agent endpoints).
Install
npm install @depixapp/sdk

ESM-only. Node ≥ 22.4, Linux / macOS. Amounts on-chain are bigint 8-decimal sats (R$1.00 = 100_000_000n DePix); Pix amounts are integer BRL cents.

Quickstart

Create a wallet, fund it with Pix, convert to L-BTC — all client-side.

create → deposit → convert
import { DepixWallet } from "@depixapp/sdk";

// create + back up (headless) + open the receive gate
const { wallet } = await DepixWallet.create({
  passphrase: process.env.DEPIX_WALLET_PASSPHRASE, // ≥ 12 chars
  mnemonicSecured: true,
});
await wallet.confirmBackup();

// deposit: the owner pays the QR (needs DEPIX_API_KEY)
const dep = await wallet.deposit({ amountCents: 1000, payerTaxNumber: "OWNER_CPF" });
console.log("Pay this:", dep.qrCopyPaste);
await wallet.waitForDeposit(dep.id, { timeoutMs: 15 * 60_000 }); // always bound human waits

// convert R$5 of DePix to L-BTC (client-side)
await wallet.convert({ from: "DEPIX", to: "LBTC", amount: 500_000_000n });
await wallet.close();

Wallet reference

DepixWallet.create() / open() / restore() return a wallet. Its money and read methods:

MethodDoes
deposit({ amountCents, payerTaxNumber })Create a Pix deposit → { id, qrCopyPaste }. Owner pays; credited net of fees.
withdraw({ pixKey, recipientTaxNumber, amountCents, mode })Pix withdrawal. mode: "send" | "payout".
convert({ from, to, amount, … })Convert between DEPIX / LBTC / USDT / BTC across networks. amount is bigint sats.
Lightning pay / receivePay and receive Lightning invoices. Settlement goes through Boltz submarine swaps — DePix itself is not issued on Lightning, so a Lightning leg is always a swap against L-BTC. Gift-card purchases settle the same way.
send({ asset, amountSats, address })On-chain send of DEPIX / USDT / LBTC.
quote(intent)Read-only route quotes for a conversion.
getBalances()Per-asset balances + a BRL estimate.
getReceiveAddress()A fresh Liquid receive address (after backup is confirmed).
waitForDeposit(id, { timeoutMs })Poll a deposit to settlement. Always pass timeoutMs.
Guardrails run before every signature — default R$100/tx and R$500 per rolling 24h, immutable at runtime. The one custodial route (SideShift cross-network USDt) is always disclosed via custodial: true.

Agent onboarding

DepixAgent drives the agent endpoints — it generates the Ed25519 identity and signs every request. Create it with DepixAgent.create() (or open() to reload).

MethodDoes
register({ name, operatorToken, operatorEmail, liquidAddress, … })Create the account → merchant + starter keys.
status()Account status + graduation progress + keys.
createKey({ live, scopes, … })Mint a new key (returned once).
revokeKey(id)Revoke a key.
rotateWebhookSecret()Rotate the webhook signing secret.

Errors & runtime

Every error extends DepixSdkError with a stable .code; narrow with isDepixSdkError(err, code?). Common codes:

Codes worth handling
BACKUP_REQUIRED         no receive/deposit until the backup is confirmed
API_KEY_REQUIRED        deposit/withdraw need DEPIX_API_KEY
graduation_pending      live key requested before graduation
domain_required         merchant scope without a verified domain
GUARDRAIL_PER_TX_LIMIT  amount over the per-tx guardrail
GUARDRAIL_DAILY_LIMIT   amount over the rolling-24h guardrail
MULTIPLE_ROUTES_AVAILABLE  pick a route id from quote() and retry
POLL_TIMEOUT            a wait*() hit its timeoutMs

The agent-facing spec (exact signatures, routing table, recipes) lives in AGENTS.md; full source at github.com/depixapp/depix-sdk (@depixapp/sdk).