# Stackfy — API de pagamentos em cripto (guia de integração)

Este documento é a referência completa para integrar a **API de pagamentos em cripto da Stackfy**
(não-custodial: os valores caem direto na carteira do lojista). Pode ser usado por um desenvolvedor
ou por um agente de IA de programação para gerar uma integração que: (1) cria uma cobrança,
(2) redireciona o cliente para o checkout hospedado, (3) recebe e **verifica a assinatura** do
webhook, e (4) consulta o status da cobrança. Use a linguagem/stack do próprio projeto. Trate
`SEU_API_KEY` e `STACKFY_WHSEC` como variáveis de ambiente — nunca os escreva no código.

## O que é
Gateway para receber **Bitcoin (on-chain)** e **USDT (Tron TRC-20 e Polygon PoS)**. Você cria uma cobrança em moeda fiat (BRL/USD/EUR) ou direto em USDT; a Stackfy gera um checkout hospedado, detecta o pagamento on-chain e te avisa por webhook assinado. **Não-custodial:** o dinheiro vai direto para a carteira (xpub/endereço) que você cadastrou — a Stackfy nunca custodia fundos nem pede sua chave privada.

## Seus dados de conta
Você está lendo a versão **pública** (genérica). Depois de criar a conta e configurar uma loja, o painel gera esta mesma referência **já preenchida** com o seu `store_id`, moedas e domínio, em `/conta/api`.

- **Endereço da API:** `https://api.stackfy.io/v1`
- **Seu store_id:** `SEU_STORE_ID`
- **Moeda padrão de precificação:** `BRL`
- **Base do checkout:** `https://pay.stackfy.io`

## Autenticação
Toda chamada leva a sua **chave Stackfy** (`sk_live_...`) no header `Authorization`. Crie/revogue chaves em **/conta/api**. A chave é mostrada **uma única vez** na criação.

```http
Authorization: Bearer SEU_API_KEY
```

## Criar uma cobrança
`POST https://api.stackfy.io/v1/invoices`

Campos do corpo (JSON):

| Campo | Obrigatório | Descrição |
|---|---|---|
| `price` | sim | Valor da cobrança (string ou número). Ex.: `"49.90"`. |
| `store_id` | sim | Sua loja (uma conta pode ter várias). |
| `currency` | não | Moeda de PREÇO: `BRL` (padrão), `USD`, `EUR` ou `USDT` (1:1). |
| `pay_currency` | não | Cripto de recebimento: omita/`ANY` = cliente escolhe · `BTC` · `USDT`/`USDT-TRC20` (Tron) · `USDT-MATIC`/`USDT-POLYGON` (Polygon). |
| `notes` | não | Descrição curta (aparece no checkout/recibo). |

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

Resposta (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
}
```
Guarde o `id` retornado ligado ao seu pedido — é por ele que você casa o webhook e a consulta de status (o campo `metadata` **não** é ecoado hoje; use o `id`). Redirecione o cliente para o `checkout_url`.

### Multi-moeda (o cliente escolhe a cripto)
Omita `pay_currency` (ou mande `ANY`) e a Stackfy cria um checkout onde o **comprador escolhe** BTC / USDT-Tron / USDT-Polygon. O `checkout_url` vem como `/pay/<id>`.

## Levar o cliente para pagar
Simplesmente redirecione (ou abra) o `checkout_url` devolvido na criação. A página de pagamento (QR + endereço + timer + acompanhamento ao vivo) é hospedada pela Stackfy — você não precisa construir UI de pagamento.

## Webhook (confirmação de pagamento)
Configure a **URL do seu webhook** (HTTPS) em **/conta/api**. Na primeira vez, a Stackfy gera um **segredo de assinatura** (`whsec_...`) mostrado uma única vez — guarde como `STACKFY_WHSEC`. A cada mudança relevante de status, a Stackfy faz um `POST` **assinado** para a sua URL.

Cabeçalhos que chegam:

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

Corpo (exemplo):

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

### Como validar a assinatura
A assinatura é **HMAC-SHA256** sobre a string `"<timestamp>.<corpo_cru>"` com o seu `STACKFY_WHSEC` (mesmo esquema do Stripe). Compare com o `v1` do header `X-Stackfy-Signature` usando comparação **timing-safe**. Use o **corpo CRU** (bytes), não o JSON re-serializado. Responda **2xx** rápido; a Stackfy re-tenta 3× (backoff 0/2/5s) se não receber 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);
```

## Consultar status
Se preferir puxar em vez de esperar o webhook:

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

Valores de `status`:

| status | Significado | Liberar acesso? |
|---|---|---|
| `pending` | Aguardando pagamento | não |
| `paid` | Pago, aguardando confirmações | opcional |
| `confirmed` | Confirmado on-chain | sim |
| `complete` | Concluído (recomendado) | sim |
| `expired` | Expirou sem pagar | não |
| `invalid` | Inválida/cancelada | não |

## Limites e erros
Rate-limit: **240 req/min por IP** e **60 cobranças/min por conta**. Corpo do POST **≤ 16 KB**. Códigos de erro:

| Código | Quando |
|---|---|
| `401` | chave ausente/inválida/revogada |
| `402` | teto do plano atingido (`{"error":"limite_atingido"}`) |
| `404` | `store_id` não é da sua conta / cobrança não encontrada |
| `409` | loja ou moeda desativada / sem endereço configurado |
| `413` | corpo maior que 16 KB |
| `422` | faltou `price` ou `store_id` |
| `429` | rate-limit (respeite o header `Retry-After`) |
| `502` | erro temporário — tente de novo |

## Boas práticas
- Trate o pagamento como confirmado só em `complete` (ou `confirmed`) — nunca em `pending`.
- Idempotência: o mesmo `id` pode chegar mais de uma vez no webhook; aplique o efeito uma vez só.
- Nunca confie no status vindo do cliente/redirect — confie no webhook assinado ou no GET de status.
- Guarde `SEU_API_KEY` e `STACKFY_WHSEC` em variáveis de ambiente/secret manager, fora do código.

