Fake Pay API
Base URL https://fakepay.tringify.store/v1 · API version 2026-10-01
Overview
Fake Pay is a demo payment company with a Stripe-like REST API at https://fakepay.tringify.store/v1. Nothing is ever charged and no money moves. Every account has two environments, test and live, each with its own API keys, webhook endpoints and data. Keys decide the mode: sk_test_… or sk_live_….
- Test — only Fake Pay test cards work; chaos switches (dashboard) inject faults; payments settle instantly.
- Live — no chaos; any Luhn-valid card number with a future expiry works and test card numbers are rejected with test_card_in_live_mode; payments go through processing for ~3 s and refunds settle in ~20 s, like a real acquirer.
Requests take JSON (Content-Type: application/json) or form bodies (metadata[order_id]=42). Responses are JSON. Amounts are integers in the currency's minor unit.
Authentication
Send a secret key as a bearer token (basic auth with the key as the username also works). Publishable keys (pk_…) are only for the express wallet script.
curl https://fakepay.tringify.store/v1/account -H "Authorization: Bearer sk_test_..."
Sign up at /signup; your first test and live secret keys are shown once. Create or revoke keys in the dashboard under API keys.
Currencies
| Currency | Decimals | Minimum | Example |
|---|---|---|---|
| aud | 2 | 50 | 1050 = AUD 10.50 |
| usd | 2 | 50 | 1050 = USD 10.50 |
| aed | 2 | 200 | 1050 = AED 10.50 |
| inr | 2 | 100 | 1050 = INR 10.50 |
Maximum amount: 99999999.
Errors
Errors use HTTP status codes and a JSON body:
{ "error": { "type": "invalid_request_error", "code": "parameter_missing", "message": "Missing required param: currency.", "param": "currency" } }
| 400 | invalid_request_error — bad or missing parameter, wrong state |
| 401 | authentication_error — missing or invalid key |
| 402 | card_error — declined; includes decline_code, payment_method and the payment_intent object |
| 404 | resource_missing |
| 409 | idempotency_error (request with the same key still running) or a concurrent state change |
| 429 | rate_limit_error — back off and retry |
| 5xx | api_error — safe to retry with the same Idempotency-Key |
Idempotency
Every POST accepts an Idempotency-Key header (up to 255 characters). The first response (2xx or 4xx) is stored for 24 hours and replayed for retries with the same key and the same body, marked Idempotent-Replayed: true. Reusing a key with a different body returns 400 idempotency_key_reused. 5xx responses are not stored, so retrying them runs the request again.
Lists & pagination
List endpoints return {"object":"list","data":[…],"has_more":true}, newest first. Use limit (1–100, default 10) and starting_after=<last id>.
Checkout Sessions
Create a session on your server, redirect the shopper to url, and fulfil the order on the checkout.session.completed webhook (then retrieve the session to verify).
curl https://fakepay.tringify.store/v1/checkout/sessions \
-H "Authorization: Bearer sk_test_..." -H "Idempotency-Key: order-1001" \
-H "Content-Type: application/json" -d '{
"amount": 2599, "currency": "aud",
"success_url": "https://shop.example/thanks?session={CHECKOUT_SESSION_ID}",
"cancel_url": "https://shop.example/cart",
"description": "Order #1001", "metadata": {"order_id": "1001"},
"customer_email": "sam@example.com"
}'
| amount, currency | required |
| success_url, cancel_url | required; https (http allowed in test mode). {CHECKOUT_SESSION_ID} is replaced. |
| description, metadata | optional; metadata up to 20 string keys |
| customer / customer_email | optional |
| setup_future_usage | off_session saves the card to the customer (one is created if none is given) for later off-session charges |
| expires_at | unix seconds, 30 min–24 h ahead (test mode: from 1 min). Default 24 h. |
Session fields: status open | complete | expired, payment_status unpaid | paid, payment_intent, url.
GET /v1/checkout/sessions/{id} · GET /v1/checkout/sessions?status=&payment_intent= · POST /v1/checkout/sessions/{id}/expire (open sessions only; cancels its unfinished PaymentIntent and sends checkout.session.expired).
Test cards
Use any future expiry and any CVC in test mode.
| Number | Behaviour |
|---|---|
| 4242424242424242 | Succeeds |
| 5555555555554444 | Succeeds (Mastercard) |
| 4000000000000002 | Declined (generic_decline) |
| 4000000000003184 | Requires 3-D Secure; saved, off-session charges need authentication |
| 4000000000009995 | Declined (insufficient_funds) |
| 4000000000000069 | Declined (expired_card) |
| 4000000000000341 | Succeeds and saves; later off-session charges fail with insufficient_funds |
| 4000000000000259 | Succeeds and saves; later off-session charges fail: the card was revoked |
3-D Secure cards send the shopper to a fake bank challenge page where they can complete or fail authentication. Live mode rejects all of these numbers.
PaymentIntents
Every payment is a PaymentIntent. Capture is automatic.
| requires_payment_method | created, or the last attempt failed (last_payment_error) |
| requires_action | 3-D Secure needed: send the customer to next_action.redirect_to_url.url |
| processing | authorised, capture landing (live: ~3 s) |
| succeeded | captured; amount_received set |
| canceled | canceled via API or with an expired session |
POST /v1/payment_intents — amount, currency, customer, description, metadata, setup_future_usage, and with confirm=true: payment_method (a saved card) or confirmation_token (wallet), off_session, return_url.
POST /v1/payment_intents/{id}/confirm · POST /v1/payment_intents/{id}/cancel (cancellation_reason) · GET /v1/payment_intents/{id} · GET /v1/payment_intents?customer=
Refunds
Full or partial; several partial refunds may add up to the captured amount. Refunds start pending, then become succeeded (test ~3 s, live ~20 s) or failed. Always send an Idempotency-Key.
curl https://fakepay.tringify.store/v1/refunds -H "Authorization: Bearer sk_test_..." -H "Idempotency-Key: refund-1001-1" \ -d payment_intent=pi_... -d amount=500 -d reason=requested_by_customer
GET /v1/refunds/{id} · GET /v1/refunds?payment_intent=. The PaymentIntent's amount_refunded grows as refunds succeed. Events: refund.created, refund.updated, refund.failed, payment_intent.refunded.
Customers & saved cards
POST /v1/customers (email, name, description, metadata) · GET/POST /v1/customers/{id} · DELETE /v1/customers/{id} · GET /v1/customers?email=
Cards are saved by checkouts with setup_future_usage=off_session (event payment_method.attached). List and detach them:
GET /v1/customers/{id}/payment_methods · GET /v1/payment_methods/{id} · POST /v1/payment_methods/{id}/detach
Off-session charges
curl https://fakepay.tringify.store/v1/payment_intents -H "Authorization: Bearer sk_test_..." -H "Idempotency-Key: renewal-77" \ -d amount=1500 -d currency=usd -d customer=cus_... -d payment_method=pm_... \ -d confirm=true -d off_session=true
| Saved test card | Off-session result |
|---|---|
| 4242424242424242 | 200, succeeded |
| 4000000000003184 | 402 authentication_required; the PaymentIntent is requires_action — bring the customer back to next_action to authenticate |
| 4000000000000341 | 402 card_declined / insufficient_funds |
| 4000000000000259 | 402 payment_method_revoked; the card is marked revoked and cannot be charged again |
Express wallet
Fake Pay buttons in Apple Pay and Google Pay style, built on the browser Payment Request API (Chrome and Edge, desktop and Android, over https). The wallet installs itself on first use. Try it in the dashboard under Express wallet.
<script src="https://fakepay.tringify.store/js/express.js"></script>
<div id="express"></div>
<script>
const ex = FakePay("pk_test_...").expressCheckout({
container: "#express", amount: 5250, currency: "usd", label: "My store",
displayItems: [{ label: "Goods", amount: 5250 }],
requestShipping: true, requestPayerEmail: true,
shippingOptions: [{ id: "std", label: "Standard", amount: 1000, selected: true }],
buttons: ["apple", "google"]
});
// Fired by the wallet sheet. Return the new totals (minor units).
ex.on("shippingaddresschange", async ({ shippingAddress }) =>
({ amount: 6250, displayItems: [...], shippingOptions: [...] })); // or { error: "We don't ship there" }
ex.on("shippingoptionchange", async ({ shippingOption }) => ({ amount: 7750 }));
ex.on("confirm", async ({ confirmationToken, amount, currency, shippingAddress, payerEmail }) => {
const r = await fetch("/checkout/fakepay-confirm", { method: "POST", body: JSON.stringify({ confirmationToken }) });
return r.ok; // false or a throw shows failure in the sheet
});
ex.on("cancel", () => {}); ex.on("unavailable", ({ reason }) => {});
</script>
On your server, confirm the token at the exact amount the shopper approved (a different amount or currency fails with amount_mismatch; tokens are single-use and expire after 10 minutes):
curl https://fakepay.tringify.store/v1/payment_intents -H "Authorization: Bearer sk_test_..." -H "Idempotency-Key: order-1002" \ -d amount=7750 -d currency=usd -d confirmation_token=ctoken_... -d confirm=true
GET /v1/confirmation_tokens/{id} shows the approved amount, card preview, payer and shipping address.
Events
Every state change creates an event, kept in the event log. Retrieve events to verify webhooks: GET /v1/events/{id} · GET /v1/events?type=refund.*&object_id=
Types: checkout.session.completed, checkout.session.expired, checkout.session.async_payment_succeeded, payment_intent.created, payment_intent.requires_action, payment_intent.processing, payment_intent.succeeded, payment_intent.payment_failed, payment_intent.canceled, payment_intent.refunded, refund.created, refund.updated, refund.failed, customer.created, customer.updated, customer.deleted, payment_method.attached, payment_method.detached, payout.created, payout.paid.
Webhooks
Add endpoints in the dashboard (or POST /v1/webhook_endpoints with url and enabled_events; the response includes the secret once). Each delivery is a POST with the event JSON and these headers:
FakePay-Signature: t=1792800000,v1=5f2b… (HMAC-SHA256 of "<t>.<raw body>" with the endpoint's whsec_ secret, hex) FakePay-Event-Id: evt_… FakePay-Delivery-Id: whd_…
// Node.js verification
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const t = /t=(\d+)/.exec(header)?.[1];
const sigs = [...header.matchAll(/v1=([0-9a-f]{64})/g)].map((m) => m[1]);
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return sigs.some((s) => crypto.timingSafeEqual(Buffer.from(s), Buffer.from(want)));
}
Respond 2xx within 10 s. Other responses, timeouts and network errors are retried after 10 s, 1 min, 5 min, 30 min, 2 h, 6 h, 12 h; then the delivery is parked. At most 3 deliveries to one endpoint run at a time. After 20 consecutive failed attempts the endpoint is disabled (its pending deliveries park) until you enable it again in the dashboard. The dashboard shows every attempt and can resend any event. Events, deliveries and attempts are kept for 30 days. Events can arrive more than once and out of order: deduplicate on the event id and re-read objects with the API.
No server yet? The dashboard's built-in test receiver accepts deliveries and checks their signatures.
Payouts
Every day at 00:30 UTC Fake Pay settles the previous UTC day: one payout per currency of captured payments − succeeded refunds − fees (2.9% + 30 minor units; INR 2%). Test payouts are paid at once; live payouts arrive two days later. GET /v1/payouts?currency= · GET /v1/payouts/{id} · events payout.created, payout.paid.
Chaos switches (test mode)
Per-account switches in the dashboard. Objects they touch carry a fakepay_chaos field.
| Delay webhooks N s | first delivery attempt waits N seconds |
| Duplicate webhooks | every event delivered twice |
| Drop first webhook | first attempt never sent; arrives on the 10 s retry |
| Late capture after session expiry | checkout payment processes ~25 s; the session expires first; then checkout.session.async_payment_succeeded |
| Duplicate capture | the completed session is captured again on a second PaymentIntent |
| Refund API 503 for first N calls | POST /v1/refunds fails before doing anything; retry with the same key |
| Refund failed, then succeeded | refund.failed then refund.updated (succeeded) ~30 s later |
| Mismatched refund id | refund webhooks carry a different id from the API object |
| Slow API N ms | every /v1 call waits N ms |
Rate limits
About 100 API requests per 10 s per client IP; checkout, wallet and authentication pages have lower limits. Over the limit you get 429.