GhostPay API

The GhostPay API lets you accept stablecoin payments programmatically. Create payment links, track orders, receive signed webhooks and integrate non-custodial on-chain payments into your app — all with a simple REST API.

Bearer token auth
JSON REST API
60 req/min rate limit

Base URL: https://ghostpay-production.up.railway.app/v1

Authentication

Every request requires your merchant API key in the Authorization header. The API key is different from your dashboard login session and never expires unless you rotate it.

header
Authorization: Bearer your_api_key_here

Where to find your API key

  1. Log in to the GhostPay dashboard.
  2. Go to Profile & API in the sidebar.
  3. Copy the key. Use Rotate API key if it ever leaks — the old key stops working immediately.

Security: Keep your API key secret. Never expose it in client-side code, mobile apps or public repositories. Use it only from your backend server.

Rate limits

The API is rate-limited to 60 requests per minute per API key. Exceeding this limit returns 429 Too Many Requests. Prefer webhooks over tight polling loops.

Response
{
  "detail": "Too many requests, please slow down."
}

Tokens & networks

GhostPay accepts two stablecoins — usdt and usdc — on three networks. Requests accept the canonical id or any alias; responses, order ids and webhooks always use the canonical id.

NetworkCanonical idAccepted aliases (input)Tokens
BSC (BEP-20)bsctestnetbsc, bnb, bep20usdt, usdc
Ethereum (ERC-20)sepoliaethereum, eth, erc20usdt, usdc
Tron (TRC-20)trontrc20usdt, usdc

The ids bsctestnet and sepolia are historical names: they refer to BSC and Ethereum mainnet in production. Order ids look like GP-BSC-48291734 — treat them as opaque strings and always read the network from the network field.

List orders

GET/v1/orders

List orders with optional filters

Query parameters

ParameterTypeDescription
statusstringFilter: paid, pending, or expired.
tokenstringFilter by token (usdt or usdc).
networkstringFilter by network (canonical id or alias).
limitintResults per page (1-100, default 20).
offsetintNumber of results to skip (default 0).
bash
curl "https://ghostpay-production.up.railway.app/v1/orders?status=paid&limit=5" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "data": [
    {
      "order_id": "GP-BSC-48291734",
      "status": "paid",
      "amount": "30.148293",
      "token": "usdt",
      "network": "bsctestnet",
      "customer_email": "buyer@example.com",
      "created_at": "2026-09-03T14:32:00+00:00",
      "expires_at": "2026-09-03T14:47:00+00:00",
      "paid_at": "2026-09-03T14:36:12+00:00",
      "tx_hash": "0x9f3c…e1a7",
      "link_id": "GP-A1B2C3",
      "usd_value": 29.99
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 5,
    "offset": 0
  }
}

Get order details

GET/v1/orders/{order_id}

Get details for a specific order

Returns 404 if the order does not exist or belongs to another merchant.

bash
curl https://ghostpay-production.up.railway.app/v1/orders/GP-BSC-48291734 \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "data": {
    "order_id": "GP-BSC-48291734",
    "status": "paid",
    "amount": "30.148293",
    "token": "usdt",
    "network": "bsctestnet",
    "customer_email": "buyer@example.com",
    "created_at": "2026-09-03T14:32:00+00:00",
    "expires_at": "2026-09-03T14:47:00+00:00",
    "paid_at": "2026-09-03T14:36:12+00:00",
    "tx_hash": "0x9f3c…e1a7",
    "link_id": "GP-A1B2C3",
    "usd_value": 29.99
  }
}

Webhooks

Configure a webhook URL and secret in the dashboard to receive a POST with a JSON body every time an order is paid. Every delivery is signed so you can verify it came from GhostPay and was not replayed.

Webhook payload
{
  "event": "order.paid",
  "order_id": "GP-BSC-48291734",
  "link_id": "GP-A1B2C3",
  "status": "paid",
  "amount": "30.148293",
  "token": "usdt",
  "network": "bsctestnet",
  "usd_value": 29.99,
  "customer_email": "buyer@example.com",
  "tx_hash": "0x9f3c…e1a7",
  "paid_at": "2026-09-03T14:36:12+00:00"
}

Signature headers

ParameterTypeDescription
X-GhostPay-TimestamprequiredstringUnix time (seconds) when the delivery was signed.
X-GhostPay-Signaturerequiredstringt=<ts>,v1=<hex HMAC-SHA256 of "<ts>.<raw body>"> using your webhook secret.

How to verify

  1. Read the raw request body (before any JSON parsing).
  2. Take t from the signature header; reject if |now − t| > 300 seconds (replay protection).
  3. Compute HMAC-SHA256(secret, `${t}.${rawBody}`) as hex.
  4. Compare it to v1 with a constant-time comparison. Respond 2xx quickly; process asynchronously.
  5. Deliveries may be retried — make your handler idempotent on order_id.
javascript
// Node.js (Express) — verify a GhostPay webhook
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.GHOSTPAY_WEBHOOK_SECRET;

// Use the RAW body for signing — express.raw() keeps it untouched.
app.post("/webhooks/ghostpay", express.raw({ type: "*/*" }), (req, res) => {
  const ts = req.get("X-GhostPay-Timestamp") || "";
  const sigHeader = req.get("X-GhostPay-Signature") || "";
  const parts = Object.fromEntries(
    sigHeader.split(",").map((kv) => kv.split("=").map((s) => s.trim()))
  );

  // 1. Timestamp tolerance (5 minutes)
  const now = Math.floor(Date.now() / 1000);
  if (!ts || parts.t !== ts || Math.abs(now - Number(ts)) > 300) {
    return res.status(400).send("stale or missing timestamp");
  }

  // 2. Recompute HMAC over "<ts>.<raw body>"
  const rawBody = req.body.toString("utf8");
  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${ts}.${rawBody}`)
    .digest("hex");

  // 3. Constant-time compare
  const given = Buffer.from(parts.v1 || "", "hex");
  const want = Buffer.from(expected, "hex");
  if (given.length !== want.length || !crypto.timingSafeEqual(given, want)) {
    return res.status(401).send("invalid signature");
  }

  const event = JSON.parse(rawBody);
  if (event.event === "order.paid") {
    // TODO: deliver the product for event.order_id (idempotently)
  }
  res.sendStatus(200);
});

app.listen(3000);

Error codes

All errors follow the same JSON format:

Response
{
  "detail": {
    "code": "not_found",
    "message": "Payment link not found."
  }
}
StatusCodeMeaning
400invalid_amountamount_usd is not a positive number.
400invalid_tokensOne or more tokens are not supported (only usdt, usdc).
400invalid_networksOne or more networks are not supported.
400invalid_token_networkToken not available on selected networks.
401invalid_auth_headerMissing or malformed Authorization header.
401invalid_api_keyAPI key does not match any merchant.
404not_foundResource not found or belongs to another merchant.
422(validation)Request body failed validation.
429(rate limit)Too many requests. Wait and retry.
500create_failedServer error creating a resource.

Full integration example

End-to-end flow: create a payment link, share the checkout URL with your customer, then get notified (webhook) or poll for paid orders.

1

Create a payment link

bash
curl -X POST https://ghostpay-production.up.railway.app/v1/payment-links \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Premium Plan", "amount_usd": "49.99", "tokens": ["usdt"], "networks": ["bsc", "tron"]}'
2

Share the checkout URL

Send checkout_url (for example https://ghostpay.cloud/checkout/GP-A1B2C3) to your customer — via email, embed it in your site with the Pay with GhostPay button, or redirect after signup. The customer picks a token and network, sends the exact amount, and GhostPay detects the payment on-chain automatically.

3

Receive the webhook (or poll)

bash
curl "https://ghostpay-production.up.railway.app/v1/orders?status=paid&limit=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

When an order's status is "paid", the payment is confirmed on-chain and already settled to your wallet.

Complete JavaScript example

javascript
const API_KEY = process.env.GHOSTPAY_API_KEY;
const BASE = "https://ghostpay-production.up.railway.app/v1";

// 1. Create a payment link
const linkRes = await fetch(`${BASE}/payment-links`, {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    title: "Pro Plan",
    amount_usd: "19.99",
    tokens: ["usdt", "usdc"],
    networks: ["bsc"],
  }),
});

const { data: link } = await linkRes.json();
// link.checkout_url → e.g. "https://ghostpay.cloud/checkout/GP-A1B2C3"

// 2. Later — check for paid orders (or use webhooks instead)
const ordersRes = await fetch(`${BASE}/orders?status=paid`, {
  headers: { "Authorization": `Bearer ${API_KEY}` },
});
const { data: orders } = await ordersRes.json();

for (const order of orders) {
  // order.order_id, order.token, order.network, order.link_id
}