Gpaynow Card API documentation
Issue and manage prepaid virtual Visa and Mastercard cards for your users through one REST API. Create cardholders, issue cards, load and withdraw funds, control spending and receive real-time events.
Base URL: https://gpaynow.net/api/v2 — all requests use HTTPS and JSON.
How it works
- Get API access. Request early access to receive test keys. Live keys are issued after business verification (KYB).
- Fund your balance. Add USD to your Gpaynow balance with any stablecoin (USDT, USDC and more), $1.00 flat per top-up. Check it with GET /balance.
- Create a cardholder. Every card belongs to a verified person. Identity verification (KYC) runs on the cardholder before cards can be issued.
- Issue a card with an initial load. The load and the issue fee are taken from your balance.
- Manage the card: fund, unload, set limits, freeze, terminate, read transactions — and react to webhooks.
Quickstart
Issue your first test card in four requests. Replace gpn_test_xxx with your test key.
1. Check your key
curlcurl https://gpaynow.net/api/v2/ping \
-H "Authorization: Bearer gpn_test_xxx"
2. Create a cardholder
curlcurl https://gpaynow.net/api/v2/cardholders \
-H "Authorization: Bearer gpn_test_xxx" \
-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"
}'
3. Pick a product
curlcurl https://gpaynow.net/api/v2/products -H "Authorization: Bearer gpn_test_xxx"
4. Issue a card with $50
curlcurl https://gpaynow.net/api/v2/cards \
-H "Authorization: Bearer gpn_test_xxx" \
-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" }'
Same flow in code
Node.jsconst API = "https://gpaynow.net/api/v2";
const headers = { Authorization: `Bearer ${process.env.GPAYNOW_KEY}`, "Content-Type": "application/json" };
const ch = await fetch(`${API}/cardholders`, { method: "POST", headers: { ...headers, "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" } }) }).then(r => r.json());
const card = await fetch(`${API}/cards`, { method: "POST", headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({ cardholder_id: ch.data.id, product_id: "prd_visa_us_01", amount: 50 }) }).then(r => r.json());
console.log(card.data.id, card.data.last4, card.data.status);
PHP$api = 'https://gpaynow.net/api/v2';
$send = function (string $path, array $body) use ($api) {
$ch = curl_init($api . $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);
};
$card = $send('/cards', ['cardholder_id' => 'ch_9LqT2', 'product_id' => 'prd_visa_us_01', 'amount' => 50]);
echo $card['data']['id'];
Pythonimport os, uuid, requests
API = "https://gpaynow.net/api/v2"
H = {"Authorization": f"Bearer {os.environ['GPAYNOW_KEY']}"}
card = requests.post(f"{API}/cards", headers={**H, "Idempotency-Key": str(uuid.uuid4())},
json={"cardholder_id": "ch_9LqT2", "product_id": "prd_visa_us_01", "amount": 50}).json()
print(card["data"]["id"], card["data"]["status"])
Authentication
Send your API key as a Bearer token on every request. Keys are created in your Gpaynow dashboard under API → Keys and are shown only once.
headerAuthorization: Bearer gpn_live_4f8a…
| Key prefix | Mode |
|---|---|
gpn_test_ | Test mode — simulated money and instant KYC |
gpn_live_ | Live mode — real cards and real money (after KYB approval) |
Scopes
| Scope | Allows |
|---|---|
cardholders:read / cardholders:write | Read / create and change cardholders |
cards:read / cards:write | Read / issue, fund, unload, freeze and terminate cards |
cards:sensitive | Retrieve full card number and CVV (see card details) |
transactions:read | Read card and balance transactions |
webhooks:write | Manage webhook endpoints |
Test & live mode
One base URL serves both modes; the key decides. Test mode behaves like live, except: KYC is approved automatically, cards cannot make real purchases, and your test balance starts with simulated funds. Objects created in test mode are never visible in live mode.
Requests & responses
Send JSON bodies with Content-Type: application/json. Amounts are decimal US dollars (e.g. 50.00). Timestamps are ISO 8601 UTC. Every response uses the same envelope:
200{
"data": { "id": "card_3KpX9", "status": "active", "...": "..." },
"meta": { "request_id": "req_01HF7Q…", "environment": "test" }
}
Errors
402{
"error": { "type": "balance_error", "code": "insufficient_balance", "message": "Your balance is $12.40; this request needs $70.00." },
"meta": { "request_id": "req_01HF7R…", "environment": "live" }
}
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or invalid field (see error.param) |
| 401 | invalid_api_key | Key missing, wrong or revoked |
| 402 | insufficient_balance | Your Gpaynow balance can't cover the amount plus fees |
| 402 | insufficient_card_balance | Card balance too low for the unload |
| 403 | insufficient_scope · ip_not_allowed · live_not_enabled | Key lacks the scope, IP not allowlisted, or live access not approved |
| 404 | not_found | Object doesn't exist or isn't yours |
| 409 | idempotency_conflict | Same Idempotency-Key reused with a different body |
| 409 | card_has_pending_transactions | Card can't be terminated until pending transactions settle (24–72 h) |
| 422 | cardholder_kyc_not_approved · cardholder_email_exists · kyc_field_locked | Cardholder state blocks the action |
| 429 | rate_limited | Too many requests — retry after X-RateLimit-Reset |
| 503 | issuing_unavailable | Our issuing partner is temporarily unavailable — safe to retry with the same Idempotency-Key |
Retry only on 429 and 5xx, with exponential backoff (max 4 attempts), always reusing the same Idempotency-Key.
Idempotency
Send an Idempotency-Key header (any unique string up to 255 characters, e.g. a UUID) on every POST. For 24 hours, repeating the request with the same key returns the original response with Idempotency-Replayed: true — so a network retry never issues two cards or loads money twice. Reusing a key with a different body returns 409 idempotency_conflict.
Pagination
List endpoints accept limit (1–100, default 20) and cursor. Responses include "has_more" and "next_cursor":
GET /cards?limit=2{ "data": [ { "id": "card_3KpX9" }, { "id": "card_8WmQ1" } ], "has_more": true, "next_cursor": "eyJpZCI6ImNhcmRfOFdtUTEifQ", "meta": { "...": "..." } }
Rate limits
Default: 60 requests per minute per key (higher limits on request). Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Exceeding the limit returns 429 rate_limited.
Account
/pingany keyChecks that your key works and shows the mode.
200{ "data": { "ok": true }, "meta": { "request_id": "req_…", "environment": "test" } }
/accountany keyYour API client profile: company, plan, live access status and the scopes of the key in use.
200{ "data": { "id": "cli_2Vh8", "company": "Acme Fintech Ltd", "plan": "standard", "live_enabled": true, "key_scopes": ["cards:read","cards:write"] } }
/balanceany keyYour available Gpaynow balance in USD. Top it up from the dashboard with any stablecoin (USDT, USDC and more). Each top-up costs a flat $1.00, whatever the amount.
200{ "data": { "available": 1240.50, "currency": "USD" } }
/ledgertransactions:readMovements on your Gpaynow balance: deposits, card loads, unloads, refunds and fees.
| Query | Description |
|---|---|
type | deposit · card_load · card_unload · card_refund · fee |
card_id | Only entries for one card |
from / to | ISO dates |
200{ "data": [ { "id": "led_5Tq", "type": "card_load", "amount": -50.00, "fee": 0.00, "balance_after": 1240.50, "card_id": "card_3KpX9", "created_at": "2026-10-16T09:12:44Z" } ], "has_more": false }
/feesany keyThe live fee schedule for your account. Only card_issue depends on your plan (Starter $5.00, Growth $3.00, Scale $0.50); every other fee is the same on all plans. Fees are charged to your Gpaynow balance at the moment of each action. Always read fees from this endpoint — they can change with notice.
200{ "data": {
"plan": "growth",
"card_issue": 3.00, "card_monthly": 0.00,
"card_fund": 0.00, "transaction": 0.05,
"decline": 0.00, "freeze": 0.00,
"balance_topup": 1.00, "currency": "USD"
} }
Card products
A product defines the card network, BIN, issuing country and features (3-D Secure, Apple Pay / Google Pay, reloadable, single-use).
/productscards:read200{ "data": [ {
"id": "prd_visa_us_01", "network": "visa", "bin": "4xxxxx", "issuing_country": "US", "currency": "USD",
"features": { "three_d_secure": true, "apple_pay": true, "google_pay": true, "reloadable": true, "single_use": false },
"limits": { "max_load": 10000.00, "spending_limit": 1000.00, "spending_period": "monthly" },
"card_validity_months": 36
} ] }
/restricted-merchantscards:readMerchants and merchant categories that are blocked at network level (for example gambling). Purchases there are always declined.
Cardholders
A cardholder is the person who owns and uses cards. Identity verification (KYC) must be approved before cards can be issued in live mode.
/cardholderscardholders:write| Field | Type | Description |
|---|---|---|
first_namerequired | string | Legal first name |
last_namerequired | string | Legal last name |
emailrequired | string | Unique per cardholder; receives verification and security codes |
date_of_birthrequired | YYYY-MM-DD | Must be 18+ |
nationalityrequired | ISO-3166 alpha-2 | e.g. NG |
addressrequired | object | line1, line2, city, state, postal_code, country |
phone | E.164 | e.g. +2348012345678 |
external_id | string | Your own user ID, for reconciliation |
metadata | object | Up to 20 keys of your own data |
201{ "data": { "id": "ch_9LqT2", "status": "active", "kyc_status": "pending", "external_id": "user_8841", "cards_count": 0, "created_at": "2026-10-16T09:10:02Z" } }
/cardholderscardholders:readFilters: kyc_status (pending · approved · rejected), status, external_id, search (name or email).
/cardholders/{id}cardholders:readFull record, including KYC status, rejection reason and which products the cardholder is eligible for.
200{ "data": { "id": "ch_9LqT2", "kyc_status": "approved", "eligible_products": ["prd_visa_us_01"], "cards_count": 1 } }
/cardholders/{id}cardholders:writePartial update. After KYC approval, identity fields (name, date of birth, nationality) are locked and return 422 kyc_field_locked.
/cardholders/{id}/kyccardholders:writeStarts identity verification. When a hosted verification link is available, it is returned in verification_url — send it to the cardholder. The result arrives by webhook (cardholder.kyc_approved or cardholder.kyc_rejected).
200{ "data": { "id": "ch_9LqT2", "kyc_status": "pending", "verification_url": "https://verify.example/…" } }
/cardholders/{id}/suspendcardholders:write/cardholders/{id}/reactivatecardholders:writeSuspending freezes all the cardholder's cards; reactivating unfreezes the cards that the suspension froze. Optional body: {"reason": "…"}.
Cards
/cardscards:writeIssues a virtual card and loads it. The amount plus the issue fee are taken from your Gpaynow balance. Issuing is usually instant; the card starts as creating and becomes active — listen for card.issued.
| Field | Type | Description |
|---|---|---|
cardholder_idrequired | string | A cardholder with approved KYC |
product_idrequired | string | From GET /products |
amountrequired | number | Initial load in USD (within the product's limits) |
name_on_card | string | Defaults to the cardholder's name |
metadata | object | Your own data |
201{ "data": {
"id": "card_3KpX9", "cardholder_id": "ch_9LqT2", "product_id": "prd_visa_us_01",
"status": "creating", "network": "visa", "last4": "4821", "masked_number": "4xxx xxxx xxxx 4821",
"expiry": "10/29", "name_on_card": "AMINA OKAFOR", "balance": 50.00, "currency": "USD",
"billing_address": { "line1": "…", "city": "…", "country": "US" },
"fee_charged": 3.00, "created_at": "2026-10-16T09:12:44Z"
} }
/cardscards:readFilters: cardholder_id, status (creating · active · frozen · terminated).
/cards/{id}cards:readCard details with live balance, amount on hold and pending authorisations. The card number is always masked here.
200{ "data": { "id": "card_3KpX9", "status": "active", "balance": 48.20, "on_hold": 1.80, "pending_authorizations": 1, "last4": "4821" } }
/cards/{id}/sensitivecards:sensitiveReturns the full card number, CVV and expiry so your cardholder can pay online. Every call is logged.
200{ "data": { "id": "card_3KpX9", "number": "4…", "cvv": "…", "expiry_month": 10, "expiry_year": 2029 } }
/cards/{id}/fundcards:writeMoves money from your Gpaynow balance onto the card, free of charge. Body: {"amount": 25.00, "reference": "your-unique-ref"}. The reference is single-use.
200{ "data": { "card_id": "card_3KpX9", "amount": 25.00, "fee_charged": 0.00, "card_balance": 73.20, "reference": "topup-7781" } }
/cards/{id}/unloadcards:writeMoves money from the card back to your Gpaynow balance. Body: {"amount": 20.00, "reference": "…"}. Returns 402 insufficient_card_balance if the card can't cover it.
/cards/{id}/limitscards:writeSets spending limits in USD. At least one is required: daily, monthly, per_transaction.
body{ "daily": 200.00, "per_transaction": 50.00 }
/cards/{id}/freezecards:write/cards/{id}/unfreezecards:writeFreezing blocks new purchases instantly; the balance stays on the card. Unfreeze to resume.
/cards/{id}/terminatecards:writePermanently closes the card and returns its remaining balance to your Gpaynow balance. This can't be undone. Body must include {"confirm": "TERMINATE_CARD"}. Returns 409 card_has_pending_transactions until pending purchases settle (typically 24–72 hours).
/cards/{id}/transactionstransactions:readPurchases, declines, refunds, reversals and fees on the card.
200{ "data": [ {
"id": "txn_7Rk2", "type": "purchase", "status": "settled", "amount": 1.80, "currency": "USD",
"merchant": { "name": "OPENAI *CHATGPT", "category": "5734", "country": "US" },
"decline_reason": null, "created_at": "2026-10-16T10:01:12Z"
} ], "has_more": false }
status: pending · settled · declined · reversed.
/cards/{id}/statementtransactions:readGenerates a PDF statement for a date range (from, to). Returns a download link valid for 24 hours.
Webhooks
Webhooks push events to your server as they happen — card issued, transaction authorised, KYC approved and more. Your endpoint must use HTTPS and answer with any 2xx within 10 seconds. Failed deliveries are retried after 1 min, 10 min, 1 h, 6 h and 24 h; you can replay any delivery from the dashboard.
event{
"id": "evt_01HF8A…", "type": "transaction.authorized", "created_at": "2026-10-16T10:01:12Z", "environment": "live",
"data": { "card_id": "card_3KpX9", "transaction_id": "txn_7Rk2", "amount": 1.80, "merchant": { "name": "OPENAI *CHATGPT" } }
}
Events can arrive more than once or out of order — de-duplicate on id.
Manage endpoints
/webhookswebhooks:writeBody: {"url": "https://example.com/gpaynow/webhook", "events": ["card.*", "transaction.*"]}. The response contains the signing secret once — store it safely.
/webhookswebhooks:write/webhooks/{id}webhooks:write/webhooks/{id}webhooks:write/webhooks/{id}/testwebhooks:writeEvent types
| Event | When |
|---|---|
card.issued | Card is active and ready to use |
card.issue_failed | Issuing failed — the load and fee are refunded to your balance |
card.funded · card.fund_failed | Top-up completed / failed (failed top-ups are refunded) |
card.unloaded | Money moved back to your balance |
card.frozen · card.unfrozen | Card blocked / unblocked |
card.terminated | Card closed; refund amount included |
card.3ds_otp | One-time code for an online checkout — deliver it to the cardholder |
card.tokenization_otp | One-time code to add the card to Apple Pay / Google Pay |
transaction.authorized | Purchase approved (pending settlement) |
transaction.cleared | Purchase settled |
transaction.declined | Purchase declined, with decline_reason |
transaction.reversed | Authorisation reversed or refunded |
transaction.fee | A fee was charged on the card |
cardholder.kyc_approved · cardholder.kyc_rejected | Verification result |
cardholder.suspended · cardholder.reactivated | Cardholder status changed |
balance.low | Your Gpaynow balance fell below your alert threshold |
Verify signatures
Every delivery carries Gpaynow-Signature: t=<unix_seconds>,v1=<hex>. Compute HMAC-SHA256(secret, t + "." + raw_body), compare in constant time, and reject events older than 5 minutes. Always use the raw request body.
Node.jsimport 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));
}
PHPfunction 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');
Pythonimport 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"])
3-D Secure & wallet codes
When a merchant asks for 3-D Secure, the one-time code is sent to you in a card.3ds_otp event, not by SMS. Show it to the cardholder in your app or forward it by email/SMS within its validity (about 5 minutes). Adding a card to Apple Pay or Google Pay works the same way with card.tokenization_otp.
event{ "id": "evt_…", "type": "card.3ds_otp", "data": { "card_id": "card_3KpX9", "code": "482913", "expires_in_seconds": 300, "merchant": "NETFLIX.COM" } }
Compliance & acceptable use
- Business verification (KYB) is required before live keys are issued.
- Cardholder KYC is required for every cardholder in live mode. You must collect accurate data and may not create cardholders for people who haven't consented.
- Prohibited: gambling, sanctioned countries or persons, evading merchant or regional restrictions, ATM withdrawals, and any use banned by card-network rules.
- Liability: you are responsible for fraud, disputes and chargebacks on cards you issue, as set out in the Gpaynow API Terms.
- At launch, cards will be issued by a licensed card-issuing partner under licence from Visa or Mastercard (no issuer is connected yet). Gpaynow and its API clients are not card issuers and must not present themselves as such.
Changelog
| Version | Date | Notes |
|---|---|---|
2.0.0-preview | 2026-10-16 | Early-access preview of the v2 reference. Endpoints open to early-access members first. |
Be first to issue cards with the Gpaynow API
Tell us about your project. Our team reviews every request and contacts you with sandbox access and onboarding details before launch.
- No payment now; you choose a plan at launch
- Sandbox keys before the public launch
- Help from our team with integration and business verification
- Reply on WhatsApp, Telegram or email within 1–2 business days