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.
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.
Authorization: Bearer your_api_key_hereWhere to find your API key
- Log in to the GhostPay dashboard.
- Go to Profile & API in the sidebar.
- 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.
{
"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.
| Network | Canonical id | Accepted aliases (input) | Tokens |
|---|---|---|---|
| BSC (BEP-20) | bsctestnet | bsc, bnb, bep20 | usdt, usdc |
| Ethereum (ERC-20) | sepolia | ethereum, eth, erc20 | usdt, usdc |
| Tron (TRC-20) | tron | trc20 | usdt, 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.
Create payment link
/v1/payment-linksCreate a new payment link
Request body
| Parameter | Type | Description |
|---|---|---|
titlerequired | string | Name of the payment link (1-200 chars). |
amount_usdrequired | string | Price in USD (e.g. "30", "9.99"). |
tokensrequired | string[] | Accepted tokens: "usdt" and/or "usdc". |
networksrequired | string[] | Accepted networks: canonical ids or aliases (e.g. ["bsc", "tron"]). |
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": "Pro Plan - Monthly",
"amount_usd": "29.99",
"tokens": ["usdt", "usdc"],
"networks": ["bsc", "tron"]
}'{
"data": {
"linkid": "GP-A1B2C3",
"title": "Pro Plan - Monthly",
"amount_usd": 29.99,
"tokens": ["usdt", "usdc"],
"networks": ["bsctestnet", "tron"],
"checkout_url": "https://ghostpay.cloud/checkout/GP-A1B2C3"
}
}List payment links
/v1/payment-linksList your payment links (paginated)
Query parameters
| Parameter | Type | Description |
|---|---|---|
limit | int | Results per page (1-100, default 20). |
offset | int | Number of results to skip (default 0). |
curl "https://ghostpay-production.up.railway.app/v1/payment-links?limit=10&offset=0" \
-H "Authorization: Bearer YOUR_API_KEY"{
"data": [
{
"linkid": "GP-A1B2C3",
"title": "Pro Plan - Monthly",
"amount_usd": 29.99,
"tokens": ["usdt", "usdc"],
"networks": ["bsctestnet", "tron"],
"checkout_url": "https://ghostpay.cloud/checkout/GP-A1B2C3",
"active": true
}
],
"pagination": {
"total": 1,
"limit": 10,
"offset": 0
}
}Get link details
/v1/payment-links/{linkid}Get details for a single payment link
Returns 404 if the link does not exist or belongs to another merchant.
curl https://ghostpay-production.up.railway.app/v1/payment-links/GP-A1B2C3 \
-H "Authorization: Bearer YOUR_API_KEY"{
"data": {
"linkid": "GP-A1B2C3",
"title": "Pro Plan - Monthly",
"amount_usd": 29.99,
"tokens": ["usdt", "usdc"],
"networks": ["bsctestnet", "tron"],
"checkout_url": "https://ghostpay.cloud/checkout/GP-A1B2C3",
"active": true
}
}Deactivate payment link
/v1/payment-links/{linkid}Soft-delete a payment link
This is a soft delete. Existing in-flight orders can still be paid, but no new orders will be created from this link.
curl -X DELETE https://ghostpay-production.up.railway.app/v1/payment-links/GP-A1B2C3 \
-H "Authorization: Bearer YOUR_API_KEY"{
"data": {
"linkid": "GP-A1B2C3",
"active": false
}
}List orders
/v1/ordersList orders with optional filters
Query parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter: paid, pending, or expired. |
token | string | Filter by token (usdt or usdc). |
network | string | Filter by network (canonical id or alias). |
limit | int | Results per page (1-100, default 20). |
offset | int | Number of results to skip (default 0). |
curl "https://ghostpay-production.up.railway.app/v1/orders?status=paid&limit=5" \
-H "Authorization: Bearer YOUR_API_KEY"{
"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
/v1/orders/{order_id}Get details for a specific order
Returns 404 if the order does not exist or belongs to another merchant.
curl https://ghostpay-production.up.railway.app/v1/orders/GP-BSC-48291734 \
-H "Authorization: Bearer YOUR_API_KEY"{
"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.
{
"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
| Parameter | Type | Description |
|---|---|---|
X-GhostPay-Timestamprequired | string | Unix time (seconds) when the delivery was signed. |
X-GhostPay-Signaturerequired | string | t=<ts>,v1=<hex HMAC-SHA256 of "<ts>.<raw body>"> using your webhook secret. |
How to verify
- Read the raw request body (before any JSON parsing).
- Take
tfrom the signature header; reject if|now − t| > 300seconds (replay protection). - Compute
HMAC-SHA256(secret, `${t}.${rawBody}`)as hex. - Compare it to
v1with a constant-time comparison. Respond2xxquickly; process asynchronously. - Deliveries may be retried — make your handler idempotent on
order_id.
// 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:
{
"detail": {
"code": "not_found",
"message": "Payment link not found."
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_amount | amount_usd is not a positive number. |
| 400 | invalid_tokens | One or more tokens are not supported (only usdt, usdc). |
| 400 | invalid_networks | One or more networks are not supported. |
| 400 | invalid_token_network | Token not available on selected networks. |
| 401 | invalid_auth_header | Missing or malformed Authorization header. |
| 401 | invalid_api_key | API key does not match any merchant. |
| 404 | not_found | Resource not found or belongs to another merchant. |
| 422 | (validation) | Request body failed validation. |
| 429 | (rate limit) | Too many requests. Wait and retry. |
| 500 | create_failed | Server 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.
Create a payment link
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"]}'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.
Receive the webhook (or poll)
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
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
}