# Stackfy — Crypto payments API (integration guide)

This document is the complete reference for integrating the **Stackfy crypto payment API**
(non-custodial: funds land straight in the merchant's wallet). It can be used by a developer or an
AI coding agent to generate an integration that: (1) creates a charge, (2) redirects the customer
to the hosted checkout, (3) receives and **verifies the webhook signature**, and (4) polls the
charge status. Use the project's own language/stack. Treat `YOUR_API_KEY` and `STACKFY_WHSEC` as
environment variables — never hard-code them.

## What it is
Gateway to accept **Bitcoin (on-chain)** and **USDT (Tron TRC-20 and Polygon PoS)**. You create a charge in fiat (BRL/USD/EUR) or directly in USDT; Stackfy hosts a checkout, detects the on-chain payment and notifies you via a signed webhook. **Non-custodial:** money goes straight to the wallet (xpub/address) you registered — Stackfy never holds funds nor asks for your private key.

## Your account data
You are reading the **public** (generic) version. After you create an account and configure a store, the panel generates this same reference **pre-filled** with your `store_id`, currencies and domain, at `/conta/api`.

- **API endpoint:** `https://api.stackfy.io/v1`
- **Your store_id:** `SEU_STORE_ID`
- **Default pricing currency:** `BRL`
- **Checkout base:** `https://pay.stackfy.io`

## Authentication
Every call carries your **Stackfy key** (`sk_live_...`) in the `Authorization` header. Create/revoke keys at **/conta/api**. The key is shown **only once** on creation.

```http
Authorization: Bearer YOUR_API_KEY
```

## Create a charge
`POST https://api.stackfy.io/v1/invoices`

Body fields (JSON):

| Field | Required | Description |
|---|---|---|
| `price` | yes | Charge amount (string or number). E.g. `"49.90"`. |
| `store_id` | yes | Your store (an account may have several). |
| `currency` | no | PRICING currency: `BRL` (default), `USD`, `EUR` or `USDT` (1:1). |
| `pay_currency` | no | Receiving coin: omit/`ANY` = buyer chooses · `BTC` · `USDT`/`USDT-TRC20` (Tron) · `USDT-MATIC`/`USDT-POLYGON` (Polygon). |
| `notes` | no | Short description (shown on the checkout/receipt). |

```bash
curl -X POST https://api.stackfy.io/v1/invoices \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "price": "49.90",
    "currency": "BRL",
    "store_id": "SEU_STORE_ID",
    "notes": "Pedido #1234"
  }'
```

Response (201/200):

```json
{
  "id": "abc123...",
  "status": "pending",
  "price": "49.90",
  "currency": "BRL",
  "checkout_url": "https://pay.stackfy.io/c/abc123...",
  "created": "2026-08-15T12:00:00Z",
  "expiration": 15
}
```
Store the returned `id` against your order — it's how you match the webhook and the status lookup (the `metadata` field is **not** echoed today; use the `id`). Redirect the customer to `checkout_url`.

### Multi-currency (customer picks the coin)
Omit `pay_currency` (or send `ANY`) and Stackfy builds a checkout where the **buyer chooses** BTC / USDT-Tron / USDT-Polygon. The `checkout_url` comes as `/pay/<id>`.

## Send the customer to pay
Just redirect to (or open) the `checkout_url` returned on creation. The payment page (QR + address + timer + live tracking) is hosted by Stackfy — you don't build any payment UI.

## Webhook (payment confirmation)
Configure your **webhook URL** (HTTPS) at **/conta/api**. On first setup, Stackfy generates a **signing secret** (`whsec_...`) shown only once — keep it as `STACKFY_WHSEC`. On every relevant status change, Stackfy sends a **signed** `POST` to your URL.

Incoming headers:

```http
X-Stackfy-Signature: t=1718900000,v1=6a3f...hexdigest
X-Stackfy-Event: invoice.complete
Content-Type: application/json
User-Agent: Stackfy-Webhook/1
```

Body (example):

```json
{
  "id": "abc123...",
  "status": "complete",
  "price": "49.90",
  "currency": "BRL",
  "paid_currency": "BTC",
  "paid_date": "2026-08-15T12:07:31Z",
  "store_id": "SEU_STORE_ID"
}
```

### How to verify the signature
The signature is **HMAC-SHA256** over the string `"<timestamp>.<raw_body>"` with your `STACKFY_WHSEC` (same scheme as Stripe). Compare with the `v1` from the `X-Stackfy-Signature` header using a **timing-safe** compare. Use the **RAW body** (bytes), not re-serialized JSON. Reply **2xx** fast; Stackfy retries 3× (backoff 0/2/5s) if it doesn't get a 2xx.

```js
// Node.js / Express — corpo CRU obrigatório p/ a assinatura bater
const crypto = require("crypto");
app.post("/webhooks/stackfy", express.raw({ type: "*/*" }), (req, res) => {
  const raw = req.body.toString("utf8");
  const sig = Object.fromEntries(
    req.headers["x-stackfy-signature"].split(",").map(p => p.split("=")));
  const expected = crypto.createHmac("sha256", process.env.STACKFY_WHSEC)
                         .update(sig.t + "." + raw).digest("hex");
  const ok = crypto.timingSafeEqual(Buffer.from(sig.v1), Buffer.from(expected));
  if (!ok) return res.status(401).end();            // assinatura inválida = descarta
  const ev = JSON.parse(raw);
  if (ev.status === "complete") liberar(ev.id);     // case-a o id ao seu pedido
  res.status(200).end();
});
```

```python
# Python / Flask
import hmac, hashlib
@app.post("/webhooks/stackfy")
def stackfy_webhook():
    raw = request.get_data()  # bytes CRUS
    sig = dict(p.split("=") for p in request.headers["X-Stackfy-Signature"].split(","))
    msg = (sig["t"] + ".").encode() + raw
    expected = hmac.new(os.environ["STACKFY_WHSEC"].encode(), msg, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig["v1"]):
        return "", 401
    ev = request.get_json()
    if ev["status"] == "complete":
        liberar(ev["id"])
    return "", 200
```

```php
<?php // PHP
$raw = file_get_contents("php://input");
parse_str(str_replace(",", "&", $_SERVER["HTTP_X_STACKFY_SIGNATURE"]), $sig);
$expected = hash_hmac("sha256", $sig["t"] . "." . $raw, getenv("STACKFY_WHSEC"));
if (!hash_equals($expected, $sig["v1"])) { http_response_code(401); exit; }
$ev = json_decode($raw, true);
if ($ev["status"] === "complete") { liberar($ev["id"]); }
http_response_code(200);
```

## Poll status
If you prefer polling instead of waiting for the webhook:

```bash
curl https://api.stackfy.io/v1/invoices/abc123... \
  -H "Authorization: Bearer YOUR_API_KEY"
```

`status` values:

| status | Meaning | Release access? |
|---|---|---|
| `pending` | Awaiting payment | no |
| `paid` | Paid, awaiting confirmations | optional |
| `confirmed` | Confirmed on-chain | yes |
| `complete` | Done (recommended) | yes |
| `expired` | Expired unpaid | no |
| `invalid` | Invalid/cancelled | no |

## Limits and errors
Rate limit: **240 req/min per IP** and **60 charges/min per account**. POST body **≤ 16 KB**. Error codes:

| Code | When |
|---|---|
| `401` | missing/invalid/revoked key |
| `402` | plan cap reached (`{"error":"limite_atingido"}`) |
| `404` | `store_id` not in your account / charge not found |
| `409` | store or coin disabled / no address configured |
| `413` | body larger than 16 KB |
| `422` | missing `price` or `store_id` |
| `429` | rate limit (honor the `Retry-After` header) |
| `502` | temporary error — retry |

## Best practices
- Treat payment as done only on `complete` (or `confirmed`) — never on `pending`.
- Idempotency: the same `id` may arrive more than once on the webhook; apply the effect once.
- Never trust status coming from the client/redirect — trust the signed webhook or the status GET.
- Keep `YOUR_API_KEY` and `STACKFY_WHSEC` in env vars / a secret manager, out of the code.

