Fake Pay

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

CurrencyDecimalsMinimumExample
aud2501050 = AUD 10.50
usd2501050 = USD 10.50
aed22001050 = AED 10.50
inr21001050 = 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" } }
400invalid_request_error — bad or missing parameter, wrong state
401authentication_error — missing or invalid key
402card_error — declined; includes decline_code, payment_method and the payment_intent object
404resource_missing
409idempotency_error (request with the same key still running) or a concurrent state change
429rate_limit_error — back off and retry
5xxapi_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, currencyrequired
success_url, cancel_urlrequired; https (http allowed in test mode). {CHECKOUT_SESSION_ID} is replaced.
description, metadataoptional; metadata up to 20 string keys
customer / customer_emailoptional
setup_future_usageoff_session saves the card to the customer (one is created if none is given) for later off-session charges
expires_atunix 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.

NumberBehaviour
4242424242424242Succeeds
5555555555554444Succeeds (Mastercard)
4000000000000002Declined (generic_decline)
4000000000003184Requires 3-D Secure; saved, off-session charges need authentication
4000000000009995Declined (insufficient_funds)
4000000000000069Declined (expired_card)
4000000000000341Succeeds and saves; later off-session charges fail with insufficient_funds
4000000000000259Succeeds 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_methodcreated, or the last attempt failed (last_payment_error)
requires_action3-D Secure needed: send the customer to next_action.redirect_to_url.url
processingauthorised, capture landing (live: ~3 s)
succeededcaptured; amount_received set
canceledcanceled 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 cardOff-session result
4242424242424242200, succeeded
4000000000003184402 authentication_required; the PaymentIntent is requires_action — bring the customer back to next_action to authenticate
4000000000000341402 card_declined / insufficient_funds
4000000000000259402 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 sfirst delivery attempt waits N seconds
Duplicate webhooksevery event delivered twice
Drop first webhookfirst attempt never sent; arrives on the 10 s retry
Late capture after session expirycheckout payment processes ~25 s; the session expires first; then checkout.session.async_payment_succeeded
Duplicate capturethe completed session is captured again on a second PaymentIntent
Refund API 503 for first N callsPOST /v1/refunds fails before doing anything; retry with the same key
Refund failed, then succeededrefund.failed then refund.updated (succeeded) ~30 s later
Mismatched refund idrefund webhooks carry a different id from the API object
Slow API N msevery /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.