Overview

MockCard is a zero-setup REST API for writing real payment tests without a sandbox. Generate Luhn-valid mock cards across four networks — Visa, MasterCard, RuPay, and Amex — and simulate every payment outcome your code needs to handle: approvals, declines, 3DS challenges, network timeouts, and edge-case failure modes like webhook race conditions and delivery failures.

Every response includes a webhook_event object showing the exact payload a real gateway would POST to your server. Supply a webhook_url and the event is delivered asynchronously with HMAC-SHA256 signing, automatic retries (up to 3 attempts), and optional race condition simulation — so you can test your idempotency logic before it breaks in production.

PropertyValue
Base URLhttps://www.mockcard.io
ProtocolHTTPS / HTTP
FormatJSON (application/json)
AuthenticationNone — no API key required
Webhook retriesUp to 3 attempts · exponential backoff (0 s → 2 s → 4 s)
Race condition testingsimulate_race: true fires a duplicate delivery ~1 s later

Want to see it before you code?

The live Playground lets you fire a real API request, inspect the JSON response, and watch the webhook event arrive — all in your browser.

Open Playground →

Quick Start

Send a single POST request. No API key, no sign-up, no SDK required.

curl -X POST https://www.mockcard.io/api/v1/generate \
  -H "Content-Type: application/json" \
  -d '{
    "brand": "visa",
    "scenario": "success",
    "amount": 1000,
    "currency": "inr"
  }'

POST /api/v1/generate

The core endpoint. Returns a mock card and a simulated webhook event. For error scenarios the HTTP status reflects the gateway outcome (402, 504) and the card field is absent from the response.

Request body

FieldTypeDefaultDescription
brandstringvisaCard network. One of: visa · mastercard · rupay · amex
scenariostringsuccessTest scenario (see Scenarios section below)
gatewaystringstripeResponse format to simulate. stripe (default) or razorpay — shapes both the error body and webhook payload to match that gateway's wire format.
amountinteger1000Amount in smallest currency unit (e.g. paise)
currencystringinrISO 4217 currency code (3 chars)
webhook_url Prostring?nullIf provided, the event is POSTed here after the response is sent. Must be https:// (or http://localhost for local testing). Silently ignored on free plans.
simulate_race ProbooleanfalseFire the webhook twice ~1 s apart with the same event id — tests idempotency handling. Requires a webhook_url.

Response — 201 Created (success scenario)

{
  "status": "success",
  "payment_intent_id": "pi_mock_x7y8z9a0b1c2",
  "scenario": "success",
  "card": {
    "brand": "visa",
    "card_number": "4539578763621486",
    "masked_number": "••••••••••••1486",
    "expiry_month": "07",
    "expiry_year": "2028",
    "cvv": "382",
    "cardholder_name": "PRIYA PATEL",
    "luhn_valid": true
  },
  "webhook_event": {
    "id": "evt_mock_a1b2c3d4e5f6",
    "object": "event",
    "type": "payment_intent.succeeded",
    "created": 1741708800,
    "livemode": false,
    "data": {
      "object": {
        "id": "pi_mock_x7y8z9a0b1c2",
        "object": "payment_intent",
        "amount": 1000,
        "currency": "inr",
        "status": "succeeded",
        "payment_method_details": {
          "type": "card",
          "card": { "brand": "visa", "last4": "1486", "exp_month": "07", "exp_year": "2028" }
        }
      }
    },
    "delivery": null
  }
}

Response — 402 / 504 (error scenarios)

{
  "error": {
    "code": "card_declined",
    "message": "Your card has insufficient funds.",
    "decline_code": "insufficient_funds"
  },
  "webhook_event": {
    "id": "evt_mock_d4e5f6g7h8i9",
    "object": "event",
    "type": "payment_intent.payment_failed",
    "created": 1741708800,
    "livemode": false,
    "data": {
      "object": {
        "id": "pi_mock_j0k1l2m3n4o5",
        "object": "payment_intent",
        "amount": 1000,
        "currency": "inr",
        "status": "requires_payment_method",
        "last_payment_error": {
          "code": "card_declined",
          "decline_code": "insufficient_funds",
          "message": "Your card has insufficient funds.",
          "type": "card_error"
        }
      }
    },
    "delivery": null
  }
}

Top-level response fields

FieldTypeDescription
payment_intent_idstringYour transaction reference — store this in your DB. pi_mock_xxx for Stripe, pay_mock_xxx for Razorpay. Matches webhook_event.data.object.id (Stripe) or webhook_event.payload.payment.entity.id (Razorpay).
log_idinteger | nullMockCard's internal log row — use it to correlate support requests.
webhook_eventobjectFull event payload that was (or would be) delivered to your webhook_url. Shape depends on gateway.
cardobject | absentPresent only on 201 success responses. See card fields below.
errorobject | absentPresent on all non-2xx responses. Shape depends on gateway (see Error Codes section).

Response fields — card object

FieldTypeDescription
brandstringCard network (visa · mastercard · rupay · amex)
card_numberstringFull 15 or 16-digit PAN, Luhn-valid
masked_numberstringPAN with all but last 4 replaced by •
expiry_monthstringTwo-digit month (01–12)
expiry_yearstringFour-digit year (current year + 1 to + 5)
cvvstring3-digit CVV (4-digit CID for Amex)
cardholder_namestringUppercase cardholder name
luhn_validbooleanAlways true — confirms Luhn checksum passes

Card Brands

BIN prefixes follow the official IIN ranges. Luhn check digit is computed and appended so every generated number passes real gateway validation.

BrandEnum valueBIN prefixesCard length
Visavisa416
MasterCardmastercard51–55, 2221–272016
RuPayrupay60, 65, 81, 8216
Amexamex34, 3715

Test Scenarios

Pass any of the following values as the scenario field. The API returns the appropriate HTTP status, error payload, and webhook event for each. Webhook event types shown are for gateway: stripe (default) — see Gateway Simulation for Razorpay equivalents.

Standard scenarios

ScenarioEnum valueHTTP statusStripe event typeDescription
Successsuccess201payment_intent.succeededApproved — valid card returned
Insufficient Fundsinsufficient_funds402payment_intent.payment_failedCard declined: no funds
Do Not Honordo_not_honor402payment_intent.payment_failedGeneric issuer decline
Expired Cardexpired_card402payment_intent.payment_failedCard past its expiry date
Incorrect CVVincorrect_cvv402payment_intent.payment_failedSecurity code mismatch
3DS Challenge3ds_challenge402payment_intent.requires_actionBank redirect required
Network Timeoutnetwork_timeout504payment_intent.payment_failed10 s gateway lag, then 504

Chaos scenarios Pro

These scenarios simulate edge cases that production systems fail to handle — and that no sandbox exposes. Available on Pro plans only.

ScenarioEnum valueHTTP statusStripe event typeDescription
3DS Abandoned3ds_abandoned402payment_intent.canceledUser closes the 3DS modal — intent is cancelled, payment_intent.canceled fires immediately
Limbolimbo201payment_intent.succeededPayment captured, 201 returned — but webhook is intentionally never delivered. Tests reconciliation logic.
Latencylatency201payment_intent.succeededPayment succeeds, webhook delayed 9 s — triggers timeout-then-retry double-processing bugs.

Gateway Simulation

Add "gateway": "razorpay"to any request and MockCard returns the response and webhook payload in Razorpay's exact wire format — including their error envelope, payment entity structure, and signature header. No Razorpay account, no sandbox key, no webhook config needed.

The card data is always MockCard's own format (same across all gateways). Only the webhook_event shape and error body change.

Stripe vs Razorpay — at a glance

PropertyStripe (default)Razorpay
payment_intent_id prefixpi_mock_xxxpay_mock_xxx
Success eventpayment_intent.succeededpayment.captured
Failure eventpayment_intent.payment_failedpayment.failed
3DS eventpayment_intent.requires_actionpayment.authorized
Error envelopeerror.code + decline_codeerror.code + reason + source + step + metadata.payment_id
Signature headerX-MockCard-Signature: sha256=…X-Razorpay-Signature: <hex>
Event fieldX-MockCard-EventX-Razorpay-Event-Type

Razorpay error codes

Scenariocodereasonsourcestep
insufficient_fundsBAD_REQUEST_ERRORinsufficient_balancecustomerpayment_authorization
do_not_honorBAD_REQUEST_ERRORcard_declinedcustomerpayment_authorization
expired_cardBAD_REQUEST_ERRORcard_expiredcustomerpayment_initiation
incorrect_cvvBAD_REQUEST_ERRORincorrect_cvccustomerpayment_initiation
network_timeoutGATEWAY_ERRORgateway_timeoutinternalpayment_authorization
3ds_abandonedBAD_REQUEST_ERRORauthentication_failedcustomerpayment_authentication

Verifying Razorpay webhook signatures

Razorpay uses plain HMAC-SHA256 hex — no sha256= prefix. Check the X-Razorpay-Signature header instead of X-MockCard-Signature.

import { createHmac } from "crypto";

// Razorpay uses plain HMAC-SHA256 hex — no "sha256=" prefix
function verifyRazorpayWebhook(
  rawBody: string,   // req.body as raw string (express.text() middleware)
  signature: string, // X-Razorpay-Signature header
  secret: string     // your WEBHOOK_SECRET env var
): boolean {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  return expected === signature;
}

app.post("/webhook", express.text({ type: "*/*" }), (req, res) => {
  const sig = req.headers["x-razorpay-signature"] as string;
  if (!verifyRazorpayWebhook(req.body, sig, process.env.WEBHOOK_SECRET!)) {
    return res.status(400).send("Invalid signature");
  }
  const event = JSON.parse(req.body);
  const payment = event.payload.payment.entity;

  if (event.event === "payment.captured") {
    // fulfil order — payment.id is your transaction reference
    await db.orders.fulfill(payment.id);
  }
  res.status(200).send("ok");
});

Webhook Events

Every API response includes a webhook_event field showing the exact payload that would be sent to your server. If you supply a webhook_url, the event is delivered as a background HTTP POST after the API responds — it does not block your request.

Event types

ScenarioEvent typePaymentIntent statusNotes
successpayment_intent.succeededsucceededpayment_method_details.card populated with brand + last4
insufficient_fundspayment_intent.payment_failedrequires_payment_methodlast_payment_error.decline_code = insufficient_funds
do_not_honorpayment_intent.payment_failedrequires_payment_methodlast_payment_error.decline_code = do_not_honor
expired_cardpayment_intent.payment_failedrequires_payment_methodlast_payment_error.decline_code = expired_card
incorrect_cvvpayment_intent.payment_failedrequires_payment_methodlast_payment_error.decline_code = incorrect_cvc
3ds_challengepayment_intent.requires_actionrequires_actionNot a decline — next_action.redirect_to_url present; last_payment_error is null
network_timeoutpayment_intent.payment_failedrequires_payment_methodlast_payment_error.code = gateway_timeout, type = api_error

Sample payloads — Stripe

Exact JSON body POSTed to your webhook_url when gateway: stripe (default).

// Headers
// X-MockCard-Event:     payment_intent.succeeded
// X-MockCard-Signature: sha256=a3f9c12b8e1d…

{
  "id": "evt_mock_a3f9c12b8e1d",
  "type": "payment_intent.succeeded",
  "created": 1741843200,
  "data": {
    "object": {
      "id": "pi_mock_7b2e4d9a1c3f",
      "object": "payment_intent",
      "amount": 1000,
      "currency": "inr",
      "status": "succeeded",
      "payment_method_details": {
        "card": {
          "brand": "visa",
          "last4": "1486",
          "exp_month": "07",
          "exp_year": "2028"
        }
      },
      "last_payment_error": null,
      "next_action": null
    }
  }
}

Sample payloads — Razorpay

Same scenarios with gateway: razorpay. All decline scenarios share the same shape — only error_reason differs.

// Headers
// X-Razorpay-Event-Type: payment.captured
// X-Razorpay-Signature:  a3f9c12b8e1d… (plain HMAC-SHA256 hex — no "sha256=" prefix)

{
  "entity": "event",
  "account_id": "acc_mock_f9e8d7c6b5a4",
  "event": "payment.captured",
  "contains": ["payment"],
  "payload": {
    "payment": {
      "entity": {
        "id": "pay_mock_7b2e4d9a1c3f",
        "entity": "payment",
        "amount": 1000,
        "currency": "INR",
        "status": "captured",
        "method": "card",
        "card": {
          "id": "card_mock_x1y2z3a4b5c6",
          "entity": "card",
          "name": "PRIYA PATEL",
          "network": "Visa",
          "last4": "1486",
          "expiry_month": "07",
          "expiry_year": "2028"
        }
      }
    }
  },
  "created_at": 1741843200
}

Delivery reliability

MockCard retries failed deliveries up to 3 attempts with exponential backoff (0 s → 2 s → 4 s). Your endpoint should return a 2xx status to acknowledge receipt. Any 4xx/5xx or connection error triggers the next retry.

AttemptDelayBehaviour
1immediateFirst delivery
2+2 sRetry on any 4xx/5xx or network error
3+4 sFinal retry — outcome written to api_logs

Race condition simulation

Set simulate_race: true to fire an identical duplicate delivery ~1 second after the first. Both requests carry the same event id — exactly what happens when a real gateway retries a timed-out HTTP request. Your handler must deduplicate on the event id (e.g. a DB unique constraint or a Redis SET NX check) to avoid double-processing.

Delivery headers

HeaderStripeRazorpay
Content-Typeapplication/jsonapplication/json
Event headerX-MockCard-Event: payment_intent.succeededX-Razorpay-Event-Type: payment.captured
Signature headerX-MockCard-Signature: sha256=<hex>X-Razorpay-Signature: <hex> (no prefix)

Verifying the signature

Compute HMAC-SHA256 over the raw request body bytes using your WEBHOOK_SECRET and compare to the X-MockCard-Signature header. Always use a constant-time comparison to prevent timing attacks.

import { createHmac } from "crypto";

// Call this BEFORE json.parse() — sign the raw body string, not the object
function verifyWebhook(
  rawBody: string,   // req.body as raw string (use express.text() middleware)
  signature: string, // X-MockCard-Signature header
  secret: string     // your WEBHOOK_SECRET env var
): boolean {
  const expected =
    "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  return expected === signature;
}

// Express.js example
app.post("/webhook", express.text({ type: "*/*" }), (req, res) => {
  const sig = req.headers["x-mockcard-signature"] as string;
  if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET!)) {
    return res.status(400).send("Invalid signature");
  }
  const event = JSON.parse(req.body);
  if (event.type === "payment_intent.succeeded") {
    // fulfil order…
  }
  res.status(200).send("ok");
});

Error Codes

All non-2xx responses use a consistent { error: { code, message, decline_code? } } envelope so you can handle every case with a single switch statement.

codedecline_codeHTTP statusDescription
card_declinedinsufficient_funds402No funds available
card_declineddo_not_honor402Generic issuer decline
expired_cardexpired_card402Card past expiry
incorrect_cvcincorrect_cvc402CVV mismatch
authentication_required4023DS redirect needed
gateway_timeout504Upstream timeout (10 s lag)
validation_error422Invalid request body field

Failure Scenarios

Real payment flows break in ways that happy-path tests never catch. MockCard exposes four classes of failure — visit the Chaos Simulator to see live payloads, then reproduce them in your CI using the parameters below.

1 · Webhook Race Condition Pro

A real gateway may deliver the same webhook twice when its first HTTP attempt times out before your server responds. Both carry the same event id, arriving ~1 s apart. Without deduplication, a double-delivery double-credits the user.

How to triggerWhat to fix
Add simulate_race: true + a webhook_url. Works with any scenario.Reject any event whose id was already processed — DB UNIQUE constraint on event_id or Redis SET NX.

2 · 3DS Abandonment Pro

User closes the 3DS modal before completing authentication. The gateway fires payment_intent.canceled immediately — your app must cancel the order rather than leaving it in pending.

How to triggerWhat to fix
3ds_abandoned — fires the canceled event directly, no manual interaction needed.Handle payment_intent.canceled (Stripe) or payment.failed with reason=authentication_failed (Razorpay) — mark the order as cancelled, not just pending.

3 · The Limbo State Pro

Payment is captured and a 201 is returned — but the webhook is intentionally never delivered. The order stays pending with no automated recovery path. Tests your reconciliation / polling logic.

How to triggerWhat to fix
limbo — returns 201 success with a valid card, silences the webhook entirely.Build a reconciliation job that polls for captured payments with no matching fulfilled order and re-queues or flags them for manual review.

4 · Latency Storm Pro

Payment succeeds instantly, but webhook delivery is delayed 9 s per attempt. On default Nginx / ALB timeout configs (10 s), your server closes the connection and MockCard retries. If the retry succeeds your handler runs twice for the same event.

How to triggerWhat to fix
latency — returns 201 immediately, delays all webhook delivery attempts by 9 s.Make your handler idempotent and deduplicate on event id — the same event arriving twice after a timeout must be a no-op.