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.
| Property | Value |
|---|---|
| Base URL | https://api.mockcard.io |
| Protocol | HTTPS / HTTP |
| Format | JSON (application/json) |
| Authentication | None |
| Rate limit | 60 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.
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
| Field | Type | Default | Description |
|---|---|---|---|
| brand | string | visa | Card network. One of: visa · mastercard · rupay · amex |
| scenario | string | success | Test scenario (see Scenarios section below) |
| amount | integer | 1000 | Amount in smallest currency unit (e.g. paise) |
| currency | string | inr | ISO 4217 currency code (3 chars) |
| webhook_url | string? | null | Optional. 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
| Field | Type | Description |
|---|---|---|
| brand | string | Card network (visa · mastercard · rupay · amex) |
| card_number | string | Full 15 or 16-digit PAN, Luhn-valid |
| masked_number | string | PAN with all but last 4 replaced by • |
| expiry_month | string | Two-digit month (01–12) |
| expiry_year | string | Four-digit year (current year + 1 to + 5) |
| cvv | string | 3-digit CVV (4-digit CID for Amex) |
| cardholder_name | string | Uppercase cardholder name |
| luhn_valid | boolean | Always 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 field | Value |
|---|---|
| status | ok |
| version | Semver 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.
| Brand | Enum value | BIN prefixes | Card length |
|---|---|---|---|
| Visa | visa | 4 | 16 |
| MasterCard | mastercard | 51–55, 2221–2720 | 16 |
| RuPay | rupay | 60, 65, 81, 82 | 16 |
| Amex | amex | 34, 37 | 15 |
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.
| Scenario | Enum value | HTTP status | Webhook event type | Description |
|---|---|---|---|---|
| Success | success | 201 | payment_intent.succeeded | Approved — valid card returned |
| Insufficient Funds | insufficient_funds | 402 | payment_intent.payment_failed | Card declined: no funds |
| Do Not Honor | do_not_honor | 402 | payment_intent.payment_failed | Generic issuer decline |
| Expired Card | expired_card | 402 | payment_intent.payment_failed | Card past its expiry date |
| Incorrect CVV | incorrect_cvv | 402 | payment_intent.payment_failed | Security code mismatch |
| 3DS Challenge | 3ds_challenge | 402 | payment_intent.requires_action | Bank redirect required |
| Network Timeout | network_timeout | 504 | payment_intent.payment_failed | 10 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
| Scenario | Event type | PaymentIntent status | Notes |
|---|---|---|---|
| success | payment_intent.succeeded | succeeded | payment_method_details.card populated with brand + last4 |
| insufficient_funds | payment_intent.payment_failed | requires_payment_method | last_payment_error.decline_code = insufficient_funds |
| do_not_honor | payment_intent.payment_failed | requires_payment_method | last_payment_error.decline_code = do_not_honor |
| expired_card | payment_intent.payment_failed | requires_payment_method | last_payment_error.decline_code = expired_card |
| incorrect_cvv | payment_intent.payment_failed | requires_payment_method | last_payment_error.decline_code = incorrect_cvc |
| 3ds_challenge | payment_intent.requires_action | requires_action | Not a decline — next_action.redirect_to_url present; last_payment_error is null |
| network_timeout | payment_intent.payment_failed | requires_payment_method | last_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
| Header | Description |
|---|---|
Content-Type | application/json |
X-MockCard-Event | Event type string, e.g. payment_intent.succeeded |
X-MockCard-Signature | sha256=<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.
| code | decline_code | HTTP status | Description |
|---|---|---|---|
| card_declined | insufficient_funds | 402 | No funds available |
| card_declined | do_not_honor | 402 | Generic issuer decline |
| expired_card | expired_card | 402 | Card past expiry |
| incorrect_cvc | incorrect_cvc | 402 | CVV mismatch |
| authentication_required | — | 402 | 3DS redirect needed |
| gateway_timeout | — | 504 | Upstream timeout (10 s lag) |
| validation_error | — | 422 | Invalid 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"
}