API reference
Partner API
Two endpoints: one to check a code, one to consume it. Bearer auth. Sliding-window rate limits. Replay-safe with an Idempotency-Key.
Base URL
All endpoints live under https://paypaycards.com/api/v1. Requests and responses are JSON. HTTPS is enforced; HTTP calls are rejected.
Authentication
Authenticate every request with a bearer token (your API key). Keys are issued by PayPay.cards after your partner account is approved; create and rotate them in the dashboard. Only the first 8 characters and last 4 are visible after creation — store the full key in your secret manager at the moment of creation.
Authorization: Bearer pk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXRequests without a valid bearer return 401 unauthorized. Keys for suspended or revoked partners also return 401.
GET /v1/health
Public, unauthenticated. Use it for uptime monitors and deploy smoke tests.
curl https://paypaycards.com/api/v1/health{ "ok": true, "time": "2026-04-18T14:25:01.798Z" }POST /v1/validate
Check a code without consuming it. Use this when the user pastes the code but before you commit access on your side — for example, to show a preview of the tier they will unlock.
curl -X POST https://paypaycards.com/api/v1/validate \
-H "Authorization: Bearer $PAYPAY_KEY" \
-H "Content-Type: application/json" \
-d '{"code":"PP-XXXX-XXXX-XXXX"}'{
"valid": true,
"tier": "month",
"entitlement_label": "1 month",
"expires_at": "2026-04-25T14:25:01.798Z"
}{ "valid": false, "reason": "expired" }
// reason ∈ { "not_found" | "expired" | "already_redeemed" | "revoked" }Validate is idempotent and does not change the code's state. Every call is audited on the partner side under redemption_attempts.
POST /v1/redeem
Consume a code. Call this the moment you grant access on your side. A successful redeem is atomic — two concurrent redeems against the same code only succeed once. Subsequent calls return 409 already_redeemed.
curl -X POST https://paypaycards.com/api/v1/redeem \
-H "Authorization: Bearer $PAYPAY_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 01HZ9YW3M1X4GK3PEXAMPLE" \
-d '{"code":"PP-XXXX-XXXX-XXXX"}'{
"redeemed": true,
"tier": "month",
"entitlement_label": "1 month",
"redeemed_at": "2026-04-18T14:30:00.000Z",
"redemption_id": "b0f2c5e8-..."
}404 → { "redeemed": false, "reason": "not_found" }
410 → { "redeemed": false, "reason": "expired" }
423 → { "redeemed": false, "reason": "revoked" }
409 → { "redeemed": false, "reason": "already_redeemed", "redeemed_at": "..." }Errors
Every 4xx/5xx response follows the same envelope so you can log and alert uniformly.
{
"error": "short_code",
"message": "Human readable",
"request_id": "req_xxxxxxxxxxxxxxxxxx"
}Short codes you will encounter:
unauthorized— missing or invalid bearer (401)invalid_body— request body does not match the schema (400)rate_limited— you exceeded your rate limit (429)
Rate limits
Each API key is limited to 60 requests per minute, sliding window. Exceeding the limit returns 429 with a Retry-After header (seconds). Scaling to a higher limit requires a written request at partners@paypay.cards.
Idempotency
Supply an Idempotency-Key header (UUID or any high-entropy string) on every /v1/redeem call. A retry with the same key within 24 hours returns the cached response, avoiding double-counting a redemption when your network hiccups.
/v1/validate is read-only and does not require idempotency keys.
Quickstart (Node.js)
A minimal Express handler that reads a code submitted by the user, redeems it with PayPay.cards, and unlocks access only if the redeem succeeds.
import express from "express";
import { randomUUID } from "node:crypto";
const app = express();
app.use(express.json());
app.post("/redeem", async (req, res) => {
const { code, userId } = req.body;
const r = await fetch("https://paypaycards.com/api/v1/redeem", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PAYPAY_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": randomUUID(),
},
body: JSON.stringify({ code }),
});
const payload = await r.json();
if (!r.ok || !payload.redeemed) {
return res.status(400).json({ error: payload.reason ?? "invalid_code" });
}
// 👇 grant access on your side, keyed by tier
await grantAccess(userId, payload.tier);
res.json({ ok: true, tier: payload.tier });
});Questions?
Email partners@paypay.cards. Not yet a partner? Apply here.