Accept crypto payments on any website by embedding a signed payment link that redirects customers to your gateway and back.
Your gateway uses HMAC-SHA256 signatures to verify every incoming payment request. The external site signs the link before sending the customer — the gateway checks the signature before allowing payment. After completion, the customer is redirected back with a signed response you can verify.
tx, ref, status=success to your return_url. Failure sends ref, status=failed. Verify the tx hash on-chain to confirm.Contact the gateway operator to receive your HMAC secret key — a long random string shared only between you and the gateway. Keep it server-side only. Never put it in frontend code, git, or environment files that ship to the browser.
You will also receive:
| Item | Example | What it is |
|---|---|---|
| HMAC Secret | sk_gw_a3f9... | Your signing key — keep private |
| Gateway Origin | https://pay.example.com | Base URL of the gateway |
| Product ID | 42 | ID of the product registered on the gateway |
| Ref Code | MYSHOP | Identifier for your site — agreed with gateway operator |
A valid payment link looks like this:
This code runs on your server when you render the checkout button. Never run it in the browser.
const crypto = require("crypto");
const GATEWAY_SECRET = process.env.GATEWAY_HMAC_SECRET; // from your .env
const GATEWAY_ORIGIN = "https://pay.example.com";
const PRODUCT_ID = "42";
const REF_CODE = "MYSHOP";
function buildSignedUrl(returnUrl) {
const params = {
productId: PRODUCT_ID,
ref: REF_CODE,
return_url: returnUrl,
nonce: crypto.randomBytes(8).toString("hex"),
expires: (Date.now() + 10 * 60 * 1000).toString(), // 10 minutes
};
// Sign: sort keys alphabetically, join as key=value&key=value, then HMAC
const message = Object.keys(params)
.sort()
.map((k) => `${k}=${params[k]}`)
.join("&");
params.sig = crypto
.createHmac("sha256", GATEWAY_SECRET)
.update(message)
.digest("hex");
const query = new URLSearchParams(params).toString();
return `${GATEWAY_ORIGIN}/pay/${PRODUCT_ID}?${query}`;
}
// Usage — call when rendering your checkout page:
const payUrl = buildSignedUrl("https://myshop.com/order/confirm");
// Embed payUrl in your checkout button hrefNode.js
<?php
function buildSignedUrl($returnUrl) {
$secret = getenv('GATEWAY_HMAC_SECRET');
$origin = 'https://pay.example.com';
$productId = '42';
$ref = 'MYSHOP';
$params = [
'productId' => $productId,
'ref' => $ref,
'return_url' => $returnUrl,
'nonce' => bin2hex(random_bytes(8)),
'expires' => strval(intval(microtime(true) * 1000) + 600000),
];
// Sort keys alphabetically, build key=value string, sign
ksort($params);
$message = implode('&', array_map(
fn($k, $v) => "$k=$v",
array_keys($params),
array_values($params)
));
$params['sig'] = hash_hmac('sha256', $message, $secret);
return $origin . '/pay/' . $productId . '?' . http_build_query($params);
}
// Usage:
$payUrl = buildSignedUrl('https://myshop.com/order/confirm');
?>
<a href="<?= htmlspecialchars($payUrl) ?>">Pay with Crypto</a>PHP
import hmac, hashlib, os, secrets, time
from urllib.parse import urlencode
def build_signed_url(return_url):
secret = os.environ["GATEWAY_HMAC_SECRET"].encode()
origin = "https://pay.example.com"
product_id = "42"
params = {
"productId": product_id,
"ref": "MYSHOP",
"return_url": return_url,
"nonce": secrets.token_hex(8),
"expires": str(int(time.time() * 1000) + 600_000),
}
# Sort keys alphabetically, build message, sign
message = "&".join(f"{k}={params[k]}" for k in sorted(params))
params["sig"] = hmac.new(secret, message.encode(), hashlib.sha256).hexdigest()
return f"{origin}/pay/{product_id}?{urlencode(params)}"
# Usage:
pay_url = build_signed_url("https://myshop.com/order/confirm")
Python
After payment, the customer is redirected to your return_url with these query params appended:
| Param | When present | Value |
|---|---|---|
status | Always | success or failed |
tx | On success only | The blockchain transaction hash, e.g. 0xabc123... |
ref | Always | Your ref code echoed back, e.g. MYSHOP |
tx hash on-chain or via a block explorer API before marking an order as paid. The redirect can be faked by anyone who guesses the URL structure.
// pages/order/confirm.js (Next.js API route or page)
export default function ConfirmPage({ query }) {
const { status, tx, ref } = query;
if (status === "success" && tx) {
// 1. Verify tx on-chain (recommended)
// GET https://api.etherscan.io/api?module=transaction&action=gettxreceiptstatus&txhash={tx}
// Check status === "1" (success) and to === your contract address
// 2. Then mark the order as paid in your database
markOrderPaid({ ref, tx });
}
}Node.js
// Check a transaction was successful on-chain
const res = await fetch(
`https://api.etherscan.io/api?module=transaction&action=gettxreceiptstatus` +
`&txhash=${tx}&apikey=${YOUR_ETHERSCAN_KEY}`
);
const { result } = await res.json();
if (result.status === "1") {
// Transaction succeeded on-chain — safe to fulfil the order
} else {
// Transaction failed or not found — do not fulfil
}Node.js
| Attack | How it's prevented |
|---|---|
| Forging a payment link manually | The sig is a cryptographic HMAC over all params. Without the secret, a valid sig cannot be produced. |
| Reusing a valid link | nonce is random per request. expires enforces a 10-minute window. After that, the link is dead. |
| Changing any parameter (ref, return_url, productId) | All params are included in the signed message. Modifying any one of them breaks the signature. |
| Timing attacks on signature comparison | The gateway uses crypto.timingSafeEqual() — comparison time is constant regardless of input. |
| Faking a success redirect | Always verify the tx hash independently on-chain. The redirect alone is not proof of payment. |
| Customer tampering with URL after landing | Params are read once into locked state and the URL is immediately cleaned from the browser bar. |
| ✓ | Secret key stored in server environment variable only (not in code or git) |
| ✓ | buildSignedUrl is called server-side, never in the browser |
| ✓ | Your return_url is HTTPS |
| ✓ | Confirmation page verifies tx on-chain before fulfilling orders |
| ✓ | Orders are only marked paid after on-chain verification, not after redirect |
| ✓ | Tested with an expired link — gateway should reject it |
| ✓ | Tested with a tampered param — gateway should reject it |
ref code and return_url domain with the gateway operator so they can whitelist you. Contact them to receive your secret key.
productId, ref, return_url, nonce, expiresexpires=...&nonce=...&productId=...&ref=...&return_url=...HMAC-SHA256(message, secret), hex-encode the result&sig=<hex> to your URL query string| Param | Type | Description |
|---|---|---|
productId | string | Product ID as registered on the gateway |
ref | string | Your site identifier — agreed with gateway operator |
return_url | string | Full URL to redirect to after payment (not URL-encoded when signing) |
nonce | string | Random hex string — generate fresh for every link |
expires | string | Unix timestamp in milliseconds — recommend now + 10 minutes |
sig | string | HMAC-SHA256 hex digest — computed last, over all above params |