# Stackfy — API de pagos en cripto (guía de integración)

Este documento es la referencia completa para integrar la **API de pagos en cripto de Stackfy**
(no-custodial: los fondos llegan directo a la billetera del comercio). Puede ser usado por un
desarrollador o por un agente de IA de programación para generar una integración que: (1) crea un
cobro, (2) redirige al cliente al checkout alojado, (3) recibe y **verifica la firma del webhook**,
y (4) consulta el estado del cobro. Usa el lenguaje/stack del propio proyecto. Trata `YOUR_API_KEY`
y `STACKFY_WHSEC` como variables de entorno — nunca los escribas en el código.

## Qué es
Pasarela para recibir **Bitcoin (on-chain)** y **USDT (Tron TRC-20 y Polygon PoS)**. Creas un cobro en fiat (BRL/USD/EUR) o directo en USDT; Stackfy aloja un checkout, detecta el pago on-chain y te avisa por webhook firmado. **No-custodial:** el dinero va directo a la billetera (xpub/dirección) que registraste — Stackfy nunca custodia fondos ni pide tu clave privada.

## Datos de tu cuenta
Estás leyendo la versión **pública** (genérica). Tras crear la cuenta y configurar una tienda, el panel genera esta misma referencia **ya rellena** con tu `store_id`, monedas y dominio, en `/conta/api`.

- **Dirección de la API:** `https://api.stackfy.io/v1`
- **Tu store_id:** `SEU_STORE_ID`
- **Moneda de precio por defecto:** `BRL`
- **Base del checkout:** `https://pay.stackfy.io`

## Autenticación
Cada llamada lleva tu **clave Stackfy** (`sk_live_...`) en el header `Authorization`. Crea/revoca claves en **/conta/api**. La clave se muestra **una sola vez** al crearla.

```http
Authorization: Bearer YOUR_API_KEY
```

## Crear un cobro
`POST https://api.stackfy.io/v1/invoices`

Campos del cuerpo (JSON):

| Campo | Obligatorio | Descripción |
|---|---|---|
| `price` | sí | Importe del cobro (string o número). Ej.: `"49.90"`. |
| `store_id` | sí | Tu tienda (una cuenta puede tener varias). |
| `currency` | no | Moneda de PRECIO: `BRL` (por defecto), `USD`, `EUR` o `USDT` (1:1). |
| `pay_currency` | no | Cripto de cobro: omite/`ANY` = elige el cliente · `BTC` · `USDT`/`USDT-TRC20` (Tron) · `USDT-MATIC`/`USDT-POLYGON` (Polygon). |
| `notes` | no | Descripción corta (aparece en el checkout/recibo). |

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

Respuesta (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
}
```
Guarda el `id` devuelto junto a tu pedido — con él casas el webhook y la consulta de estado (el campo `metadata` **no** se devuelve hoy; usa el `id`). Redirige al cliente a `checkout_url`.

### Multi-moneda (el cliente elige la cripto)
Omite `pay_currency` (o envía `ANY`) y Stackfy crea un checkout donde el **comprador elige** BTC / USDT-Tron / USDT-Polygon. El `checkout_url` llega como `/pay/<id>`.

## Llevar al cliente a pagar
Solo redirige (o abre) el `checkout_url` devuelto al crear. La página de pago (QR + dirección + temporizador + seguimiento en vivo) la aloja Stackfy — no necesitas construir UI de pago.

## Webhook (confirmación de pago)
Configura la **URL de tu webhook** (HTTPS) en **/conta/api**. La primera vez, Stackfy genera un **secreto de firma** (`whsec_...`) mostrado una sola vez — guárdalo como `STACKFY_WHSEC`. En cada cambio de estado relevante, Stackfy hace un `POST` **firmado** a tu URL.

Cabeceras que llegan:

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

Cuerpo (ejemplo):

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

### Cómo verificar la firma
La firma es **HMAC-SHA256** sobre la cadena `"<timestamp>.<cuerpo_crudo>"` con tu `STACKFY_WHSEC` (mismo esquema que Stripe). Compara con el `v1` del header `X-Stackfy-Signature` con una comparación **timing-safe**. Usa el **cuerpo CRUDO** (bytes), no el JSON re-serializado. Responde **2xx** rápido; Stackfy reintenta 3× (backoff 0/2/5s) si no recibe 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 estado
Si prefieres consultar en vez de esperar el webhook:

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

Valores de `status`:

| status | Significado | ¿Liberar acceso? |
|---|---|---|
| `pending` | Esperando pago | no |
| `paid` | Pagado, esperando confirmaciones | opcional |
| `confirmed` | Confirmado on-chain | sí |
| `complete` | Completado (recomendado) | sí |
| `expired` | Expiró sin pagar | no |
| `invalid` | Inválida/cancelada | no |

## Límites y errores
Rate-limit: **240 req/min por IP** y **60 cobros/min por cuenta**. Cuerpo del POST **≤ 16 KB**. Códigos de error:

| Código | Cuándo |
|---|---|
| `401` | clave ausente/inválida/revocada |
| `402` | tope del plan alcanzado (`{"error":"limite_atingido"}`) |
| `404` | `store_id` no es de tu cuenta / cobro no encontrado |
| `409` | tienda o moneda desactivada / sin dirección configurada |
| `413` | cuerpo mayor a 16 KB |
| `422` | falta `price` o `store_id` |
| `429` | rate-limit (respeta el header `Retry-After`) |
| `502` | error temporal — reintenta |

## Buenas prácticas
- Considera el pago hecho solo en `complete` (o `confirmed`) — nunca en `pending`.
- Idempotencia: el mismo `id` puede llegar más de una vez en el webhook; aplica el efecto una vez.
- Nunca confíes en el estado que venga del cliente/redirect — confía en el webhook firmado o el GET.
- Guarda `YOUR_API_KEY` y `STACKFY_WHSEC` en variables de entorno / gestor de secretos, fuera del código.

