Testing
Everything works in test mode with a test key (sk_test_…), and no money moves. Test and live
data never mix: a test key can’t see a live payment, and the dashboard shows the mode on every
page.
| Keys | Every account gets a standard test pair (secret and publishable) at sign-up, under Developers → API keys. When you’re signed in, these pages fill them into the examples |
| Standard keys | Can be rolled but never revoked, so there’s always one to start from |
| Underneath | A full test environment: the test cards, bank accounts and timings below behave as live ones do |
Test cards
Section titled “Test cards”Wherever you type a card (our payment page, a payment link, @tuppence/js fields), use any future
expiry, any CVC (four digits for Amex) and any postcode or ZIP code. On your server, confirm with
the name instead: POST /v1/payments/{id}/confirm with payment_method: "pm_card_gb"payment_method: "pm_card_us"payment_method: "pm_card_de"
(more).
| Card number | What happens | Server-side name |
|---|---|---|
4000 0582 6000 0005 | Succeeds (UK Visa debit) | pm_card_gb |
4242 4242 4242 4242 | Succeeds (US Visa) | pm_card_us, pm_card_visa |
3782 822463 10005 | Succeeds (Amex) | pm_card_amex |
4000 0000 0000 0077 | Succeeds; money available at once | pm_card_bypassPending |
4000 0037 2000 0278 | Succeeds; money available at once (issued abroad) | pm_card_bypassPendingInternational |
4000 0025 0000 3155 | The bank checks it’s the cardholder (3D Secure) | pm_card_authenticationRequired |
4000 0000 0000 0002 | Declined, generic_decline | pm_card_chargeDeclined |
4000 0000 0000 9995 | Declined, insufficient_funds | pm_card_chargeDeclinedInsufficientFunds |
4000 0000 0000 0259 | Succeeds, then disputed as fraudulent | pm_card_createDispute |
A card issued outside your account’s country pays the foreign-card rate: the US cards here on a UK account, the UK card on a US account.
Money to pay out
Section titled “Money to pay out”A test payment’s money stays pending for days, as in live mode, and no API releases it sooner.
| You want | Do |
|---|---|
| Money to pay out at once | Pay with pm_card_bypassPending (Visa •••• 0077) or pm_card_bypassPendingInternational (•••• 0278). The money, and a platform’s fee on it, are available straight away. Issued in the US and Ireland: the foreign-card rate applies to a card from outside your account’s country |
| To pay out pending money | An instant payout (method: "instant"): it’s advanced to you, in test as in live. Once a new account’s first 168 hours are up and it has seasoned (£1,000$1,000€1,000 from at least four cards, seven days old; move the clock to get there), instant_payouts.destinations.bank on GET /v1/payout_settings says whether you can, or why not (reason, message, fix_url) |
| To see a payment clear your settlement days | Move your test clock: seven days while the account is new, then three |
Money from any other test card shows on Balance under Pending (still clearing). The clock moves Tuppence’s settlement hold, not the card’s own pending period, so that money stays pending however far you move the clock.
- Pay
pm_card_bypassPending - Move time
clock/advance7 days - Pay out
POST /v1/payouts - Paidthe test bank
curl https://api.tuppence.ai/v1/test_helpers/clock/advance \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Content-Type: application/json" -d '{"days": 7}'
curl https://api.tuppence.ai/v1/payouts \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" -X POSTconst payment = await tuppence.payments.create({ amount: 10_000, currency: "gbp" });await tuppence.payments.confirm(payment.id, { payment_method: "pm_card_bypassPending" });// …wait until payment.net is set, then:await tuppence.request("POST", "/v1/test_helpers/clock/advance", { body: { days: 7 } });const payout = await tuppence.payouts.create({});const payment = await tuppence.payments.create({ amount: 10_000, currency: "usd" });await tuppence.payments.confirm(payment.id, { payment_method: "pm_card_bypassPending" });// …wait until payment.net is set, then:await tuppence.request("POST", "/v1/test_helpers/clock/advance", { body: { days: 7 } });const payout = await tuppence.payouts.create({});const payment = await tuppence.payments.create({ amount: 10_000, currency: "eur" });await tuppence.payments.confirm(payment.id, { payment_method: "pm_card_bypassPending" });// …wait until payment.net is set, then:await tuppence.request("POST", "/v1/test_helpers/clock/advance", { body: { days: 7 } });const payout = await tuppence.payouts.create({});import osimport timefrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
payment = tuppence.payments.create(amount=10_000, currency="gbp")# The test card whose money is available at once (no days pending).tuppence.payments.confirm(payment.id, payment_method="pm_card_bypassPending")while tuppence.payments.retrieve(payment.id).net is None: time.sleep(0.5)
# A new account's payments pay out after seven days: move this account's clock on a week.tuppence.request("POST", "/v1/test_helpers/clock/advance", body={"days": 7})payout = tuppence.payouts.create() # everything cleared
# It settles by itself, by the bank account's number: the test bank is paid.while tuppence.payouts.retrieve(payout.id).status != "paid": time.sleep(0.5)print("paid out", payout.amount) # the 10000 less our feeimport osimport timefrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
payment = tuppence.payments.create(amount=10_000, currency="usd")# The test card whose money is available at once (no days pending).tuppence.payments.confirm(payment.id, payment_method="pm_card_bypassPending")while tuppence.payments.retrieve(payment.id).net is None: time.sleep(0.5)
# A new account's payments pay out after seven days: move this account's clock on a week.tuppence.request("POST", "/v1/test_helpers/clock/advance", body={"days": 7})payout = tuppence.payouts.create() # everything cleared
# It settles by itself, by the bank account's number: the test bank is paid.while tuppence.payouts.retrieve(payout.id).status != "paid": time.sleep(0.5)print("paid out", payout.amount) # the 10000 less our feeimport osimport timefrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
payment = tuppence.payments.create(amount=10_000, currency="eur")# The test card whose money is available at once (no days pending).tuppence.payments.confirm(payment.id, payment_method="pm_card_bypassPending")while tuppence.payments.retrieve(payment.id).net is None: time.sleep(0.5)
# A new account's payments pay out after seven days: move this account's clock on a week.tuppence.request("POST", "/v1/test_helpers/clock/advance", body={"days": 7})payout = tuppence.payouts.create() # everything cleared
# It settles by itself, by the bank account's number: the test bank is paid.while tuppence.payouts.retrieve(payout.id).status != "paid": time.sleep(0.5)print("paid out", payout.amount) # the 10000 less our feePayouts
Section titled “Payouts”A test payout settles by itself, depending on the bank account’s number. To see one bounce, set a
failing test account as your bank account and pay out to it. The money returns to your available
balance and you receive payout.failed.
Sort code 10-88-00, account | Result |
|---|---|
00012345 (the test bank, or the account test onboarding attaches) | Paid |
11111116 | Fails no_account |
11111113 | Fails account_closed |
22222227 | Fails insufficient_funds |
66666668 | Takes no instant payout |
Routing number 110000000, account | Result |
|---|---|
000123456789 (the test bank, or the account test onboarding attaches) | Paid |
000111111116 | Fails no_account |
000111111113 | Fails account_closed |
000222222227 | Fails insufficient_funds |
A bank’s check (3D Secure)
Section titled “A bank’s check (3D Secure)”pm_card_authenticationRequired makes the bank check that the payer is the cardholder. Only the
payer’s browser can answer: on our payment page (or @tuppence/js on yours), a test
challenge opens with buttons to complete or fail it. Server-side examples, with no payer present, use a
card without the check, such as pm_card_gbpm_card_uspm_card_de.
Test helpers
Section titled “Test helpers”Helpers do on demand what would otherwise take days, a bank or an angry cardholder. They work in
test mode only, and all are POST:
/v1/test_helpers/… | Does |
|---|---|
payments/{id}/dispute | The cardholder disputes a payment (reason optional) |
disputes/{id}/close | The bank decides: outcome: "won" or "lost" |
webhook_inboxes | A URL that receives your webhooks |
clock/advance, clock/reset | Move time |
issuing/fund | UK: a bank transfer of amount arrives in the card balance, spendable at once (Top up the cards). US cards spend cleared earnings instead: a test payment, once it has cleared |
issuing/authorizations | A purchase on an agent’s card (card, amount, merchant), decided by its rules as a real one would be. It’s declined insufficient_funds first if the cards can’t cover it. Options below |
issuing/authorizations/{id}/capture | The merchant takes the money (all, or amount); the rest of the hold is released |
issuing/authorizations/{id}/reverse | The merchant lets the hold go |
issuing/transactions/{id}/refund | The merchant refunds a capture: all, or amount |
card_disputes/{id}/close | The network decides a submitted card dispute: outcome: "won" (money back to what the cards can spend) or "lost"; or a draft runs out of time: "expired" |
cards/{id}/shipping/{step} | The post moves a physical card: ship, deliver, fail or return |
accounts/{id}/verify | A connected account passes verification with the test values; it can take payments once the checks finish (charges_enabled; up to a minute or two) |
accounts/{id}/requirements | Set what verification asks of a connected account: currently_due, past_due, pending_verification, disabled_reason, current_deadline. Anything past due turns charges and payouts off |
bookings/seed_week | A plausible week of bookings for example clients at example.test, from your own services, staff and hours (week: any day of it, next week by default; location; density 1–100). The same week every time, never twice. Example bookings message nobody and reach no calendar |
bookings/remove_seeded | Every seeded week removed: the example bookings and the example clients nothing else names. Anything you’ve used since (money recorded against a booking, a note or form on a client) stays, and kept lists it |
issuing/authorizations also takes:
| Field | Makes it |
|---|---|
merchant_currency (and merchant_amount) | A purchase abroad, which costs FX |
risk_assessment (fraud_risk: {level, score}, merchant_dispute_risk, card_testing_risk) | What the fraud checks reported. A high score is declined suspected_fraud where your account’s limits apply |
verification_data (cvc_check, expiry_check, address_line1_check, address_postal_code_check) | What the card checks reported |
Card disputes
Section titled “Card disputes”In test mode, a card dispute is decided by its explanation.
Each sends its card_dispute.* events, as a live one would.
explanation | Outcome |
|---|---|
"winning_evidence" | Won as soon as it’s submitted |
"losing_evidence" | Lost as soon as it’s submitted |
| anything else | Stays submitted until POST /v1/test_helpers/card_disputes/{id}/close (outcome: won or lost; expired runs a draft out of time) |
curl https://api.tuppence.ai/v1/card_transactions/ctx_…/disputes \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"reason": "fraudulent", "explanation": "winning_evidence", "submit": true}'To get a purchase to dispute, fund the cards, then make and capture a purchase with the Issuing helpers above.
Card limits in test mode
Section titled “Card limits in test mode”Card limits always apply in live mode. In test mode they apply only when
limit enforcement is on for the account, so a card count never stops CI. enforced on
GET /v1/issuing/limits says which. Our team switches it on, with the rest of our fraud checks, on
request (support@tuppence.ai). Test mode then refuses cards and purchases over the limits exactly
as live does.
Moving time
Section titled “Moving time”Your account’s test mode can run ahead of real time. Every test-mode request then sees the new time: what you create is stamped then, and “the last 30 days” ends then. Live mode, and everyone else, stay on real time.
| Call | Does |
|---|---|
POST /v1/test_helpers/clock/advance | Forward by days, hours, minutes, seconds (up to five years in all) |
GET /v1/test_helpers/clock | Where it is |
POST /v1/test_helpers/clock/reset | Back to real time |
curl https://api.tuppence.ai/v1/test_helpers/clock/advance \ -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Content-Type: application/json" -d '{"days": 30}'import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);
const clock = await tuppence.request("POST", "/v1/test_helpers/clock/advance", { body: { days: 30 },});const customer = await tuppence.customers.create({ name: "Made a month from now" });console.log(clock.offset_seconds, customer.created >= clock.now); // 2592000 true
await tuppence.request("POST", "/v1/test_helpers/clock/reset");clock = tuppence.request("POST", "/v1/test_helpers/clock/advance", body={"days": 30})customer = tuppence.customers.create(name="Made a month from now")print(clock["offset_seconds"], customer.created >= clock["now"]) # 2592000 True
tuppence.request("POST", "/v1/test_helpers/clock/reset")Scheduled work (payout runs, statements, hourly usage rollups) still runs on real time. These exceptions follow the clock:
| When you advance the clock | |
|---|---|
| Subscriptions | Renewals in the skipped time are charged immediately, so you can test a year of billing in a second |
| Agents’ spending profiles | What’s normal for each agent moves with it: a week of routine bought a day at a time reads as a week on GET /v1/agents/{id}/profile at once |
| Bookings | Lapsed holds and requests free their slots; long-over visits are marked no-shows where the business asked; recurring series fill the new weeks; waitlist offers pass on; late-cancellation and no-show fees the policy charges automatically are charged when due; a bill to charge after the visit is charged an hour after completion; three days after the booking page went live, the owner’s one email about what’s left goes |
Point your tests at the test API with a test key, and use the helpers above for the slow parts.
The SDKs read TUPPENCE_API_URL; these docs use it to run their own examples.
Another API address
Section titled “Another API address”To use another Tuppence environment you’ve been given, change the address in one place:
| Where | How |
|---|---|
@tuppence/node, tuppence (Python), @tuppence/mcp | TUPPENCE_API_URL, or the baseUrl / base_url option |
@tuppence/js | loadTuppence("pk_test_…", { apiBase: "https://…" }), and that address in your page’s connect-src |
| curl, or your own client | That address in place of https://api.tuppence.ai |
Keys belong to one environment: a key from one dashboard is refused by another’s API.
- Going live
- Webhooks: test them without a server.