FiroGate API Docs

API Documentation

Accept FIRO cryptocurrency payments on your website, app, or bot.

Platform URLs
APIhttps://api.firogate.com Dashboardhttps://dashboard.firogate.com Checkouthttps://checkout.firogate.com/invoice/{id}

Overview

A self-hosted cryptocurrency payment gateway for Firo (FIRO). Full control over your payment processing on your own infrastructure.

How It Works

1. Your store calls POST /api/payments/create with the amount and order info.

2. The gateway generates a unique Firo address and returns a checkout_url pointing to your-gateway/invoice/{id}.

3. Redirect your buyer to the checkout URL. They send FIRO to the displayed address.

4. The gateway monitors the Firo blockchain. After 2 confirmations (~5 min) it fires your webhook.

5. Your webhook handler delivers the product/activates the service.

Authentication

All API requests require your API key in the X-API-Key header.

GET /api/payments/
X-API-Key: fgate_your_api_key_here

Get your API key from the Dashboard → API & Webhooks tab.

Error Codes

200OKSuccess
201CreatedPayment created
400Bad RequestInvalid parameters
401UnauthorizedMissing or invalid API key
402Payment RequiredAPI request limit reached
422ValidationInvalid field value
503UnavailableFiro node unreachable

Create Payment

POST /api/payments/create

Creates a new payment invoice. Returns a unique Firo address and checkout URL.

Request Fields

amount_firofloat*Amount in FIRO the buyer must send
order_idstringYour internal order identifier
order_descriptionstringProduct/service description shown at checkout
customer_emailstringPre-fill buyer email (skip email step)
success_urlstringRedirect URL after payment confirmed
cancel_urlstringRedirect URL if buyer cancels
timeout_minutesintPayment window in minutes (default: 20)
collect_emailboolShow email step at checkout (default: true)
required_confirmationsintBlockchain confirmations needed (default: 2)
metadataobjectAny JSON object — stored with the payment, returned in webhooks and GET responses

Response

{
  "payment_id":             "193d7d11-5ade-4993-9ad9-...",
  "checkout_url":           "https://checkout.firogate.com/invoice/193d7d11...",
  "receiving_address":      "TRdhAfFoDhwYYLkDhchz...",
  "amount_firo":            2.5,
  "platform_fee_pct":       1.5,
  "order_id":              "ORD-1234",
  "expires_at":            "2025-01-15T14:30:00Z",
  "required_confirmations": 2,
  "status":                "pending"
}

Error Codes

401UnauthorizedMissing or invalid API key
402Payment RequiredMonthly quota exhausted — upgrade your plan
422Validation ErrorInvalid field value (amount ≤ 0, bad URL, etc.)
503Service UnavailableNode RPC unreachable — retry in a few seconds

Get Payment Status

GET /api/payments/{payment_id}

Check the status of a payment. Poll this every 10–30 seconds until status is confirmed.

Payment Status Values

pendingWaiting for buyer to send FIRO
confirmingPayment detected, waiting for confirmations
confirmedPayment complete — deliver product
expiredBuyer did not pay within timeout
cancelledPayment cancelled by buyer

List Payments

GET /api/payments/?limit=50&status=confirmed

Returns your recent payments.

Query Parameters

limitintMax results to return (default: 50, max: 200)
offsetintPagination offset (default: 0)
statusstringFilter by status: pending / confirming / confirmed / expired / cancelled

Cancel Payment (Public)

POST /api/payments/public/{payment_id}/cancel

Cancel a pending or confirming payment. Only works before the payment is confirmed.

Response

{
  "cancelled":    true,
  "redirect_url": "https://yourstore.com/cancel",
  "message":      "Payment cancelled"
}

Webhook Events

The gateway sends a signed POST request to your webhook URL for various events. Each webhook includes a nonce and timestamp for replay protection.

Always verify the webhook signature before processing. See the Verify Signature section.

Event Types

payment.confirmedPayment received and confirmed on blockchain
payment.cancelledPayment cancelled by buyer before completion

payment.confirmed payload

{
  "event":           "payment.confirmed",
  "payment_id":      "193d7d11-5ade-4993...",
  "order_id":        "ORD-1234",
  "amount_firo":     2.5,
  "amount_received": 2.5,
  "platform_fee":    0.0375,
  "merchant_net":    2.4625,
  "txid":            "a1b2c3d4e5f6...",
  "confirmations":   2,
  "customer_email":  "buyer@example.com",
  "confirmed_at":    "2025-01-15T14:35:00Z",
  "nonce":           "7f8a9b0c1d2e3f4a...",
  "timestamp":       1705312500
}

payment.cancelled payload

{
  "event":        "payment.cancelled",
  "payment_id":   "193d7d11-5ade-4993...",
  "order_id":     "ORD-1234",
  "amount_firo":  2.5,
  "status":       "cancelled",
  "cancelled_at": "2025-01-15T14:20:00Z",
  "nonce":        "9a8b7c6d5e4f3a2b...",
  "timestamp":    1705311600
}

Python

import requests

GATEWAY_URL = "https://api.firogate.com"
API_KEY     = "fgate_your_api_key"

def create_payment(amount_firo, order_id, success_url):
    response = requests.post(
        f"{GATEWAY_URL}/api/payments/create",
        json={
            "amount_firo":       amount_firo,
            "order_id":          order_id,
            "order_description": "Premium Subscription",
            "success_url":       success_url,
            "cancel_url":        "https://yourstore.com/cancel",
            "timeout_minutes":   20,
        },
        headers={"X-API-Key": API_KEY},
    )
    response.raise_for_status()
    return response.json()

# Usage
payment = create_payment(2.5, "ORD-1234", "https://yourstore.com/success")
print(f"Send buyer to: {payment['checkout_url']}")
print(f"Payment ID:    {payment['payment_id']}")
import time, requests

def wait_for_payment(payment_id, timeout=1200):
    """Poll every 10s until confirmed or expired."""
    start = time.time()
    while time.time() - start < timeout:
        r = requests.get(
            f"{GATEWAY_URL}/api/payments/{payment_id}",
            headers={"X-API-Key": API_KEY},
        )
        data = r.json()
        status = data["status"]

        if status == "confirmed":
            print(f"✅ Paid! TX: {data['txid']}")
            return data
        elif status == "expired":
            print("❌ Payment expired")
            return None

        print(f"Status: {status} ({data.get('confirmations',0)} confs)")
        time.sleep(10)

    return None
# Flask webhook handler with HMAC-SHA256 verification
from flask import Flask, request, jsonify
import hmac, hashlib, json, time

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_from_dashboard"

@app.route("/webhook/firo", methods=["POST"])
def firo_webhook():
    data = request.json
    sig  = request.headers.get("X-FiroGate-Signature", "")

    # 1. Verify HMAC-SHA256 (sort_keys + compact separators)
    canonical = json.dumps(data, sort_keys=True, separators=(",", ":")).encode()
    expected  = hmac.new(WEBHOOK_SECRET.encode(), canonical, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        return jsonify({"error": "Invalid signature"}), 401

    # 2. Verify timestamp (reject if older than 5 min)
    ts = data.get("timestamp", 0)
    if abs(time.time() - ts) > 300:
        return jsonify({"error": "Stale webhook"}), 400

    # 3. Check nonce (store in DB/Redis to prevent replays)
    nonce = data.get("nonce")
    # if is_nonce_used(nonce): return jsonify({"error": "Replay"}), 409
    # mark_nonce_used(nonce)

    # 4. Process event
    event = data.get("event")
    if event == "payment.confirmed":
        activate_order(data["order_id"], data["merchant_net"], data["txid"])

    return jsonify({"ok": True})

JavaScript / Node.js

// Node.js — create payment
const GATEWAY_URL = 'https://api.firogate.com';
const API_KEY     = 'fgate_your_api_key';

async function createPayment(amountFiro, orderId, successUrl) {
  const res = await fetch(`${GATEWAY_URL}/api/payments/create`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-API-Key': API_KEY,
    },
    body: JSON.stringify({
      amount_firo:       amountFiro,
      order_id:          orderId,
      order_description: 'Premium Plan',
      success_url:       successUrl,
      cancel_url:        'https://yourstore.com/cancel',
      timeout_minutes:   20,
    }),
  });

  if (!res.ok) throw new Error(`Gateway error: ${res.status}`);
  return res.json();
}

// Usage
const payment = await createPayment(2.5, 'ORD-1234', 'https://yourstore.com/ok');
console.log(`Checkout: ${payment.checkout_url}`);
// Express.js webhook handler with HMAC-SHA256 verification
const express = require('express');
const crypto  = require('crypto');

const app    = express();
const SECRET = 'your_webhook_secret';

app.post('/webhook/firo', express.json(), (req, res) => {
  const data = req.body;
  const sig  = req.headers['x-firogate-signature'] || '';

  // 1. Compute HMAC-SHA256 over sorted, compact JSON
  const keys    = Object.keys(data).sort();
  const sorted  = {};
  keys.forEach(k => sorted[k] = data[k]);
  const canonical = JSON.stringify(sorted);
  const expected  = crypto
    .createHmac('sha256', SECRET)
    .update(canonical)
    .digest('hex');

  if (!crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(sig)
  )) {
    return res.status(401).json({ error: 'Bad signature' });
  }

  // 2. Verify timestamp (reject if older than 5 min)
  const ts = data.timestamp || 0;
  if (Math.abs(Date.now() / 1000 - ts) > 300) {
    return res.status(400).json({ error: 'Stale webhook' });
  }

  // 3. Process event
  const { event, order_id, merchant_net, txid } = data;
  if (event === 'payment.confirmed') {
    activateOrder(order_id, merchant_net, txid);
  }
  res.json({ ok: true });
});
// Poll payment status until confirmed
async function waitForPayment(paymentId, intervalMs = 10000) {
  return new Promise((resolve, reject) => {
    const iv = setInterval(async () => {
      const res = await fetch(
        `${GATEWAY_URL}/api/payments/${paymentId}`,
        { headers: { 'X-API-Key': API_KEY } }
      );
      const data = await res.json();

      if (data.status === 'confirmed') {
        clearInterval(iv);
        resolve(data);
      } else if (data.status === 'expired') {
        clearInterval(iv);
        reject(new Error('Payment expired'));
      }
    }, intervalMs);
  });
}

Java

import java.net.URI;
import java.net.http.*;
import java.net.http.HttpResponse.BodyHandlers;

public class FiroGate {
    static final String GATEWAY = "https://api.firogate.com";
    static final String API_KEY = "fgate_your_api_key";

    public static String createPayment(
            double amount, String orderId, String successUrl
    ) throws Exception {
        String json = """
            {
              "amount_firo": %s,
              "order_id": "%s",
              "success_url": "%s",
              "timeout_minutes": 20
            }""".formatted(amount, orderId, successUrl);

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(GATEWAY + "/api/payments/create"))
            .header("Content-Type", "application/json")
            .header("X-API-Key", API_KEY)
            .POST(HttpRequest.BodyPublishers.ofString(json))
            .build();

        HttpResponse<String> res = HttpClient.newHttpClient()
            .send(req, BodyHandlers.ofString());

        System.out.println("Status: " + res.statusCode());
        return res.body();
    }
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
import com.google.gson.*;

public class WebhookVerifier {
    static final String SECRET = "your_webhook_secret";

    public static boolean verify(String jsonBody, String signature)
            throws Exception {
        // Parse + sort keys for canonical JSON
        JsonObject obj = JsonParser.parseString(jsonBody).getAsJsonObject();
        TreeMap<String, JsonElement> sorted = new TreeMap<>();
        obj.entrySet().forEach(e -> sorted.put(e.getKey(), e.getValue()));
        String canonical = new Gson().toJson(sorted);

        // HMAC-SHA256
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(SECRET.getBytes(), "HmacSHA256"));
        byte[] hash = mac.doFinal(canonical.getBytes());
        StringBuilder hex = new StringBuilder();
        for (byte b : hash) hex.append(String.format("%02x", b));

        return hex.toString().equals(signature);
    }
}

// Spring Boot controller:
// String sig = request.getHeader("X-FiroGate-Signature");
// if (!WebhookVerifier.verify(body, sig)) return 401;

C# / .NET

using System.Net.Http;
using System.Text;
using System.Text.Json;

var client  = new HttpClient();
var gateway = "https://api.firogate.com";
var apiKey  = "fgate_your_api_key";

async Task<string> CreatePayment(
    double amount, string orderId, string successUrl)
{
    var payload = new {
        amount_firo       = amount,
        order_id          = orderId,
        order_description = "Product Purchase",
        success_url       = successUrl,
        timeout_minutes   = 20
    };

    var json = JsonSerializer.Serialize(payload);
    var req  = new HttpRequestMessage(HttpMethod.Post,
                 $"{gateway}/api/payments/create");
    req.Headers.Add("X-API-Key", apiKey);
    req.Content = new StringContent(json, Encoding.UTF8,
                      "application/json");

    var res = await client.SendAsync(req);
    return await res.Content.ReadAsStringAsync();
}

var result = await CreatePayment(2.5, "ORD-1234",
                 "https://yourstore.com/ok");
Console.WriteLine(result);
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

static bool VerifyWebhook(
    string jsonBody, string signature, string secret)
{
    // Parse + sort keys for canonical JSON
    var dict = JsonSerializer.Deserialize
        <SortedDictionary<string, JsonElement>>(jsonBody);
    var canonical = JsonSerializer.Serialize(dict);

    // HMAC-SHA256
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
    var expected = Convert.ToHexString(hash).ToLower();

    return CryptographicOperations
        .FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected),
            Encoding.UTF8.GetBytes(signature));
}

// ASP.NET Core:
// var sig = Request.Headers["X-FiroGate-Signature"];
// if (!VerifyWebhook(body, sig, SECRET)) return Unauthorized();

PHP

<?php
define('GATEWAY_URL', 'https://api.firogate.com');
define('API_KEY',     'fgate_your_api_key');

function createPayment($amountFiro, $orderId, $successUrl) {
    $ch = curl_init(GATEWAY_URL . '/api/payments/create');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'X-API-Key: ' . API_KEY,
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'amount_firo'       => $amountFiro,
            'order_id'          => $orderId,
            'order_description' => 'Product Purchase',
            'success_url'       => $successUrl,
            'timeout_minutes'   => 20,
        ]),
    ]);
    $response = curl_exec($ch);
    curl_close($ch);
    return json_decode($response, true);
}

$payment = createPayment(2.5, 'ORD-1234', 'https://yourstore.com/ok');
header('Location: ' . $payment['checkout_url']);
exit;
<?php
// webhook.php — HMAC-SHA256 verification
define('WEBHOOK_SECRET', 'your_webhook_secret');

$body = file_get_contents('php://input');
$data = json_decode($body, true);
$sig  = $_SERVER['HTTP_X_FIROGATE_SIGNATURE'] ?? '';

// Sort keys + compact JSON to match server-side signing
ksort($data);
$canonical = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$expected  = hash_hmac('sha256', $canonical, WEBHOOK_SECRET);

if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit('Bad signature');
}

// Verify timestamp
$ts = $data['timestamp'] ?? 0;
if (abs(time() - $ts) > 300) {
    http_response_code(400);
    exit('Stale webhook');
}

if ($data['event'] === 'payment.confirmed') {
    $orderId = $data['order_id'];
    $netFiro = $data['merchant_net'];
    activateOrder($orderId, $netFiro);
}

echo json_encode(['ok' => true]);

Go

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

const (
    GatewayURL = "https://api.firogate.com"
    APIKey     = "fgate_your_api_key"
)

type PaymentRequest struct {
    AmountFiro       float64 `json:"amount_firo"`
    OrderID          string  `json:"order_id"`
    OrderDescription string  `json:"order_description"`
    SuccessURL        string  `json:"success_url"`
    TimeoutMinutes   int     `json:"timeout_minutes"`
}

type PaymentResponse struct {
    PaymentID   string `json:"payment_id"`
    CheckoutURL string `json:"checkout_url"`
    AmountFiro  float64 `json:"amount_firo"`
    Status      string `json:"status"`
}

func CreatePayment(req PaymentRequest) (*PaymentResponse, error) {
    body, _ := json.Marshal(req)
    httpReq, _ := http.NewRequest("POST",
        GatewayURL+"/api/payments/create",
        bytes.NewBuffer(body))
    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("X-API-Key", APIKey)

    resp, err := http.DefaultClient.Do(httpReq)
    if err != nil { return nil, err }
    defer resp.Body.Close()

    var result PaymentResponse
    json.NewDecoder(resp.Body).Decode(&result)
    return &result, nil
}

func main() {
    p, _ := CreatePayment(PaymentRequest{
        AmountFiro: 2.5,
        OrderID:    "ORD-1234",
        SuccessURL: "https://yourstore.com/ok",
        TimeoutMinutes: 20,
    })
    fmt.Println("Checkout:", p.CheckoutURL)
}
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "math"
    "net/http"
    "time"
)

const WebhookSecret = "your_webhook_secret"

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    sig      := r.Header.Get("X-FiroGate-Signature")

    // Parse + re-marshal (Go sorts map keys by default)
    var data map[string]interface{}
    json.Unmarshal(body, &data)
    canonical, _ := json.Marshal(data)

    // Verify HMAC-SHA256
    mac := hmac.New(sha256.New, []byte(WebhookSecret))
    mac.Write(canonical)
    expected := hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(expected), []byte(sig)) {
        http.Error(w, "Unauthorized", 401)
        return
    }

    // Verify timestamp
    ts, _ := data["timestamp"].(float64)
    if math.Abs(float64(time.Now().Unix())-ts) > 300 {
        http.Error(w, "Stale", 400)
        return
    }

    if data["event"] == "payment.confirmed" {
        fmt.Println("Payment confirmed:", data["order_id"])
    }
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"ok":true}`))
}

Ruby

require 'net/http'
require 'json'

GATEWAY_URL = 'https://api.firogate.com'
API_KEY     = 'fgate_your_api_key'

def create_payment(amount_firo, order_id, success_url)
  uri  = URI("#{GATEWAY_URL}/api/payments/create")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == 'https'

  req = Net::HTTP::Post.new(uri)
  req['Content-Type'] = 'application/json'
  req['X-API-Key']     = API_KEY
  req.body = {
    amount_firo:       amount_firo,
    order_id:          order_id,
    order_description: 'Digital Product',
    success_url:       success_url,
    timeout_minutes:   20
  }.to_json

  response = http.request(req)
  JSON.parse(response.body)
end

payment = create_payment(2.5, 'ORD-1234', 'https://yourstore.com/ok')
puts "Checkout: #{payment['checkout_url']}"
require 'sinatra'
require 'openssl'
require 'json'

WEBHOOK_SECRET = 'your_webhook_secret'

post '/webhook/firo' do
  body = request.body.read
  data = JSON.parse(body)
  sig  = request.env['HTTP_X_FIROGATE_SIGNATURE']

  # Sort keys + compact JSON to match server signing
  canonical = JSON.generate(data.sort.to_h)
  expected  = OpenSSL::HMAC.hexdigest('SHA256', WEBHOOK_SECRET, canonical)

  halt 401 unless Rack::Utils.secure_compare(expected, sig.to_s)

  # Verify timestamp
  ts = data['timestamp'].to_i
  halt 400 if (Time.now.to_i - ts).abs > 300

  if data['event'] == 'payment.confirmed'
    activate_order(data['order_id'], data['merchant_net'])
  end

  {ok: true}.to_json
end

Rust

use reqwest;
use serde_json::json;

const GATEWAY: &str = "https://api.firogate.com";
const API_KEY: &str = "fgate_your_api_key";

async fn create_payment(
    amount: f64, order_id: &str, success_url: &str,
) -> reqwest::Result<serde_json::Value> {
    let client = reqwest::Client::new();
    let body = json!({
        "amount_firo":       amount,
        "order_id":          order_id,
        "order_description": "Product Purchase",
        "success_url":       success_url,
        "timeout_minutes":   20
    });

    let res = client.post(format!("{}/api/payments/create", GATEWAY))
        .header("X-API-Key", API_KEY)
        .json(&body)
        .send().await?;

    res.json().await
}

// Usage:
// let p = create_payment(2.5, "ORD-1234", "https://yourstore.com/ok").await?;
// println!("Checkout: {}", p["checkout_url"]);
use hmac::{Hmac, Mac};
use sha2::Sha256;
use serde_json::Value;
use std::collections::BTreeMap;

type HmacSha256 = Hmac<Sha256>;

fn verify_webhook(
    json_body: &str, signature: &str, secret: &str,
) -> bool {
    // Parse + BTreeMap auto-sorts keys
    let data: BTreeMap<String, Value> =
        serde_json::from_str(json_body).unwrap();
    let canonical = serde_json::to_string(&data).unwrap();

    // HMAC-SHA256
    let mut mac = HmacSha256::new_from_slice(
        secret.as_bytes()).unwrap();
    mac.update(canonical.as_bytes());
    let result = mac.finalize();
    let expected = hex::encode(result.into_bytes());

    // Constant-time compare
    expected == signature
}

// Actix-web / Axum handler:
// let sig = req.headers().get("X-FiroGate-Signature");
// if !verify_webhook(&body, sig, SECRET) { return 401; }

cURL

curl -X POST https://api.firogate.com/api/payments/create \
  -H "X-API-Key: fgate_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_firo": 2.5,
    "order_id": "ORD-1234",
    "order_description": "Premium Plan",
    "success_url": "https://yourstore.com/ok",
    "timeout_minutes": 20
  }'
curl https://api.firogate.com/api/payments/PAYMENT_ID \
  -H "X-API-Key: fgate_your_api_key"
curl "https://api.firogate.com/api/payments/?limit=10&status=confirmed" \
  -H "X-API-Key: fgate_your_api_key"

Telegram Bot Integration

Complete example: a Telegram bot that creates FIRO payment requests and notifies you when confirmed.

This is a working bot — just fill in your tokens and run it.
# pip install python-telegram-bot[job-queue] requests

import asyncio, requests, time
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CommandHandler, ContextTypes

GATEWAY_URL  = "https://api.firogate.com"
API_KEY      = "fgate_your_api_key"
BOT_TOKEN    = "your_telegram_bot_token"
ADMIN_CHAT   = 123456789  # your Telegram user ID

def gw_create(amount, order_id):
    r = requests.post(f"{GATEWAY_URL}/api/payments/create",
        json={"amount_firo": amount, "order_id": order_id,
              "timeout_minutes": 20},
        headers={"X-API-Key": API_KEY}, timeout=15)
    return r.json() if r.ok else None

async def pay_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    if not ctx.args:
        await update.message.reply_text("Usage: /pay 2.5"); return
    amount = float(ctx.args[0])
    order_id = f"TG-{int(time.time())}"
    r = gw_create(amount, order_id)
    if not r:
        await update.message.reply_text("❌ Gateway error"); return
    kb = [[InlineKeyboardButton("Go to Checkout →", url=r["checkout_url"])]]
    await update.message.reply_text(
        f"💳 Send *{amount:.4f} FIRO*\n"
        f"Order: `{order_id}`\n"
        f"Expires: 20 minutes",
        parse_mode="Markdown",
        reply_markup=InlineKeyboardMarkup(kb),
    )

if __name__ == "__main__":
    app = Application.builder().token(BOT_TOKEN).build()
    app.add_handler(CommandHandler("pay", pay_cmd))
    app.run_polling()

Setting Up Webhooks

Configure your webhook URL in Dashboard → API & Webhooks.

Requirements for your webhook endpoint

MethodPOSTMust accept POST requests
Response200 OKReturn 200 within 10 seconds
ContentJSONResponse body: {"ok": true}
HTTPSRecommendedUse HTTPS in production
🔒 The gateway signs every webhook with X-FiroGate-Signature (HMAC-SHA256). Always verify before processing.

Webhook Headers

POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-FiroGate-Event: payment.confirmed
X-FiroGate-Signature: a1b2c3d4e5f6...  <-- HMAC-SHA256
X-FiroGate-Nonce: 7f8a9b0c...          <-- unique per request
X-FiroGate-Timestamp: 1705312500       <-- unix timestamp
User-Agent: FiroGate/1.0

Signature Algorithm

The signature is computed as HMAC-SHA256(json_body, webhook_secret) where the JSON body is serialized with sorted keys and compact separators (, and : with no spaces).

# Python equivalent of how the server signs:
import json, hmac, hashlib
body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
sig  = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()

Replay Protection

Every webhook includes a nonce (random hex string) and timestamp (unix seconds). Your handler should:

1. Reject stale webhooks — if abs(now - timestamp) > 300 (5 minutes)

2. Reject replays — store seen nonces in Redis/DB and reject duplicates

Verify Webhook Signature

The signature is HMAC-SHA256(sorted_compact_json, webhook_secret). Keys are sorted alphabetically, separators are , and : with no spaces.

import hmac, hashlib, json, time

def verify_webhook(payload: dict, signature: str, secret: str) -> bool:
    # 1. Recompute signature (sorted keys, compact separators)
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
    expected  = hmac.new(secret.encode(), canonical, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature):
        return False
    # 2. Check timestamp freshness
    ts = payload.get("timestamp", 0)
    if abs(time.time() - ts) > 300:
        return False
    return True

# Django example
def webhook_view(request):
    data = json.loads(request.body)
    sig  = request.headers.get('X-FiroGate-Signature', '')
    if not verify_webhook(data, sig, WEBHOOK_SECRET):
        return HttpResponse(status=401)
    # process event...
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  // Sort keys + compact JSON to match server signing
  const keys   = Object.keys(payload).sort();
  const sorted = {};
  keys.forEach(k => sorted[k] = payload[k]);
  const canonical = JSON.stringify(sorted);

  const expected = crypto
    .createHmac('sha256', secret)
    .update(canonical)
    .digest('hex');
  try {
    return crypto.timingSafeEqual(
      Buffer.from(expected), Buffer.from(signature)
    );
  } catch { return false; }
}

// Usage in Express:
const sig = req.headers['x-firogate-signature'];
if (!verifyWebhook(req.body, sig, SECRET)) { ... }
function verifyWebhook($data, $signature, $secret): bool {
    // Sort keys + compact JSON
    ksort($data);
    $canonical = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $expected  = hash_hmac('sha256', $canonical, $secret);
    return hash_equals($expected, $signature);
}

// Usage
$body = file_get_contents('php://input');
$data = json_decode($body, true);
$sig  = $_SERVER['HTTP_X_FIROGATE_SIGNATURE'] ?? '';
if (!verifyWebhook($data, $sig, WEBHOOK_SECRET)) {
    http_response_code(401); exit;
}
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "sort"
)

func verifyWebhook(data map[string]interface{}, signature, secret string) bool {
    // json.Marshal sorts keys by default in Go
    canonical, _ := json.Marshal(data)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(canonical)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signature))
}

// Usage: sig = r.Header.Get("X-FiroGate-Signature")

Tor / Onion Access

FiroGate supports a .onion hidden service. One onion address serves the API, checkout, and dashboard — no subdomain routing needed.

Same API, different base URL. Replace https://api.firogate.com with your .onion address. Every endpoint, header, and response is identical.

Environment Variables

ONION_URLstringFull .onion base URL — e.g. http://yourxxx.onion
TOR_ENABLEDboolRoute outbound .onion webhook calls through the local SOCKS5 proxy
TOR_SOCKS_PORTintLocal SOCKS5 proxy port (default: 9050)
TOR_ALL_TRAFFICboolRoute ALL outbound HTTP through Tor — not just .onion URLs

Tor Hidden Service (torrc)

Point HiddenServicePort to your backend. The generated hostname file is your ONION_URL value.

HiddenServiceDir /var/lib/tor/firogate/
HiddenServicePort 80 127.0.0.1:8000

Making API Calls Over Tor

# --socks5-hostname routes DNS resolution through Tor
curl --socks5-hostname 127.0.0.1:9050 \
  -X POST http://yourxxx.onion/api/payments/create \
  -H "X-API-Key: fgate_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"amount_firo":2.5,"order_id":"ORD-1234","timeout_minutes":20}'
# pip install requests[socks]
import requests

ONION_URL = "http://yourxxx.onion"
API_KEY   = "fgate_your_api_key"

proxies = {
    "http":  "socks5h://127.0.0.1:9050",
    "https": "socks5h://127.0.0.1:9050",
}

r = requests.post(
    f"{ONION_URL}/api/payments/create",
    json={
        "amount_firo":     2.5,
        "order_id":        "ORD-1234",
        "timeout_minutes": 20,
    },
    headers={"X-API-Key": API_KEY},
    proxies=proxies,
    timeout=30,
)
payment = r.json()
print(f"Checkout: {payment['checkout_url']}")
// npm install socks-proxy-agent node-fetch
const { SocksProxyAgent } = require('socks-proxy-agent');
const fetch = require('node-fetch');

const ONION_URL = 'http://yourxxx.onion';
const API_KEY   = 'fgate_your_api_key';
const agent     = new SocksProxyAgent('socks5h://127.0.0.1:9050');

const res = await fetch(`${ONION_URL}/api/payments/create`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': API_KEY,
  },
  body: JSON.stringify({
    amount_firo: 2.5, order_id: 'ORD-1234', timeout_minutes: 20,
  }),
  agent,
});
const payment = await res.json();
console.log(`Checkout: ${payment.checkout_url}`);
Use socks5h:// (not socks5://) to ensure DNS resolution also goes through Tor and your real IP is never exposed.

AI Agent Integration Prompt

Copy the message below and provide it to your AI coding agent to generate a complete payment integration.

Do NOT share your real API Key or Webhook Secret with any AI agent. Use the placeholder values below. Replace them only in your backend code, and always store them securely in environment variables.
## Payment Gateway Integration — AI Agent Instructions

Your task is to integrate a FIRO cryptocurrency payment gateway into my website or backend service.
Tell him what is your service and what to add or in which place tell him exactly after payment confirmed what to do and how to make the users use your service or your product don't mis anything we just confirming the paymets then fire to your webhok Mr. or Ms user 
Follow all instructions carefully.

── API Base URL ──
https://api.firogate.com

── Authentication ──
All API requests require the header:
  X-API-Key: <API_KEY>

IMPORTANT:
- The API key shown above is a placeholder.
- NEVER hardcode the API key in source code.
- ALWAYS load it from a secure backend environment variable.

Example:
  export FIROGATE_API_KEY="your_real_api_key"

── Create Payment ──
Send a POST request to create a payment:

  POST /api/payments/create

  Headers:
    Content-Type: application/json
    X-API-Key: (read from environment variable)

  Body:
    {
      "amount_firo": 2.5,
      "order_id": "ORD-1234",
      "order_description": "Product Name",
      "success_url": "https://yoursite.com/success",
      "cancel_url": "https://yoursite.com/cancel",
      "timeout_minutes": 20
    }

Response:
- The response will include checkout_url
- Redirect the buyer to this URL to complete the payment

── Webhook Handler ──
After payment confirmation, FiroGate sends a POST request to your webhook endpoint.

Headers:
  X-FiroGate-Event:     payment.confirmed
  X-FiroGate-Signature: <hmac_sha256_hex>
  X-FiroGate-Nonce:     <unique_per_request>
  X-FiroGate-Timestamp: <unix_seconds>

── Webhook Secret ──
Store your webhook secret securely:

  export FIROGATE_WEBHOOK_SECRET="your_real_secret"

NEVER expose this secret in frontend code or public repositories.

── Verify Webhook Signature ──
To validate incoming webhooks:

1. Parse the JSON payload
2. Sort keys alphabetically
3. Serialize using compact JSON (no spaces)
4. Compute: HMAC-SHA256(sorted_json, webhook_secret)
5. Compare with X-FiroGate-Signature
6. Reject if timestamp is older than 5 minutes
7. Reject if nonce was already used (prevent replay attacks)

Python example:

  import os, json, hmac, hashlib, time

  secret = os.environ["FIROGATE_WEBHOOK_SECRET"]

  def verify_webhook(payload, signature):
      canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
      expected = hmac.new(secret.encode(), canonical, hashlib.sha256).hexdigest()

      if not hmac.compare_digest(expected, signature):
          return False

      if abs(time.time() - payload.get("timestamp", 0)) > 300:
          return False

      return True

── Security Rules (Critical) ──
- NEVER hardcode API keys or secrets
- NEVER expose secrets to the frontend
- NEVER send secrets to any AI tool or third-party service
- ALWAYS validate webhook signatures before processing
- ALWAYS enforce timestamp validation (max 5 minutes)
- ALWAYS track nonces to prevent replay attacks
- ALWAYS use HTTPS

── Expected Integration Flow ──
1. User clicks "Pay with FIRO"
2. Backend creates payment using API key (from env)
3. Backend returns checkout_url
4. User is redirected to checkout page
5. User completes payment
6. Webhook is sent to backend
7. Backend verifies signature and timestamp
8. Order is marked as paid
9. User is redirected to success page

── Final Notes ──
- All sensitive operations must happen on the backend only
- The frontend should only handle UI and redirects
- Treat all webhook data as untrusted until verified
Copy this prompt and paste it into your AI agent. All keys in the prompt are fake placeholders — replace them with your real keys only in your server environment variables.