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.
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
Indian Rupee
amount: 1000 = ₹10.00
4 channels, each issued at creation:
- UPI QR code
- PhonePe E-wallet
- Paytm E-wallet
- NetBanking Bank transfer
Malaysian Ringgit
amount: 1000 = RM10.00
4 channels, each issued at creation:
- Touch ’n Go eWallet E-wallet
- GrabPay E-wallet
- FPX Online Banking Bank transfer
- DuitNow QR QR code
Vietnamese Dong
amount: 1000 = ₫1,000
3 channels, each issued at creation:
- MoMo E-wallet
- ZaloPay E-wallet
- VietQR QR code
Myanmar Kyat
amount: 1000 = K1,000
3 channels, each issued at creation:
- WavePay E-wallet
- KBZPay E-wallet
- AYA Pay E-wallet
Thai Baht
amount: 1000 = ฿10.00
3 channels, each issued at creation:
- PromptPay QR code
- TrueMoney Wallet E-wallet
- SCB Bank Transfer Bank transfer
Japanese Yen
amount: 1000 = ¥1,000
3 channels, each issued at creation:
- PayPay E-wallet
- Konbini Over the counter
- Zengin Bank Transfer Bank transfer
Tether (simulated)
amount: 1000 = ₮0.001000
2 channels, each issued at creation:
- USDT (TRC-20) Crypto
- USDT (ERC-20) Crypto
Philippine Peso
amount: 1000 = ₱10.00
5 channels, each issued at creation:
- GCash E-wallet
- Maya E-wallet
- QR Ph QR code
- InstaPay Bank transfer
- 7-Eleven Over the counter
Pakistani Rupee
amount: 1000 = ₨10.00
3 channels, each issued at creation:
- Easypaisa E-wallet
- JazzCash E-wallet
- Raast 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
- A different sandbox, API and dashboard for every gateway and market
- A simulator page to click, or a per-product simulate call, to finish each payment
- Payouts and settlement tested separately, if the test mode has them at all
- A rewrite of the payment layer when the gateway choice changes
Building against FauxPay
- Register and get an API key pair straight away
- Create a payment intent and get a checkout URL back
- Send your tester there, pick an outcome, read the webhook
- 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.
-
01
No processor integration
No Stripe, Midtrans, Xendit or bank API keys anywhere in the codebase.
-
02
No card fields
FauxPay never asks for a PAN, CVV or expiry, not even in test fixtures.
-
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.
-
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.
-
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.
-
02
Refused when the balance cannot cover it
A payout above the available balance is a 422 with insufficient_balance, before anything is written.
-
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.
-
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.
-
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.
- attempt 1 at once
- attempt 2 +1m
- attempt 3 +5m
- attempt 4 +30m
- 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 setupclaude 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.