Overview

MockCard.io is a zero-setup REST API for generating Luhn-valid mock payment cards across four networks — Visa, MasterCard, RuPay, and Amex — with seven configurable test scenarios covering every payment outcome your code needs to handle.

Every response includes a webhook_event object that mirrors what a real payment gateway would POST to your server. Optionally supply a webhook_url and the event is delivered asynchronously after the API responds — your endpoint will receive a signed HMAC-SHA256 POST, just like Stripe or Razorpay.

PropertyValue
Base URLhttps://api.mockcard.io
ProtocolHTTPS / HTTP
FormatJSON (application/json)
AuthenticationNone
Rate limit60 requests / minute per IP

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://api.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)
amountinteger1000Amount in smallest currency unit (e.g. paise)
currencystringinrISO 4217 currency code (3 chars)
webhook_urlstring?nullOptional. If provided, the event is POSTed here after the response is sent.

Response — 201 Created (success scenario)

{
  "status": "success",
  "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
  }
}

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

GET /health

Liveness probe used by load balancers and uptime monitors. Not rate-limited. Returns HTTP 200 with a JSON body — no auth required.

Response fieldValue
statusok
versionSemver string, e.g. 1.0.0

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.

ScenarioEnum valueHTTP statusWebhook 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

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

Click a tab to see the exact JSON body POSTed to your webhook_url. All decline scenarios share the same shape — only last_payment_error.decline_code differs.

// 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
    }
  }
}

Delivery headers

HeaderDescription
Content-Typeapplication/json
X-MockCard-EventEvent type string, e.g. payment_intent.succeeded
X-MockCard-Signaturesha256=<HMAC-SHA256 hex digest of the raw body bytes>

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

Rate Limits

The API is rate-limited to 60 requests per minute per IP address. If you exceed this, you will receive a 429 Too Many Requests response. The /health endpoint is exempt.

HTTP/1.1 429 Too Many Requests
Retry-After: 60

{
  "detail": "Rate limit exceeded"
}