A sandbox is very good at telling you your integration works. It is much worse at telling you your integration survives, because the happy path is the only one it produces unless you ask. This is the test plan we would want before taking real money: what the sandbox can simulate, what it claims to and did not, and what you have to fake yourself.

Everything marked as tested was run against the Paydude sandbox on 28 September 2026.

Test cards

BrandNumberCVCExpiry
Visa4111 1111 1111 1111Any 3 digitsAny future date
Visa4900 7700 0000 0001Any 3 digitsAny future date
Mastercard5100 4000 0000 0000Any 3 digitsAny future date
Discover (US)6011 2345 6789 0123Any 3 digitsAny future date
American Express (US)3701 234567 89017Any 4 digitsAny future date
Sandbox test cards, from the Paydude testing guide

Collect them through the hosted card fields exactly as in production. Posting a card number to the API from your own server works in the sandbox, but it is the one habit you must not carry into production — see PCI scope.

Declines are triggered by amount

The card number does not decide the outcome. The payment intent's amount does:

AmountFailure code
$7,777.01insufficient_funds ✓ tested
$7,777.02avs_check_failed
$7,777.03generic_decline
$7,777.04call_issuer
$7,777.05expired_card ✓ tested
$7,777.06pickup_card
$7,777.07invalid_number
$7,777.08limit_exceeded
$7,777.09not_permitted
$7,777.10incorrect_cvc
$7,777.11service_not_allowed
$7,777.12invalid_expiry
$7,777.13card_not_supported
$7,777.14restricted_card
$7,777.15fraudulent
$7,777.16processing_error
Decline trigger amounts (777701–777716 in cents)

The trigger amounts suit a test harness that creates its own payment intents. For a test that goes through your checkout, you need a product priced at $7,777.01 in your test catalogue, or a test-only hook that overrides the amount.

Disputes

Disputes are triggered the same way. These are from the testing guide and were not exercised for this article:

AmountSimulatesEnds in
$7,777.97An inquirywarning_under_review
$7,777.98An inquiry that escalates to a chargebackneeds_response
$7,777.99A formal chargebackneeds_response
Dispute trigger amounts, per the Paydude testing guide

When you submit evidence, an evidence_description of REVERSAL resolves the dispute as won and LOSS as lost, according to the same guide.

The scenarios worth a test each

ScenarioHow to produce itWhat to assert
Card approvedAny ordinary amountOrder paid only after succeeded, not after a 2xx
Card declined$7,777.01201 with status: requires_payment_method; a useful message; order still open
Retry after a declineDecline, then confirm the same intent with a new cardTwo charges on one intent; the order pays once
Reusing a declined cardConfirm a new intent with the declined payment methodYour UI asks for the card again rather than surfacing a 400
Token expiryTokenise, wait past expires_at, confirmA clear retry, not a stuck checkout
Bank paymentAn ACH debitOrder stays pending through processing; fulfils on the webhook
Subscription payment failsA subscription priced at $7,777.01 ✓ testedYour dunning opens a case when it goes past_due
Duplicate webhookReplay a stored event to your own endpointNothing happens twice
Out-of-order webhookDeliver a later event before an earlier oneFinal state is correct
RefundRefund a succeeded paymentOrder marked refunded on success, not on request
Dispute$7,777.99Someone is alerted before closing_at
A test matrix for a payments integration

Two of those deserve a note from the sandbox runs. A declined payment method cannot be reused — confirming with it again returns 400, "The payment method … is not chargeable" — so the retry test has to tokenise a fresh card. And an unattached payment method carries an expires_at: in our runs it was ten minutes after creation. Read the field rather than hard-coding a window.

Subscriptions do not bill at the exact anchor time. A subscription anchored at 22:42 UTC was billed at 23:00:00 — the sandbox appears to run renewals on the hour — so a test waiting on a renewal should poll for up to an hour rather than sleep until the anchor.

Bank payments are slow on purpose. In the sandbox an ACH debit reported processing and succeeded about fourteen and a half hours later, so an end-to-end ACH test either waits overnight or stops at processing and tests the webhook handler separately with a recorded payment_intent.succeeded payload.

What the sandbox did not simulate

The testing guide documents more simulations than the card-amount triggers. On the sandbox merchant we tested, these did not behave as described:

  • Address verification prefixes. The guide says prefixing the street with a code such as 11 makes the postal-code check fail. We charged cards with street prefixes 10, 11, 12 and A11; every address_line1_check and address_postal_code_check came back pass.
  • Failing CVC values. The guide says any CVC except 333 or 666 passes, implying those two fail. Both produced successful payments with cvc_check: pass.
  • Filtering events by delivery status. GET /v1/events?delivery_status=… returned a 500 for every documented value. See handling webhooks reliably for a sweep that does not need it.

Note also that the checks read unchecked on a freshly created payment method and are only filled in once it has been charged — so a test that inspects card.checks straight after tokenising sees nothing either way.

The practical answer is to test AVS and CVC handling at the unit level: record the response shapes, construct the failing ones yourself, and assert your code does the right thing with address_postal_code_check: "fail" without needing the sandbox to produce it.

What no sandbox can simulate

The most expensive production failure in payments is a request that succeeds on the server and whose response is lost on the way back. Your code sees a timeout; the customer has been charged. The sandbox will never do this to you on request, so wrap your API client and do it yourself:

// Test-only wrapper: performs the real call, then pretends it timed out.
// This is the exact failure an idempotency guard exists for.
function withLostResponses(api, { rate = 1 } = {}) {
  return {
    ...api,
    async post(path, body) {
      const result = await api.post(path, body);   // the write really happens
      if (Math.random() < rate) {
        throw Object.assign(new Error("ETIMEDOUT"), { code: "ETIMEDOUT" });
      }
      return result;
    },
  };
}

test("a lost response does not charge twice", async () => {
  const flaky = withLostResponses(sandboxApi, { rate: 1 });
  await expect(checkout(order, { api: flaky })).rejects.toThrow();

  // Retry with the real client, as your retry logic would.
  await checkout(order, { api: sandboxApi });

  const intents = await sandboxApi.get(
    `/v1/payment-intents?metadata[reference]=${order.reference}`);
  expect(intents.items).toHaveLength(1);
});
Fault injection for the lost-response case

The Paydude API has no idempotency keys, so this test is the only evidence your own guard works — the pattern it exercises is in idempotency keys explained. Adjust the metadata query to however your client encodes nested parameters.

The other things beyond any sandbox: how real issuers distribute their declines, real 3D Secure challenges on real cards, and real settlement timing. Sandbox payouts ran at about 12:00 UTC each day in our runs; that is the sandbox's schedule, not a promise about production. Plan a small live test with a real card and a real refund before launch, and watch the first week of production declines closely.

Before you go live

  1. Every row of the test matrix above passes.
  2. The lost-response test passes and runs in CI.
  3. Test-only code paths — raw card posting, amount overrides, fault injection — cannot run in production.
  4. Production keys and the production account id are in production config only, and sandbox keys are nowhere near it.
  5. The webhook endpoint is registered for production, verified with the production signing secret, and alerting on status changes.
  6. One real payment and one real refund have been made and reconciled against a payout.

GOOD QUESTIONS

Frequently asked

What card number should I use to test payments?+

4111 1111 1111 1111 with any future expiry and any CVC works in the Paydude sandbox, along with test Mastercard, Discover and American Express numbers. The card number does not control the outcome — the payment amount does.

How do I simulate a declined card in the sandbox?+

Create the payment intent for one of the trigger amounts between $7,777.01 and $7,777.16. $7,777.01 declines with insufficient_funds, $7,777.05 with expired_card and $7,777.15 with fraudulent. Smaller amounts such as $77.01 succeed.

How do I test a dispute?+

Use $7,777.97 for an inquiry, $7,777.98 for an inquiry that escalates, or $7,777.99 for a formal chargeback. When submitting evidence, the testing guide says an evidence description of REVERSAL wins the dispute and LOSS loses it.

Can I test AVS and CVC failures in the sandbox?+

The testing guide documents street-address prefixes and CVC values for this, but on the sandbox merchant we tested every address and CVC check came back as a pass. Test that handling at the unit level with constructed responses instead.

How do I test what happens when an API response is lost?+

Wrap your API client so it performs the real request and then throws a timeout, and assert that your retry logic does not create a second payment. No sandbox produces this failure on request, and it is the one most likely to double-charge a customer.

How long do test ACH payments take?+

In our sandbox run an ACH debit stayed in processing for about fourteen and a half hours before succeeding. Sandbox timing is not production timing, but it is long enough that ACH tests should exercise the webhook handler rather than wait for the payment.