Developer
API documentation
Everything you need to integrate: authentication, scopes, the four endpoints, errors, rate limits and signed webhooks.
Overview
The PointsStation API lets you load points, redeem points into prepaid game pockets, reset game-account passwords, read balances, create payment links/gateways (and check their payments), and request payouts — programmatically. All endpoints live under one base URL and return a consistent JSON envelope.
Base URL https://pointsstation.com/api/v1
Auth Authorization: Bearer psk_live_… (or x-api-key: psk_live_…)
Format JSON in, JSON out — every response is { "ok": true, "data": … } or { "ok": false, "error": … }Create keys and webhook endpoints in the dashboard under API & Webhooks. Prepaid pockets are managed under Game Points. A machine-readable openapi.json is linked above (import it into Postman/Insomnia or generate a client).
Authentication
Every request authenticates with an API key. A key acts as its owning account — loads debit that account's wallet or pockets and are attributed to it. Keys look like psk_live_… and are shown in full only once at creation (we store only a hash). Send the key either way:
# Preferred — bearer token
curl https://pointsstation.com/api/v1/balance -H "Authorization: Bearer psk_live_xxxxxxxx"
# Or the x-api-key header
curl https://pointsstation.com/api/v1/balance -H "x-api-key: psk_live_xxxxxxxx"A missing/invalid/revoked key → 401 UNAUTHORIZED. If the owning account isn't active → 403 FORBIDDEN.
Scopes
Each key carries a set of scopes; a call to an endpoint whose scope the key lacks returns 403 SCOPE_REQUIRED. Agent keys may hold load, reset, payments and payouts; merchant and owner keys may hold all of them.
| Scope | Grants | Agent keys |
|---|---|---|
| load | Create loads (push points to a player) | allowed |
| redeem | Redeem points from a player account into a pocket | merchant/owner only |
| reset | Reset a game-account password | allowed |
| balance | Read game-account / pocket balances | merchant/owner only |
| verify | Verify a player exists | merchant/owner only |
| payments | Create payment links/gateways & check payment status | allowed |
| payouts | Request payouts (cash-outs) & check their status | allowed |
| accounts | Create game-panel accounts (single or bulk) | allowed |
Errors
Failures return a non-2xx status and { "ok": false, "error": { "code", "message", "details?" } }. Branch on the stablecode, not the message.
A panel rejection is not an HTTP error. On POST /load a declined load returns 200 withdata.status = "FAILED" (auto-refunded). Wallet loads may also return "PENDING_REVIEW" when the provider status is unconfirmed (pocket loads treat that as FAILED and refund). Always inspect data.status.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Body failed schema validation (details = field errors). |
| 400 | BAD_REQUEST | Malformed JSON or a non-positive amount. |
| 401 | UNAUTHORIZED | Missing, invalid or revoked API key. |
| 403 | FORBIDDEN | Owning account is not active. |
| 403 | SCOPE_REQUIRED | The key lacks the scope this endpoint needs. |
| 403 | GAME_NOT_ALLOWED | Agent key restricted to an allow-list that excludes this game. |
| 404 | PLAYER_NOT_FOUND | The player doesn't exist at the provider. |
| 404 | ACCOUNT_NOT_FOUND | No game account with that username under your account (reset). |
| 404 | GAME_NOT_FOUND | Unknown providerKey. |
| 422 | INSUFFICIENT_FUNDS | Wallet balance below the charge (wallet loads). |
| 422 | INSUFFICIENT_POINTS | Not enough prepaid pocket points (pocket loads). |
| 422 | OVER_SINGLE_LIMIT / OVER_DAILY_LIMIT | Load exceeds your cap (wallet loads). |
| 422 | WALLET_FROZEN / NO_WALLET / PLAYER_BLOCKED | Account or player state blocks the load. |
| 422 | PANEL_NOT_LIVE / UNSUPPORTED / PANEL_REJECTED | The game panel can't or won't process it. |
| 429 | RATE_LIMITED | Too many requests — message includes seconds to wait. |
| 503 | MAINTENANCE | Platform maintenance (wallet loads only). |
| 500 | INTERNAL | Unexpected server error. |
Rate limits
Limits are per owning account, fixed-window. Exceeding one returns 429 RATE_LIMITED with a message telling you when to retry (there are no X-RateLimit-* headers).
POST /load 120 requests / minute
POST /redeem 120 requests / minute
GET /balance 120 requests / minute
POST /reset 60 requests / minute
POST /payment-link 60 requests / minute
POST /payout 20 requests / minuteThe read-only checks (GET /payment-link/{code}, GET /payout/{code}) have no per-route limit — poll them at a sensible interval.
Idempotency
POST /load requires an idempotencyKey (8–80 chars). Replaying the same key returns the original transaction with no second debit — safe to retry on a network error. Use a fresh key per intended load (e.g. your order id).
POST /payment-link accepts an optional idempotencyKey (8–200 chars): a retried create with the same key returns the original link instead of minting a duplicate.
POST /redeem requires an idempotencyKey (8–80 chars; body field or Idempotency-Key header) and is idempotent — replaying the same key returns the original result, never a second redeem. POST /reset is not idempotent — each call makes a new password; retry it only after confirming the first didn't succeed.
Prepaid game pockets
A pocket is a prepaid points reservoir for one game. Buy points up front (in the dashboard), then load them onto any username of that same game with POST /load + source: "pocket" — no wallet charge. Points youredeem from a player land back in that game's pocket. Pocket points are game-locked and never convert to wallet cash. Check balances with GET /balance.
GET /api/v1/games
/api/v1/gamesDiscover the providerKey values /load accepts, each with a display name, whether the panel is live (connected), and this account's list price in ¢/point (so you can convert dollars → points). Only load games where live is true.
Example request
curl https://pointsstation.com/api/v1/games -H "Authorization: Bearer psk_live_xxxxxxxx"Example response
{
"ok": true,
"data": {
"games": [
{
"key": "game-vault",
"name": "Game Vault",
"category": "Fish",
"live": true,
"ratePerPointCents": 1,
"pointsPerDollar": 100
},
{
"key": "juwa",
"name": "Juwa",
"category": "Slots",
"live": true,
"ratePerPointCents": 1,
"pointsPerDollar": 100
}
]
}
}Errors
POST /api/v1/account
/api/v1/accountCreate one account on a game's panel. You pick the username; we generate the password and return it once. /load later requires the username to exist — create it here first.
Request body
| Field | Type | Req | Notes |
|---|---|---|---|
| providerKey | string | ● | Game key from GET /games (e.g. game-vault). |
| username | string (4–20) | ● | Letters/numbers only — the player's game username. |
| note | string | ○ | Optional internal note. |
Example request
curl -X POST https://pointsstation.com/api/v1/account \
-H "Authorization: Bearer psk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "providerKey": "game-vault", "username": "luckydragon88" }'Example response
{
"ok": true,
"data": {
"username": "luckydragon88",
"password": "Kp7mQ9xR2tvW",
"providerKey": "game-vault",
"status": "ACTIVE"
}
}Errors
POST /api/v1/account/bulk
/api/v1/account/bulkCreate up to 50 accounts on ONE game in a single call. Sequential; one bad username never fails the batch — each result has its generated password (once) or an error.
Request body
| Field | Type | Req | Notes |
|---|---|---|---|
| providerKey | string | ● | Game key from GET /games. |
| usernames | string[] (1–50) | ● | Letters/numbers only, 4–20 chars each. |
Example request
curl -X POST https://pointsstation.com/api/v1/account/bulk \
-H "Authorization: Bearer psk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "providerKey": "game-vault", "usernames": ["luckydragon88", "storealpha2"] }'Example response
{
"ok": true,
"data": {
"created": 2,
"failed": 0,
"results": [
{ "username": "luckydragon88", "ok": true, "password": "Kp7mQ9xR2tvW" },
{ "username": "storealpha2", "ok": true, "password": "Bn4kT8wL5qzX" }
]
}
}Errors
POST /api/v1/load
/api/v1/loadPush game points to a player. Funds from wallet cash (default) or a prepaid pocket. Returns the transaction — inspect data.status.
Request body
| Field | Type | Req | Notes |
|---|---|---|---|
| playerIdentifier | string (2–64) | ● | Player's game username. |
| points | integer (≥1) | ● | Points to load. |
| providerKey | string | ● | Game/provider key, e.g. game-vault. |
| idempotencyKey | string (8–80) | ● | Replay-safe key (e.g. your order id). |
| source | "wallet" | "pocket" | ○ | Default "wallet". "pocket" spends prepaid points. |
| gameName | string | ○ | Optional display name. |
Example request
curl -X POST https://pointsstation.com/api/v1/load \
-H "Authorization: Bearer psk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"playerIdentifier": "luckydragon88",
"points": 500,
"providerKey": "game-vault",
"idempotencyKey": "ord_2026_0001"
}'Example response
{
"ok": true,
"data": {
"reference": "VPL-7K2M9QXR4T",
"status": "COMPLETED",
"amountCents": 2500,
"points": 500,
"fundingSource": "wallet",
"playerUsername": "luckydragon88",
"provider": "Game Vault",
"providerTxnId": "PX-558210",
"failureReason": null,
"createdAt": "2026-06-19T14:03:21.114Z",
"completedAt": "2026-06-19T14:03:22.880Z"
}
}Errors
POST /api/v1/redeem
/api/v1/redeemPull points out of a player's game account and credit them to your prepaid pocket for that game (never to wallet cash). Requires an idempotencyKey and is idempotent (same key → original result).
Request body
| Field | Type | Req | Notes |
|---|---|---|---|
| providerKey | string | ● | Game/provider key. |
| account | string (2–64) | ● | Player username to redeem from. |
| points | integer (≥1) | ● | Points to redeem. |
| idempotencyKey | string (8–80) | ● | Required (body or Idempotency-Key header). Same key → original result, never a 2nd redeem. |
Example request
curl -X POST https://pointsstation.com/api/v1/redeem \
-H "Authorization: Bearer psk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "providerKey": "game-vault", "account": "luckydragon88", "points": 250, "idempotencyKey": "rdm_2026_0001" }'Example response
{
"ok": true,
"data": {
"reference": "VPR-7KQ2M9X4PB",
"providerKey": "game-vault",
"providerName": "Game Vault",
"pointsRedeemed": 250,
"pocketBalance": 12500,
"message": "Redeem successful"
}
}Errors
POST /api/v1/reset
/api/v1/resetRegenerate the password for one of your own game accounts. Returns the new password exactly once.
Request body
| Field | Type | Req | Notes |
|---|---|---|---|
| providerKey | string | ● | Game/provider key. |
| account | string (2–64) | ● | Your existing game-account username. |
Example request
curl -X POST https://pointsstation.com/api/v1/reset \
-H "Authorization: Bearer psk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "providerKey": "game-vault", "account": "store_alpha" }'Example response
{
"ok": true,
"data": {
"username": "store_alpha",
"password": "Kp7mQ9xR2tvW",
"status": "ACTIVE"
}
}Errors
GET /api/v1/balance
/api/v1/balanceWith ?account= & ?providerKey= → that game account's live panel balance. Otherwise → your prepaid pocket balances per game.
Example request
# Pocket balances (all games)
curl https://pointsstation.com/api/v1/balance -H "Authorization: Bearer psk_live_xxxxxxxx"
# One account's live panel balance
curl "https://pointsstation.com/api/v1/balance?providerKey=game-vault&account=store_alpha" \
-H "Authorization: Bearer psk_live_xxxxxxxx"Example response
{
"ok": true,
"data": {
"pockets": [
{
"id": "ckpocket123",
"providerKey": "game-vault",
"providerName": "Game Vault",
"logoUrl": "/logos/game-vault.png",
"category": "Slots",
"pointsBalance": 4200,
"updatedAt": "2026-06-19T14:03:22.118Z"
}
]
}
}Errors
POST /api/v1/payment-link
/api/v1/payment-linkCreate a shareable payment link (single amount) or a gateway (a list of amounts the payer picks from). When a customer pays, your wallet is credited net of fees and a payment.received webhook fires.
Request body
| Field | Type | Req | Notes |
|---|---|---|---|
| amount | number ($) | ○ | Fixed-amount link, in dollars (use this OR amounts). |
| amounts | number[] ($) | ○ | Gateway: the dollar amounts the payer chooses from. |
| methods | string[] | ● | cashapp|applepay|googlepay|lightning|crypto. Card methods need a preset amount. |
| allowCustomAmount | boolean | ○ | Gateway only: let the payer enter their own amount (Lightning/Crypto). |
| code | string | ○ | Vanity code for the URL (3–32 lowercase letters/numbers/hyphens). |
| idempotencyKey | string (8–200) | ○ | Retry-safe: the same key returns the original link, never a duplicate. |
| title / note | string | ○ | Shown to the payer. |
| expiresInDays | integer (1–365) | ○ | Optional expiry. |
Example request
curl -X POST https://pointsstation.com/api/v1/payment-link \
-H "Authorization: Bearer psk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"amounts": [9.99, 19.99, 49.99],
"methods": ["cashapp", "lightning", "crypto"],
"allowCustomAmount": true,
"code": "my-store",
"idempotencyKey": "gw_2026_0001"
}'Example response
{
"ok": true,
"data": {
"id": "cmq…",
"reference": "my-store",
"payUrl": "https://pointsstation.com/pay/my-store",
"amountCents": 0,
"amountsCents": [999, 1999, 4999],
"methods": ["cashapp", "lightning", "crypto"],
"status": "ACTIVE"
}
}Errors
GET /api/v1/payment-link/{code}
/api/v1/payment-link/{code}Look up one of your own links/gateways by its code to see its status, collected totals, and recent payments (each with status — COMPLETED = received).
Example request
curl https://pointsstation.com/api/v1/payment-link/my-store \
-H "Authorization: Bearer psk_live_xxxxxxxx"Example response
{
"ok": true,
"data": {
"reference": "my-store",
"status": "ACTIVE",
"amountsCents": [999, 1999, 4999],
"methods": ["cashapp", "lightning", "crypto"],
"paidCount": 3,
"paidCents": 8997,
"payable": true,
"payments": [
{ "id": "cmq…", "status": "COMPLETED", "method": "btc",
"grossCents": 4999, "netCents": 4239,
"createdAt": "2026-06-23T14:30:00.000Z",
"creditedAt": "2026-06-23T14:31:12.000Z" }
]
}
}Errors
POST /api/v1/payout
/api/v1/payoutRequest a cash-out from your wallet. Funds are reserved immediately and the request enters the owner's review queue as PENDING — it is NOT auto-paid. A flat payout fee may apply; the recipient receives amount − fee. payout.paid / payout.rejected fires when the owner acts.
Request body
| Field | Type | Req | Notes |
|---|---|---|---|
| amount | number ($) | ● | Gross cash-out in dollars (must exceed the payout fee). |
| method | "cashapp" | "chime" | "crypto" | "bank" | "other" | ○ | Default "cashapp". |
| destination | string (4–300) | ● | Where to send the funds (e.g. $cashtag, address). |
| note | string | ○ | Optional note. |
Example request
curl -X POST https://pointsstation.com/api/v1/payout \
-H "Authorization: Bearer psk_live_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "amount": 100, "method": "cashapp", "destination": "$mystore", "note": "weekly payout" }'Example response
{
"ok": true,
"data": {
"reference": "VPW-7K2M9QXR4T",
"amountCents": 10000,
"feeCents": 1000,
"netCents": 9000,
"method": "cashapp",
"destination": "$mystore",
"status": "PENDING",
"createdAt": "2026-06-23T18:04:11.425Z"
}
}Errors
GET /api/v1/payout/{code}
/api/v1/payout/{code}Look up one of your own payouts by its reference to see its current status (PENDING / APPROVED / PAID / REJECTED / CANCELLED), amount, fee and net.
Example request
curl https://pointsstation.com/api/v1/payout/VPW-7K2M9QXR4T \
-H "Authorization: Bearer psk_live_xxxxxxxx"Example response
{
"ok": true,
"data": {
"reference": "VPW-7K2M9QXR4T",
"status": "PAID",
"amountCents": 10000,
"feeCents": 1000,
"netCents": 9000,
"method": "cashapp",
"reviewedAt": "2026-06-23T19:10:02.000Z"
}
}Errors
Webhooks
Register HTTPS endpoints under API & Webhooks to receive events. We POST { event, data, sentAt } with headersx-ps-event and x-ps-signature: sha256=…. Delivery is best-effort with a 10s timeout and no retries — acknowledge with any 2xx. Agent endpoints receive load, reset, payment and payout events; redeem.completed goes to merchant/owner only.
| Event | When | Agent keys |
|---|---|---|
| load.completed | A load completed successfully. | yes |
| load.failed | A load failed (and was refunded). | yes |
| load.pending_review | A load needs manual review. | yes |
| reset.completed | A game-account password was reset. | yes |
| redeem.completed | Points were redeemed into a pocket. | no |
| payment.received | A customer paid one of your links/gateways. | yes |
| payout.paid | A payout (cash-out) was marked paid. | yes |
| payout.rejected | A payout was rejected (funds refunded). | yes |
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
const SECRET = process.env.PS_WEBHOOK_SECRET; // "whsec_…" shown once at creation
// Capture the RAW body so the signature matches byte-for-byte.
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
app.post("/webhooks/pointsstation", (req, res) => {
const sig = req.header("x-ps-signature") || "";
const expected = "sha256=" + createHmac("sha256", SECRET).update(req.rawBody).digest("hex");
const a = Buffer.from(sig), b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).end();
const { event, data, sentAt } = req.body; // verified
// handle event…
res.sendStatus(200); // any 2xx marks delivery successful
});