Guides, API reference and code samples for issuing and managing virtual cards. REST over HTTPS, JSON in and out, Bearer-key auth, signed webhooks and a full test mode.
https://gpaynow.net/api/v2
# 1. Check your test key curl https://gpaynow.net/api/v2/ping \ -H "Authorization: Bearer gpn_test_..." # 200 OK { "data": { "ok": true }, "meta": { "request_id": "req_01HF7Q...", "environment": "test" } }
Use a gpn_test_ key: test mode approves KYC instantly and uses simulated money. Send an Idempotency-Key on every POST so retries are safe.
POST /cardholdersGET /productsPOST /cardscurl https://gpaynow.net/api/v2/ping \ -H "Authorization: Bearer gpn_test_..."
const API = "https://gpaynow.net/api/v2";
const headers = { Authorization: `Bearer ${process.env.GPAYNOW_KEY}` };
const ping = await fetch(`${API}/ping`, { headers }).then(r => r.json());
console.log(ping.meta.environment); // "test"
$ch = curl_init('https://gpaynow.net/api/v2/ping');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GPAYNOW_KEY')],
]);
$ping = json_decode(curl_exec($ch), true);
echo $ping['meta']['environment']; // "test"
import os, requests
API = "https://gpaynow.net/api/v2"
H = {"Authorization": f"Bearer {os.environ['GPAYNOW_KEY']}"}
print(requests.get(f"{API}/ping", headers=H).json()["meta"]["environment"])
curl https://gpaynow.net/api/v2/cardholders \
-H "Authorization: Bearer gpn_test_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7b1c-cardholder-001" \
-d '{
"first_name": "Amina", "last_name": "Okafor",
"email": "amina@example.com", "date_of_birth": "1994-03-12",
"nationality": "NG",
"address": { "line1": "12 Allen Ave", "city": "Lagos",
"postal_code": "100001", "country": "NG" },
"external_id": "user_8841"
}'
const ch = await fetch(`${API}/cardholders`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
first_name: "Amina", last_name: "Okafor", email: "amina@example.com",
date_of_birth: "1994-03-12", nationality: "NG",
address: { line1: "12 Allen Ave", city: "Lagos", postal_code: "100001", country: "NG" },
external_id: "user_8841",
}),
}).then(r => r.json());
// ch.data.id -> "ch_9LqT2", ch.data.kyc_status -> "pending"
$post = function (string $path, array $body) {
$ch = curl_init('https://gpaynow.net/api/v2' . $path);
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GPAYNOW_KEY'),
'Content-Type: application/json', 'Idempotency-Key: ' . bin2hex(random_bytes(16))],
CURLOPT_POSTFIELDS => json_encode($body)]);
return json_decode(curl_exec($ch), true);
};
$holder = $post('/cardholders', ['first_name' => 'Amina', 'last_name' => 'Okafor',
'email' => 'amina@example.com', 'date_of_birth' => '1994-03-12', 'nationality' => 'NG',
'address' => ['line1' => '12 Allen Ave', 'city' => 'Lagos', 'postal_code' => '100001', 'country' => 'NG']]);
import uuid
holder = requests.post(f"{API}/cardholders",
headers={**H, "Idempotency-Key": str(uuid.uuid4())},
json={"first_name": "Amina", "last_name": "Okafor", "email": "amina@example.com",
"date_of_birth": "1994-03-12", "nationality": "NG",
"address": {"line1": "12 Allen Ave", "city": "Lagos",
"postal_code": "100001", "country": "NG"}}).json()["data"]
curl https://gpaynow.net/api/v2/products \
-H "Authorization: Bearer gpn_test_..."
# { "data": [ { "id": "prd_visa_us_01", "network": "visa",
# "issuing_country": "US", "currency": "USD",
# "features": { "three_d_secure": true, "apple_pay": true, ... } } ] }
const { data: products } = await fetch(`${API}/products`, { headers }).then(r => r.json());
const visaUs = products.find(p => p.network === "visa" && p.issuing_country === "US");
$ch = curl_init('https://gpaynow.net/api/v2/products');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('GPAYNOW_KEY')]]);
$products = json_decode(curl_exec($ch), true)['data'];
products = requests.get(f"{API}/products", headers=H).json()["data"]
visa_us = next(p for p in products if p["network"] == "visa")
curl https://gpaynow.net/api/v2/cards \
-H "Authorization: Bearer gpn_test_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7b1c-card-001" \
-d '{ "cardholder_id": "ch_9LqT2", "product_id": "prd_visa_us_01",
"amount": 50.00, "name_on_card": "AMINA OKAFOR" }'
# 201 Created
# { "data": { "id": "card_3KpX9", "status": "creating", "last4": "4821",
# "balance": 50.00, "fee_charged": 3.00 } }
const card = await fetch(`${API}/cards`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({ cardholder_id: ch.data.id, product_id: visaUs.id, amount: 50 }),
}).then(r => r.json());
// Wait for the card.issued webhook before showing the card as ready.
$card = $post('/cards', [
'cardholder_id' => $holder['data']['id'],
'product_id' => 'prd_visa_us_01',
'amount' => 50,
]);
echo $card['data']['id'], ' ', $card['data']['status']; // card_3KpX9 creating
card = requests.post(f"{API}/cards",
headers={**H, "Idempotency-Key": str(uuid.uuid4())},
json={"cardholder_id": holder["id"], "product_id": visa_us["id"],
"amount": 50}).json()["data"]
print(card["id"], card["status"])
Authorization: Bearer with scoped keys and IP allowlist.
HTTP status plus a stable error.code you can switch on.
Same key within 24 h returns the original response.
limit + cursor, with has_more in every list.
| Environment | Key prefix | Money | KYC | Rate limit |
|---|---|---|---|---|
| Test | gpn_test_ | Simulated | Approved instantly | 60 / min |
| Live | gpn_live_ | Real, from your balance | Real verification | By plan (60–300 / min) |
Each delivery has a Gpaynow-Signature: t=…,v1=… header. Compute HMAC-SHA256(secret, t + "." + raw_body), compare in constant time and reject events older than 5 minutes.
card.issued
card.funded
card.frozen
card.terminated
card.3ds_otp
transaction.authorized
transaction.cleared
transaction.declined
cardholder.kyc_approved
balance.low
All events →
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
function gpaynow_verify(string $raw, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(time() - (int) ($p['t'] ?? 0)) > 300) return false;
$expected = hash_hmac('sha256', $p['t'] . '.' . $raw, $secret);
return hash_equals($expected, $p['v1'] ?? '');
}
// $raw = file_get_contents('php://input');
import hmac, hashlib, time
def verify(raw: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
curl, Node.js, PHP and Python on every endpoint in the reference.
Test keys and simulated money for approved early-access members.
Machine-readable spec for code generation and API clients.
Every request ready to run against your test key.
2.0.0-preview · 16 Oct 2026: first public preview of the v2 reference. Endpoints open to early-access members first.
Stuck on an integration? Open a ticket from the contact page with your request ID (meta.request_id) and we'll trace it.
Plans and business questions: Card API pricing.
Tell us what you are building. Approved early-access members receive gpn_test_ keys by email so they can integrate before launch.