Stackfy

API documentation

Create crypto charges directly from your system and grant the customer's access automatically when the payment confirms.

How it works

The typical flow of an integration (subscription, hosting, streaming):

1. Your system creates a charge via the API  →  POST https://api.stackfy.io/v1/invoices
2. You send the customer to payment  →  checkout link OR transparent QR
3. The customer pays in crypto
4. Stackfy calls your webhook  →  POST to your notification_url
5. You confirm the charge via the API  →  GET https://api.stackfy.io/v1/invoices/{id}
6. Status "complete"  →  you grant/renew the customer's access

Recurring: crypto has no automatic debit — on each cycle, your system repeats step 1.

Your account details

API addresshttps://api.stackfy.io/v1
Your store_idSEU_STORE_ID
Default currencyBRL

Authentication

Create a key in API and integration and send it in the header of every call:

Authorization: Bearer SUA_CHAVE

The key grants access to your account — keep it like a password. If it leaks, revoke it immediately from the panel.

Create a charge

POSThttps://api.stackfy.io/v1/invoices

FieldRequiredDescription
priceyesAmount (string), e.g.: "49.90"
currencyyesCurrency of the amount: BRL, USD, EUR… (or USDT for a direct 1:1 dollar price, only with pay_currency=USDT)
store_idyesYour store: SEU_STORE_ID
pay_currencynoOmitted = multi-currency (the buyer picks the crypto — see below). To PIN the network: BTC, USDT (Tron/TRC-20) or USDT-MATIC (Polygon). Pinning USDT requires the store to have an address on that network. price stays in fiat.
notification_urlnoYour webhook (receives the paid notification)
redirect_urlnoWhere to send the customer after paying
metadatanoFree-form object (e.g.: your order/user id)

Multi-currency — the buyer chooses the crypto

Just omit the pay_currency field: Stackfy creates an order and the buyer chooses between BTC, USDT-Tron and USDT-Polygon on the payment page (options depend on what the store has enabled). price stays in fiat; conversion happens at the live rate when the buyer chooses. The response returns pay_currency: "ANY" and the picker checkout_url.

curl -X POST https://api.stackfy.io/v1/invoices \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "price": "49.90",
    "currency": "BRL",
    "store_id": "SEU_STORE_ID"
  }'
# no pay_currency = the buyer picks the crypto at checkout

Example — cURL

curl -X POST https://api.stackfy.io/v1/invoices \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "price": "49.90",
    "currency": "BRL",
    "store_id": "SEU_STORE_ID",
    "notification_url": "https://seusite.com/webhooks/stackfy",
    "metadata": {"pedido": "1234", "usuario": "[email protected]"}
  }'

Example — Node.js

const r = await fetch("https://api.stackfy.io/v1/invoices", {
  method: "POST",
  headers: {
    "Authorization": "Bearer " + process.env.STACKFY_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    price: "49.90", currency: "BRL", store_id: "SEU_STORE_ID",
    notification_url: "https://seusite.com/webhooks/stackfy",
    metadata: { pedido: "1234" },
  }),
});
const invoice = await r.json();
// invoice.id  -> save it with your order
// redirect the customer to checkout (see below)

Example — PHP

$ch = curl_init("https://api.stackfy.io/v1/invoices");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer " . getenv("STACKFY_KEY"),
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "price" => "49.90", "currency" => "BRL", "store_id" => "SEU_STORE_ID",
    "notification_url" => "https://seusite.com/webhooks/stackfy",
  ]),
]);
$invoice = json_decode(curl_exec($ch), true);

The response is the created charge, with id, status ("pending") and the payment methods in payments (address, crypto amount and URI for QR).

Take the customer to payment

Option A — Redirect (simplest)

Send the customer to Stackfy's ready-made checkout, using the charge id:

https://pay.stackfy.io/c/<invoice_id>

The customer sees the QR, pays and (if you set redirect_url) returns to your site.

Option B — Transparent (in your own layout)

Use the payments data from the response to build your own QR/address:

{
  "id": "...",
  "status": "pending",
  "payments": [{
    "payment_address": "bc1q...",        // receiving address
    "amount": "0.00071",                 // amount in crypto
    "currency": "BTC",
    "payment_url": "bitcoin:bc1q...?amount=0.00071"  // becomes a QR
  }]
}

Webhook (payment confirmation)

When the charge is paid, Stackfy makes a signed POST to your server. You check the signature with your secret and that's it — no need to re-query anything.

1. Configure

In API and integration, save your server URL (receives the notification) and copy your signing secret (whsec_…). You don't need to set notification_url: every charge created by Stackfy (API https://api.stackfy.io/v1 or panel) already points to our relay on its own.

Stackfy receives the confirmation, checks the real status of the charge internally and only then resends it, already signed, to your server.

2. What arrives at your server

Header X-Stackfy-Signature: t=<timestamp>,v1=<hmac_sha256> and JSON body:

{
  "id": "<invoice_id>",
  "status": "complete",
  "price": "49.90",
  "currency": "BRL",
  "paid_currency": "BTC",
  "metadata": { "pedido": "1234" },
  "store_id": "SEU_STORE_ID"
}

3. Validate the signature

The signature is HMAC-SHA256 of the text "<timestamp>.<corpo_cru>" using your secret. Compare in constant time. Same scheme as Stripe.

Node.js / Express

const crypto = require("crypto");
// use the RAW body, not the already-parsed JSON:
app.post("/webhooks/stackfy", express.raw({type:"*/*"}), (req, res) => {
  const raw = req.body.toString("utf8");
  const [t, v1] = req.headers["x-stackfy-signature"].split(",")
                    .map(p => p.split("=")[1]);
  const expected = crypto.createHmac("sha256", process.env.STACKFY_WHSEC)
                         .update(t + "." + raw).digest("hex");
  const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  if (!ok) return res.status(401).end();              // forged -> reject

  const ev = JSON.parse(raw);
  if (ev.status === "complete") liberar(ev.metadata.pedido);
  res.status(200).end();                               // respond 200 quickly
});

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_acesso($ev["metadata"]["pedido"]);
http_response_code(200);
Best practices: use the raw body (don't re-serialize the JSON before validating); the webhook may arrive more than once for the same charge → handle it idempotently; release access on complete (or confirmed).

Alternative: no webhook (polling)

If you'd rather not expose an endpoint, check the status whenever you want: GEThttps://api.stackfy.io/v1/invoices/<id> with your key. The truth is always in the API.

Status reference

statusMeaningRelease access?
pendingAwaiting paymentNo
paidSeen on the network, 0 confirmationsNot yet (may revert)
confirmedConfirmed on the network (≥1 conf)Yes
completePaid and settledYes
expiredTime ran out without paymentNo
invalidInvalid paymentNo

Integration questions? [email protected]

© 2026 Stackfy · stackfy.io · Create free account · Terms · Privacy