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 My Business area, 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.

Every date and time the API returns — responses and webhooks — comes in one format, RFC 3339 in UTC: "2026-07-01T12:00:00.000Z". The two exceptions are fields that carry a date only, no time: due_date and cycle_due_date on charges ("2026-08-05"). The contents of metadata are never altered — they come back exactly as you sent them.

To get started, create an account, activate your business account in My Business, 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 My Business 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 business 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.

next_action — the next move, machine-readable

Every typed, agent-facing error carries one next move, in error.details.next_action. It exists so an agent that never read this page still knows what to do instead of guessing from the message text. In the MCP tools the same object arrives as data.next_action. That is why every error's docs_url points here.

Error with next_action
{
  "error": {
    "code": "merchant_required",
    "message": "A merchant profile is required for this operation.",
    "request_id": "gru1::iad1::v9x4k-1751476800000-abc123",
    "docs_url": "https://depixapp.com/docs/en/#errors",
    "details": {
      "next_action": {
        "kind": "call_tool",
        "tool": "get_onboarding_status"
      }
    }
  }
}

kind is a closed set of five. Branch on it:

kindWhat to doFields
call_toolCall the named tool and follow what it returns — the path is solvable without leaving the conversation.tool
human_stepOnly a human can clear this. Relay the relay text and wait; repeating the call changes nothing.url, relay
http_callCall the named endpoint before trying again. (Reserved — no code emits it today.)url
waitWait, then repeat the SAME call.retry_after_seconds
reconnectThe connection's credential went missing or expired — reconnect the connector and redo the call. There is no new key to mint.url
  • One action per error, always. Offering two would hand the agent back the choice this contract exists to end.
  • relay — the ready-made text the agent pastes to the human, in pt and en: at most 4 steps, no jargon. It is present exactly when kind is human_step, and never otherwise. It lives in the MCP, not on this server: the MCP's anti-injection boundary derives the message from the code alone and discards free text arriving from here, so copy shipped from this server would be thrown away.
  • wait carries retry_after_seconds as a mirror of error.retry_after (and of the Retry-After header), never a third source: two different numbers for the same deadline make the agent either hammer a closed door or sleep past the reopening.
  • The codes that carry next_action are the ones addressed to the account owner: credentials, the verification ladder, domain proof, blocks and retry limits. Payer-facing routes do not carry it — the caller there is a stranger paying a merchant, and pointing them at the owner's dashboard is the category error the filter exists to prevent.

Code catalog

The cross-cutting codes — the ones any route can return, including every code that carries a next_action. Every row has a stable anchor in the form #error-<code> (e.g. #error-rate_limited). Errors specific to one flow (tickets, charges, the DePix rail, agent signing) live in that flow's section.

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.
invalid_operator_token401The operator code (op_…) is missing, malformed or unknown. The human gets theirs at https://api.depixapp.com/api/agents/oauth/start — that page always shows the same code.
operator_token_revoked403This operator code was revoked. Signing in again does not reactivate it; only support can.
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 app, under AI agents, then retry.
step_up_required403An account that signs in with Google/GitHub has no password, and this sensitive operation asked for one. Re-authenticate with the provider at /api/auth/step-up/start and repeat the call with the freshly issued stepup_ref.
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.
account_exists409An account already exists for this person. Sign in to it instead of creating a second one.
email_in_use409An account already exists for this email. Sign in to it and connect this identity to it.
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).
account_suspended403Agent account paused: reads keep working, routes that change anything do not.
merchant_required403The authenticated account has no active merchant profile.
verification_required403The operation requires a verified account — creating the store is one of them. GET /api/verification lists what is missing.
verification_requirements_not_met409The account does not meet the requirements yet: details.missing says what is left and details.remaining how much.
verification_tax_number_in_use409This CPF/CNPJ has already verified another account. One document verifies one account.
verification_unavailable503Account verification temporarily unavailable — retry_after: 300.
live_access_required403Creating a sk_live_ key without production access approval.
graduation_pending403A capability that unlocks only after the agent account graduates: prove a domain via POST /api/agents/verify-domain, then poll GET /api/agents/status.
domain_required403Receiving from third parties requires a verified domain — prove one via POST /api/agents/verify-domain.
domain_txt_not_found422The DNS TXT challenge was not found or does not match. Create the record and retry after propagation.
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 }.
provider_refused400The settlement provider looked at this request and declined it (payer under compliance review, blocked address, rejected split). Its own reason is in response.errorMessage. Terminal — do not retry. Not to be confused with 503 service_unavailable, which means the provider was unreachable or failed and IS worth retrying. Until 2026-08-04 both arrived as that 503, so a refusal came with the wrong instruction.
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.
agent_pubkey_exists409This public key already belongs to an agent account.
username_taken409Username already in use. Pick another or omit it to get the default.
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.
operator_register_cap_exceeded429Too many agent accounts registered under the same operator code (op_…) inside a rolling window — details: { max_per_window, window_hours }, retry_after until the oldest of them leaves the window. Hitting it without having created those accounts means the code leaked; only support can revoke it.
platform_shutdown503Platform under maintenance (global kill switch) — retry_after: 300.
agents_disabled503The agent program is globally disabled (kill switch) — retry_after: 3600. It pauses the whole platform, never one account.
service_unavailable503Infra dependency unavailable — includes the fail-closed of wallet_* routes via API key when the rate limit cannot be checked — retry_after: 30. It is also the answer when the settlement provider does not respond in time while minting a PIX QR (POST /api/deposit and checkout creation): retrying is worthwhile. On POST /api/deposit that case was a 500 until 2026-08-04.
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",      // all dates are UTC, RFC 3339 format
    "is_live":        1,
    "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
    "delay_until":     null,        // when the money is released, if the sale is held
    "vault_hours":     0            // hours this sale was booked to wait (0 = none; null = no decision recorded)
  }
}

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.
processingThe payer's money reached us. It can sit here from seconds to days — see "A paid sale that hasn't landed yet" below.
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.

A paid sale that hasn't landed yet

processing means the payer's money reached us. Before it lands in your wallet, it may sit for a while in the vault — the protection that holds the money for up to 14 days in case the payment is disputed.

Releasing the product or service at processing is your call. Merchants selling digital goods usually release right away; those shipping physical goods or selling high amounts usually wait for completed.

Two fields tell this story:

  • delay_until — the date the money lands. It is the only date that counts; do not compute created_at + vault_hours. It is null if the sale was not held, and also while the provider has not sent the date yet.
  • vault_hours — how many hours of wait the sale was given when created. 0 = no wait; null = no record (a sandbox sale, or one paid in DePix).

The wait shrinks as the account gains age and received volume:

Account stageWait
1st saleUp to R$ 100: lands immediately. Above that: the level wait, below.
2nd to 5th saleUp to R$ 100: 24 hours. Above that: the level wait, below.
6th onwardsSee below. Sales up to R$ 100 keep landing immediately, up to R$ 500 every 14 days; past that they wait too.
Account levelWait
Level 014 days
Level 110 days
Level 27 days
Level 34 days
Level 43 days

The table is the normal case. The wait that applies to each sale is always its own delay_until.

Sandbox does not simulate the vault: there a checkout goes from pending straight to completed and never passes through processing. In production the same sale can sit in processing for days.

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",   // all dates are UTC, RFC 3339 format
      "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":       1,
      "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
      "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: they stay in processing and carry delay_until and vault_hours — what each field means is in "A paid sale that hasn't landed yet", in the Get checkout section. To reconcile "what I sold but have not received yet", list with ?status=processing and sum amount over rows with a delay_until or a vault_hours greater than zero. The list is paginated (limit caps at 100): compare against stats.total, which counts the whole filter, or walk the next pages with offset — summing one page leaves the reconciliation 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":      1,
      "is_live":     1,
      "position":    0,
      "created_at":  "2025-06-01T15:00:00.000Z",  // all dates are UTC, RFC 3339 format
      "total_checkouts":     12,
      "completed_checkouts": 5,
      "completed_amount":    14950,
      "settled_count":       5,
      "processing_count":    0
    }
  ],
  "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).

payment_url — charge rows only (kind=charge). For products the link is https://pay.depixapp.com/{merchant_slug}/{slug}, returned ready-made by the create response (201).

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":       1,
    "is_live":      1,
    "position":     0,
    "created_at":   "2025-06-01T15:00:00.000Z"   // all dates are UTC, RFC 3339 format
  },
  "stats": {
    "total":            12,
    "completed":        5,
    "pending":          1,
    "completed_amount": 14950
  }
}

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
{ "success": true }

The response does not return the product — read Get product to see the updated state.

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-29T12:30:00.000Z",
  "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://qr.example/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-01T12:00:00.000Z",
  "updated_at":   "2026-07-01T12:34:56.000Z",
  "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.
errorProcessing error at the provider. Not terminal: the provider still owes an outcome, so keep polling — the deposit can still reach depix_sent or refunded.
depix_sentyesSuccess: DePix delivered to the target Liquid address.
refundedyesDeposit refunded to the payer.
canceledyesCanceled by 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-01T00:00:00.000Z", "updated_at": "2026-01-01T00:00:00.000Z", "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: 600000. Mutually exclusive with depositAmountInCents.
taxNumberstringrequiredCPF or CNPJ of the destination Pix key holder.
refundAddressstringoptionalLiquid address where the provider returns the DePix if the Pix payout can't be completed. Use one you control — without it, a refused payout has no automatic way back. The checksum is verified: a malformed address returns 400 with error.details.field = "refundAddress", before the withdrawal is quoted. The refund covers what reached the provider (the response's depositAmountInCents), not the total that left your wallet: the platform fee (fee_cents) is a separate output and does not come back.

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",
    "refundAddress": "lq1qq..."
  }'
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",
    refundAddress: "lq1qq...",
  }),
});
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",
        "refundAddress": "lq1qq...",
    },
)
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",
        "refundAddress" => "lq1qq...",
    ]),
]);
$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",
    refundAddress = "lq1qq..."
};

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","refundAddress":"lq1qq..."}`

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", refundAddress: "lq1qq..." }.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","refundAddress":"lq1qq..."}""";

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":       9800,               // 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": 9900,                // what reaches the provider: the typed amount minus our fee
    "payoutAmountInCents":  9800,                // same arithmetic as live: provider ~1% with a R$ 1 floor
    "totalDepositAmountInCents": 10000,          // what leaves the wallet
    "split": { "address": "SANDBOX-LIQUID-FEE-ADDRESS-DO-NOT-PAY", "amountCentavos": 100 },
    "fee_cents":            100,                 // our 1% fee, in the second output
    "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-01T10:00:00.000Z",
  "updated_at":   "2026-07-01T10:00:00.000Z",
  "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.
errorThe DePix arrived and the Pix payout failed. Not terminal: the provider holds the funds and still owes an outcome, so keep polling — the withdrawal can still reach sent (payout retried) or refunded.
sentyesSuccess: Pix delivered to the destination key.
refundedyesRefunded.
cancelledyesCancelled.
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-01T00:00:00.000Z", "updated_at": "2026-01-01T00:00:00.000Z", "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. The payload carries rejection_reasons with the cause — you can revoke the order and log why without an extra lookup.

{
  "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",
    "rejection_reasons": ["PAYER_MISMATCH"],  // the reason — same catalog as the GET; [] when the provider reported none
    "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-29T12:07:00.000Z",
    "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 — not terminal, can still reach depix_sent or refunded.
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.errorThe DePix arrived and the Pix failed — not terminal, can still reach sent or refunded.
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-01T12:00:00.000Z",
    "updated_at":   "2026-07-01T12:34:56.000Z",
    "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-01T10:00:00.000Z",
    "updated_at":   "2026-07-01T10:00:00.000Z",
    "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 My Business).

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.

Delivery logs

A read-only audit of the checkout.*, deposit.* and withdraw.* deliveries to this account's callback URLs — what was delivered, retried or failed. The list returns the 50 most recent attempts (newest first) without the bodies; fetch one log by id to see the request and response payloads. Another account's log answers 404: ownership is never disclosed.

GET /api/webhook-logs
GET /api/webhook-logs/:id
Accepts either the dashboard JWT or an API key carrying the merchant_read scope — the same audit the dashboard shows, opened to an agent. An agent account uses the keypair-signed twin at GET /api/agents/webhook-logs. Over MCP, the tool is list_webhook_logs.
curl
curl https://api.depixapp.com/api/webhook-logs \
  -H "Authorization: Bearer $DEPIX_API_KEY"
Response — 200 OK (list)
{
  "logs": [
    {
      "id":           "wlog_1",
      "checkout_id":  "chk_abc123",
      "event":        "checkout.completed",
      "url":          "https://store.example.com/hook",
      "status_code":  200,
      "error":        null,
      "attempt":      1,
      "sent_at":      "2026-07-22T12:00:00.000Z"
    },
    {
      "id":           "wlog_2",
      "checkout_id":  null,               // deposit.*/withdraw.* events have no checkout
      "event":        "deposit.depix_sent",
      "url":          "https://store.example.com/hook",
      "status_code":  null,               // null = there never was an HTTP response
      "error":        "fetch timeout",
      "attempt":      2,
      "sent_at":      "2026-07-22T11:58:00.000Z"
    }
  ]
}
Response — 200 OK (detail: includes the bodies)
{
  "log": {
    "id": "wlog_1", "checkout_id": null, "merchant_id": "mrc_1",
    "event": "deposit.depix_sent", "url": "https://store.example.com/hook", "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": "2026-07-22T12:00:00.000Z"
  }
}
Errors
insufficient_scope  403   the key lacks merchant_read
merchant_required   403   the account has no merchant profile
not_found           404   log does not exist or belongs to another account (detail only)

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 marks the mode: false on create responses and on GET /api/me, 0 on checkout and product reads.
  • 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, and on withdrawals the same fee arithmetic as live, with the same fields.
  • 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"
}

Account verification

Says whether this account can already use the merchant tools (checkouts and products) and, if it cannot, exactly what is missing. GET reads the state; POST evaluates the proof and, when everything is satisfied, unlocks the tools. Poll the GET while the person completes the steps, then call the POST.

GET /api/verification
POST /api/verification
Accepts the dashboard JWT or an sk_ API key, and no scope is required. The POST takes no body and is idempotent: an already-verified account answers 200. Over MCP, the tool that reads this is get_onboarding_status, which already turns the steps into instructions for the human.
Verifying unlocks tools, never a limit. How much the account may receive is decided by its level and the Cofre, and does not change when it verifies. Only movements made after this feature went live count — older history never verifies an account by itself.

Response fields

FieldDescription
verifiedtrue when the merchant tools are unlocked on this account.
verified_atWhen verification completed (RFC 3339 UTC). null while unverified.
whatsapp_verified1 once the account has verified its WhatsApp number, 0 otherwise. It gates the first deposit: a human account must clear that step in the app before verifying. Always 0 for an agent account — it is exempt, because it proves a domain instead.
methodWhich proof applies to this account. round_trip: receive from a CPF/CNPJ and withdraw back to the same document. domain: prove a domain by DNS TXT (agent accounts — POST /api/agents/verify-domain).
enabledfalse when automatic verification is switched off platform-wide — contact support instead of retrying.
eligibletrue when every requirement is satisfied and the POST would promote the account. A promotion still has to land: a suspended account never verifies.
requirementsWhat this account's proof requires: deposit_cents, withdraw_cents, min_account_age_days, max_days_between_legs, domain_proof. Members that do not apply to the method come back null.
progress / remaining / missingWhat has been done so far, what is still left in numbers, and the list of what is pending.
stepsThe checklist in the order it must be done — including swapping a little DePix for L-BTC, which is what pays the Liquid network fee: without it a withdrawal cannot even be broadcast. Render steps as given.
curl
curl https://api.depixapp.com/api/verification \
  -H "Authorization: Bearer $DEPIX_API_KEY"
Response — 200 OK (received, still has to withdraw back)
{
  "verified":          false,
  "verified_at":       null,
  "whatsapp_verified": 1,
  "method":            "round_trip",
  "enabled":           true,
  "eligible":          false,
  "requirements": { "deposit_cents": 9500, "withdraw_cents": 8500, "min_account_age_days": 0, "max_days_between_legs": 30, "domain_proof": false },
  "missing":  ["withdraw_leg"],
  "unlocks":  ["checkouts", "products"],
  "steps": [
    { "id": "deposit",      "state": "done",    "target_cents": 9500, "remaining_cents": 0 },
    { "id": "convert_lbtc", "state": "unknown", "target_cents": 500,  "remaining_cents": null },
    { "id": "withdraw",     "state": "pending", "target_cents": 8500, "remaining_cents": 8500 }
  ]
}
POST errors
account_blocked                    403   the account is suspended
verification_tax_number_in_use     409   that CPF/CNPJ already verified another account — one document, one account
(other 409s)                       409   what is missing comes in error.details.missing / error.details.remaining

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-02T14:03:11.000Z"
    }
  ],
  "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-22T12:00:00.000Z",
    "last_activity_at": "2026-07-22T12:00:00.000Z",
    "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-22T12:00:00.000Z",
      "last_activity_at": "2026-07-22T12:00:00.000Z",
      "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-22T12:00:00.000Z",
    "last_activity_at": "2026-07-22T13:15:00.000Z",
    "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-22T12:00:00.000Z" },
    { "id": "tmsg_2", "sender": "admin", "body": "It settled just now — can you confirm?", "created_at": "2026-07-22T13:15:00.000Z" }
  ]
}

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-22T13:20:00.000Z" },
  "ticket": {
    "id": "tkt_ab12cd34ef",
    "opener_type": "human",
    "status": "awaiting_reply",
    "subject": "Withdrawal stuck as pending",
    "category": "payment",
    "created_at": "2026-07-22T12:00:00.000Z",
    "last_activity_at": "2026-07-22T13:20:00.000Z",
    "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-22T14:05:00.000Z",
    "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-22T12:00:00.000Z",
    "last_activity_at": "2026-07-22T14:05:00.000Z",
    "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-22T12:00:00.000Z",
    "last_activity_at": "2026-07-22T13:25:00.000Z",
    "closed_reason": "user",
    "closed_at": "2026-07-22T13:25:00.000Z",
    "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-22T12:00:00.000Z",
    "last_activity_at": "2026-07-22T13:25:00.000Z",
    "closed_reason": "admin",
    "closed_at": "2026-07-22T13:25:00.000Z",
    "rating": 10,
    "rated_at": "2026-07-22T13:31:00.000Z"
  }
}
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. The 24h applies to deposits up to R$ 100 — above that the account's level wait applies, which is longer. Deposit 1 up to R$ 100 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 by connecting an identity (GitHub/Google) at https://api.depixapp.com/api/agents/oauth/start — the anti-abuse anchor. The token is not show-once: that page shows the same token on every sign-in, so losing it costs nothing — just sign in again.

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": 10000,
      "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_send_max_cents": 600000,     // caps depositAmountInCents — what you send
      "verified_per_tx_withdraw_receive_max_cents": 600000  // caps payoutAmountInCents — what lands in the account, net of fees
    }
  }
}

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
operator_register_cap_exceeded 429   operator cap (details.window_hours) — wait retry_after
agents_disabled                503

The 429 is the anti-farm cap on the operator token itself: an op_ may open only a handful of agent accounts inside a rolling window. details.max_per_window and details.window_hours carry the exact shape, and retry_after the seconds until a slot frees. Hitting it when you did not open those accounts means the token has leaked — ask the operator to revoke it. Both register caps, this one and the per-IP one, fail closed: when the infrastructure that does the counting is unavailable the endpoint answers 503 instead of letting the registration through.

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, graduation not landed yet — poll
                                                     // 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", "..."] }

Turn the DePix rail on

Registers (or removes) the dedicated confidential Liquid address the agent's merchant is paid at on the DePix rail — so its checkouts can be paid in DePix on-chain, not only by Pix. The human twin door (POST /api/merchants/me/depix-pay) is password-gated; an agent has neither a password nor a browser, so it proves intent with the keypair signature, never a Bearer key.

Enabling requires the address and the view key (the blinding private key): the key is the proof of possession — the address is rebuilt from it and must match — and it is the only thing that lets us read the amounts arriving there. It is sealed at rest and never returned. The address must be confidential (lq1…) and exclusive: the payout and split addresses are refused. Disabling takes no key at all. It requires a verified account (for an agent, the proven domain) and rejects a suspended merchant.

POST /api/agents/depix-pay

Parameters

FieldTypeDescription
enabledbooleanrequiredtrue registers the dedicated address and starts watching it; false turns it off and deletes the view key (no other field is read or required).
addressstringconditionalRequired when enabled is true. Dedicated confidential Liquid address (lq1…) for this payout. A non-confidential (ex1/base58) address is refused — it would publish every amount received. Must be exclusive: never the payout or split address.
blinding_keystringconditionalRequired when enabled is true. The 32-byte blinding private key of that address, in hex. It grants visibility of only the amounts arriving at that one script — no spending power, no other address. Sent once, stored sealed and deleted when the rail is turned off.
derivation_indexintegeroptionalThe address's wallet derivation index, echoed back so the index stays reserved across a restore. Non-negative integer.
curl — enable
curl -X POST https://api.depixapp.com/api/agents/depix-pay \
  -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 '{
    "enabled": true,
    "address": "lq1qq...",
    "blinding_key": "<64hex>",
    "derivation_index": 12
  }'
curl — disable
curl -X POST https://api.depixapp.com/api/agents/depix-pay \
  -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 '{ "enabled": false }'
Response — 200 OK (enabled)
{
  "response": {
    "depix_pay_enabled": true,
    "depix_pay_address": "lq1qq...",   // the registered address (lower-cased)
    "depix_derivation_index": 12,   // the index sent in, echoed back; null if none
    "depix_discount_pct": 0        // the rail's current discount; this call does not change it
  }
}
Response — 200 OK (disabled)
{
  "response": {
    "depix_pay_enabled": false,
    "view_key_deleted": true,     // at least one view key was deleted
    "pending_addresses": 0       // addresses that kept their key because a DePix checkout there is still open
  }
}
Errors
validation_error          400   malformed body (details.field) — e.g. enabled missing
depix_address_unsupported 400   non-confidential (not lq1…) or undecodable address
depix_address_conflict    400   address not exclusive (payout/split) or already registered
invalid_blinding_key      400   the view key does not match the address
agent_unknown_key         401   signature failed or key not registered
verification_required     403   account not verified — for an agent, prove a domain
account_suspended         403   suspended merchant (account_blocked if blocked)
service_unavailable       503   rail unavailable (KEK unset / anti-replay store down)
agents_disabled           503   agent program globally disabled

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).

It is one server at two levels of access. Connected to this hosted endpoint it is the receive side (no custody) and exposes 26 tools. To hold, sign and move funds — deposits, withdrawals, conversions — run the same package on your own machine (npx -y @depixapp/mcp, stdio): with a local seed it exposes 60. What separates the levels is who holds the seed, not the transport — this endpoint holds none, and so cannot move funds.

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 app, under AI agents. 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.

Terminal commands (local mode)

Running the package on your own machine (npx -y @depixapp/mcp), five commands are yours, not the agent's. None of them is an MCP tool, deliberately: two display the wallet's 12 words — which must never pass through model context or a conversation log — and the other three decide which account the server acts as. As tools, an agent that read a poisoned web page could promote itself from its own test account to yours.

CommandWhat it doesWhen to use itThe guarantee
initCreates the local wallet — with --restore, imports a 12-word seed you already have — and connects the AI apps it finds on the machine to it.Once, before any wallet_* tool can work.It only runs in a real terminal: it refuses when input or output is not one. The passphrase does not appear on screen as you type it, and never goes into the app's config file.
backupDisplays this wallet's 12 words again.When you need to copy the seed onto paper once more.A real terminal or nothing. The passphrase is typed every time, even on a machine that opens the wallet by itself, and the screen is wiped at the end.
loginSigns you in to your own DePix account through the browser (Google or GitHub) and keeps that session encrypted on this machine.When the server should act as you, not as the account the agent opened for itself.The browser does the signing in, and the answer comes back to this same machine (127.0.0.1). No token is ever printed, written to a log, or returned in an error message.
logoutRemoves that login from this machine.When you are done with it, or on a machine you no longer control.It undoes the whole login, including an account use owner choice that would now point at nothing.
account status / account use agent|ownerSays which account is acting and why, or picks one of them.Whenever you are unsure who is spending — and after a login on a machine that already had an agent account.Reading and choosing happen at your terminal; no agent switches the identity. Note: DEPIX_API_KEY in the server's environment beats any choice, and status says so when that is the case.

On package versions 2.8.0 and 2.8.1, login needs DEPIX_WORKOS_CLIENT_ID pointing at the DePix App sign-in application — the id baked into those two versions points at an older one; from 2.8.2 the right id ships baked in and the command needs no configuration. Full detail in the package README: github.com/depixapp/depix-mcp.

Tools

26 tools on the hosted endpoint, and 60 running the same package locally with a seed (npx -y @depixapp/mcp). All amounts are in centavos. The hosted level never creates deposits or withdrawals: with no seed there is nothing to sign.

Gateway — 26 tools (hosted and local)

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.
get_onboarding_statusmerchant_readNarrate what the account still needs to go live: an ordered ladder of steps (create the wallet, verify WhatsApp, deposit, swap a little for L-BTC, withdraw back, create the store), each with a plain PT+EN title and instruction to relay to the human, an absolute app deep link, and the current numbers. When every step is complete it triggers verification itself.
update_merchant_profilemerchant_writeChange the store's light profile fields — business_name, logo_url, website, default_redirect_url, default_callback_url. Only the fields you pass change. The money-redirecting fields are not here, by design (PATCH /api/merchants/me).
get_vault_statuswallet_readRead the account's position in the Cofre deposit hold: whether the mechanism is active, how long a new deposit is held, the trust level, and the rolling receive cap with how much is left this window.
list_webhook_logsmerchant_readRead recent webhook deliveries: the event, the endpoint, the HTTP status it returned or the transport error, the attempt number and when it was sent — newest first. Pass id for one delivery, with the bodies (GET /api/webhook-logs).
open_support_ticketnoneOpen a support ticket; the body becomes the first message (POST /api/tickets).
get_support_ticketnoneRead a ticket and its messages — this is how an agent sees the reply.
list_support_ticketsnoneList the tickets opened by this same key or session.
reply_support_ticketnoneReply on an open ticket.
close_support_ticketnoneClose a ticket.
attach_support_ticket_filenoneAttach a file to a ticket.

Local mode only — 34 more

Run the package on the machine where the agent lives (npx -y @depixapp/mcp, stdio) and the same server gains 34 more tools: 29 wallet_* — balances and addresses, Pix on/off-ramp, send, quote and convert, cross-network stablecoin shifts, Lightning (settled through Boltz swaps), gift cards, spending guardrails, recovery and diagnostics — plus 5 account tools. They sign inside that very process, with a seed that never leaves it; none of them exports the seed or loosens the guardrails.

Balances always fresh; a read never breaks. Every read syncs with the network before answering, and every spend syncs before and after — a payment you received already shows up on the next balance read, with nothing to ask for. If the network is down the answer still comes, carrying the last known state and a stale flag (or post_sync_failed, when the sync that failed was the one after money already moved — the money went, only the snapshot is old).

ToolDoes
wallet_syncForce an explicit refresh. You rarely need it — reads and spends already sync on your behalf. With rescan: true it runs a deep cold re-scan from zero, for when balances look desynchronized (missing transactions, stale amounts): that can take MINUTES. Signs nothing.
wallet_list_utxosList the wallet's unspent outputs — per UTXO: asset, amount in base units, the txid:vout that funded it, the address holding it, block height and confirmations. Read-only: signs nothing, spends nothing, reserves nothing.
register_accountCreate the DePix account and its API keys in this process, on the operator's machine — no dashboard, no restart, nothing pasted into a config. Needs the human's op_ code and an initialized wallet (the payout address is the wallet's own). The keys are saved encrypted on that machine; the response carries only public facts (username, store slug, limits, key ids), never the secrets. Activates the sandbox key by default (POST /api/agents/register).
agent_statusRead the agent account's progress: active or suspended, how many personal deposits have settled, whether it has graduated to live keys and what is still blocking, and its keys with id, prefix, scopes and revocation — never the secret (GET /api/agents/status).
verify_domainProve control of a domain via a DNS TXT challenge, in two phases: without confirm it returns the record name and value to create, for the human to add at their DNS provider; with confirm: true, after propagation, the server resolves it and records the domain as verified (POST /api/agents/verify-domain).
configure_depix_railTurn on or off being paid in DePix sent directly on Liquid. Enabling derives a dedicated address from this wallet and registers it with the backend so incoming DePix is credited. You pass only enabled — the address and its private viewing key are derived and sent in-process; the key never appears in the response (POST /api/agents/depix-pay).
activate_keyChoose which of the account's two keys the server authenticates with from now on: test (sandbox, no real money) or live (the production starter). Both keys already exist since register_account; nothing is minted and no secret is shown. The choice is saved in the encrypted vault, survives restarts, and the wallet picks it up on its next call. Under live, deposits are real Pix charges — confirm with the operator first.

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 lives in these docs and in the /.well-known/agent.json manifest. The engine's source lives at github.com/depixapp/depix-mcp — the same wallet engine, now developed and released there. @depixapp/sdk is the frozen lineage on npm: the 1.2.x line keeps working, but the successor is @depixapp/mcp.