Simulation mode FauxPay never moves real money. No card data, no bank accounts, no exceptions.
FauxPay
Menu

Build the whole checkout.
Before you pick a gateway.

FauxPay is a payment gateway simulator: one signed API, a hosted checkout page, payouts and webhooks that retry, across 10 currencies and their local channels. Build and test the whole order flow against it while you are still choosing a gateway, or across markets no single sandbox covers.

Every response carries "mode": "simulation", and nothing behind it is connected to a bank, a card network or a wallet.

POST /api/v1/payment-intents 201

Indonesian Rupiah

amount: 1000 = Rp1,000

8 channels, each issued at creation:

  • QRIS QR code
  • GoPay E-wallet
  • DANA E-wallet
  • ShopeePay E-wallet
  • BCA Virtual Account Virtual account
  • BNI Virtual Account Virtual account
  • Mandiri Virtual Account Virtual account
  • BCA Bank Transfer Bank transfer

Bank and wallet names identify the payment method being simulated. FauxPay is not affiliated with or endorsed by any of them, and never connects to them.

One integer.
10 different amounts.

Amounts are sent in the smallest unit the currency has. Here is amount: 1000 in each market.

  • IDR Indonesian Rupiah

    Rp1,000

  • INR Indian Rupee

    ₹10.00

  • MYR Malaysian Ringgit

    RM10.00

  • VND Vietnamese Dong

    ₫1,000

  • MMK Myanmar Kyat

    K1,000

  • THB Thai Baht

    ฿10.00

  • JPY Japanese Yen

    ¥1,000

  • USDT Tether (simulated)

    ₮0.001000

  • PHP Philippine Peso

    ₱10.00

  • PKR Pakistani Rupee

    ₨10.00

One integration shape, before the gateway is decided.

Every major gateway gives you a free test mode, and once you have picked one, use it: it behaves exactly like that gateway's production. FauxPay is for the stretch before that, and for the work no single test mode covers: several markets at once, payouts, and outcomes your CI can force.

Testing gateway by gateway

  1. A different sandbox, API and dashboard for every gateway and market
  2. A simulator page to click, or a per-product simulate call, to finish each payment
  3. Payouts and settlement tested separately, if the test mode has them at all
  4. A rewrite of the payment layer when the gateway choice changes

Building against FauxPay

  1. Register and get an API key pair straight away
  2. Create a payment intent and get a checkout URL back
  3. Send your tester there, pick an outcome, read the webhook
  4. Port to the real gateway later with a field-by-field guide; your order states stay the same

What it is not

Not a licensed payment provider. Not connected to any bank, card or e-wallet network. Not a way to accept money from real customers, and not something to fake a real transaction with.

Built so it cannot touch money.

These are properties of the codebase, not policy promises. There is no processor integration to misconfigure, because none exists.

  1. 01

    No processor integration

    No Stripe, Midtrans, Xendit or bank API keys anywhere in the codebase.

  2. 02

    No card fields

    FauxPay never asks for a PAN, CVV or expiry, not even in test fixtures.

  3. 03

    Destinations that cannot be paid

    QR payloads are not EMVCo, the UPI handle sits on a PSP that does not exist, and the crypto addresses fail their checksum. A tester who copies one cannot send real money to a stranger.

  4. 04

    Marked everywhere

    Every API response carries "mode": "simulation", and every checkout page carries the striped banner.

FauxPay, or your gateway's own test mode?

Use FauxPay when

  • You have not chosen a gateway yet, or the choice may change.
  • You sell in several markets and want one API across their local channels.
  • Your CI has to force success, failure, delay or expiry and test webhook retries.
  • You need to test payouts against a held balance, with each rail's own routing.
  • An AI agent is writing the integration and needs a contract it can read.

Use your gateway's test mode when

  • The gateway is chosen: its test mode matches its production exactly.
  • You need its exact error codes, edge cases and dashboard.
  • You are running its certification or go-live checks.
  • You take cards: FauxPay has no card flow and never handles card data.

Money out

Payouts that behave like payouts.

Most test modes stop at the payment. The withdrawal side is where the double-spend and the half-finished reversal live, so FauxPay simulates it with the same ledger as the deposits.

  1. 01

    Held the moment it is accepted

    The debit lands on accept, so the same balance cannot be paid out twice while the first payout is still in flight.

  2. 02

    Refused when the balance cannot cover it

    A payout above the available balance is a 422 with insufficient_balance, before anything is written.

  3. 03

    Routing checked per rail

    Each payout channel validates its own destination fields, an IFSC, a branch code, an IBAN, and returns a UTR where the rail issues one.

  4. 04

    A failure is a reversal, not a deletion

    A failed or cancelled payout writes a credit that releases the hold, and the original debit stays in the history.

  5. 05

    Pending until you decide

    With no scenario a payout stays processing, so you can test the slow path, then resolve it from the dashboard.

If it works here, it works on the gateway you move to.

X-FauxPay-Signature

Webhooks that retry

A signed payload goes to your endpoint when an intent or payout reaches a terminal state. If your handler is down, the same event is sent again on this schedule, and every attempt is kept with its response body and latency.

  1. attempt 1 at once
  2. attempt 2 +1m
  3. attempt 3 +5m
  4. attempt 4 +30m
  5. attempt 5 +2h
X-Api-Key + X-Signature HMAC-signed API
Every merchant request is signed with HMAC-SHA256 over the raw body. You write the signing code you will run in production.
/checkout/{token} Hosted checkout
A token-scoped page with every method the market offers, and a simulation banner your testers cannot miss.
payouts Money out, too
Payouts hold their funds the moment they are accepted, validate the rail's own routing (an IFSC, a branch code, an IBAN) and return a UTR where the rail issues one.
backoffice A dashboard that tells you what it did
Inspect intents, payouts, the ledger and each webhook attempt, then force an outcome or resend a delivery while you debug.

Stop clicking buttons in QA.

Pass a scenario key when you create the intent and the outcome is decided before your test runs. Same request, same result, every time.

Add your own by inserting a row into the scenarios table. No code changes.

Scenario Forced outcome Delay Use it for
always_success success 0s Intent resolves as success immediately, no delay.
always_fail failed 0s Intent resolves as failed immediately — useful for QA of failure paths.
always_pending pending 0s Intent stays pending until it expires or is resolved manually.
delayed_success success 5s Success after a 5 second delay — useful for polling tests.
expired expired 0s Intent expires without producing a terminal transaction.

Copy it, sign it, ship it.

Two things to implement: signing your outbound requests and verifying our inbound webhooks. Both are HMAC-SHA256 over the raw body, the same scheme in both directions.

Create an intent

Serialise the payload once and sign those exact bytes. Re-encoding a parsed object changes the key order and breaks the signature.

<?php

$payload = json_encode([
    'amount'       => 150000,
    'currency'     => 'IDR',
    'order_id'     => 'ORDER-1029',
    'callback_url' => 'https://merchant.test/webhooks/fauxpay',
    'return_url'   => 'https://merchant.test/orders/1029/thank-you',
], JSON_UNESCAPED_SLASHES);

$response = Http::withHeaders([
    'X-Api-Key'   => config('services.fauxpay.public_key'),
    'X-Signature' => hash_hmac('sha256', $payload, config('services.fauxpay.secret')),
])->withBody($payload, 'application/json')
  ->post(config('services.fauxpay.url').'/api/v1/payment-intents');

$intent = $response->json('data');

return redirect($intent['checkout_url']);

Verify the webhook

Compare in constant time, and trust this rather than the browser redirect when deciding whether an order is paid.

<?php

$rawBody   = $request->getContent();
$signature = (string) $request->header('X-FauxPay-Signature');

$expected = hash_hmac('sha256', $rawBody, config('services.fauxpay.secret'));

if (! hash_equals($expected, $signature)) {
    abort(403, 'Invalid signature');
}

$event = json_decode($rawBody, true);

// Retries and resends repeat the same event_id: handle each one once.
if ($event['event'] === 'payment_intent.succeeded' && ! alreadyHandled($event['event_id'])) {
    // Safe to fulfil: mark the order paid, keyed by $event['order_id'].
}

The endpoints you will call first

Every endpoint in the reference
Method Path What it does
GET /api/v1/payment-channels What a market offers, and what each channel needs from the payer
POST /api/v1/payment-intents Create an intent and receive its checkout URL
GET /api/v1/payment-intents/{intent} Read a single intent
POST /api/v1/payouts Send money out, validated against the rail
GET /api/v1/balance Pending and available balance, per currency
GET /api/v1/health Public health check, no signature required

Let your AI assistant read the contract.

FauxPay runs a read-only MCP server. Add it to Claude Code, Cursor or any MCP client and ask for "integrate FauxPay IDR and MYR into my site": the agent can look up each market's channels, the fields they require, every endpoint and how to sign it.

Read the MCP setup
claude mcp add --transport http fauxpay https://fauxpay.chaad.my.id/mcp

The things you are about to ask.

Is this a real payment gateway?

No. FauxPay is a simulator. It has the shape of a gateway (intents, a hosted checkout, signed webhooks, a backoffice), but it is not licensed to process payments and has no connection to any financial network.

Can I put real card numbers into it?

Please do not. There is nowhere for them to go, and storing them would be pointless and dangerous. Use obviously fake values in obviously fake formats.

Can I use this in a live product?

No. On a FauxPay checkout the payer picks the outcome, so a live store would either give its goods away or let a customer believe they had paid. Use it for development, CI, QA and demos to stakeholders, and never put it in front of paying customers.

What happens if my webhook endpoint is down?

The delivery is retried on the schedule shown above, then marked as given up. Every attempt is logged with its response code and latency, and you can replay a delivery from the backoffice.

Do I need a queue worker to receive webhooks?

On your side, no: an HTTPS endpoint is enough. FauxPay needs its own queue worker running to deliver them.

Can I add a currency you do not support?

Yes. Insert a row into the currencies table with its symbol and decimal precision, then map its channels in the seeder. No code path is currency-specific.

What happens when I am ready for a real gateway?

You port the payment layer: no real gateway uses FauxPay's field names, statuses or signature scheme. What carries over is the shape (create an intent, send the payer to a checkout, fulfil from a verified webhook, pay out against a held balance), so your order states and handlers keep their structure. The API reference has field-by-field guides for Midtrans, Xendit and Razorpay.

Send your first payment intent today.

Create a sandbox account, copy your key pair and POST one intent. The checkout URL comes back in the same response.