Validate credit cards with the Luhn check in one API call
A mistyped card number costs you twice: the user sees a confusing decline, and your processor charges a fee for the failed authorization. You can catch most of those before they leave the browser. The Luhn algorithm is a checksum every real card number satisfies, so a number that fails it is guaranteed invalid and never needs to reach Stripe or Adyen.
This endpoint runs the Luhn check and detects the card brand in one POST. Here is the full call.
The request
curl -X POST https://api.botoi.com/v1/validate/credit-card \
-H "Content-Type: application/json" \
-d '{"number": "4111111111111111"}'
Send the number as a string. Spaces and dashes are stripped automatically, so
4111 1111 1111 1111 and 4111-1111-1111-1111 both work. The response
tells you three things:
{
"data": {
"valid": true,
"brand": "Visa",
"type": "credit"
}
} valid: passes the Luhn checksum or not.brand: Visa, Mastercard, Amex, Discover, JCB, Diners Club, or UnionPay.type: credit or debit, from the issuer identification number.
Validate at checkout in Node.js
Call the endpoint before you send anything to your processor. A failed Luhn check returns a clear error you can show the user, and you skip the authorization fee a fake number would have cost:
// Validate at checkout before you call your payment processor.
// Catches typos and fakes so they never cost you a declined-charge fee.
async function validateCard(number) {
const res = await fetch("https://api.botoi.com/v1/validate/credit-card", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ number }),
});
const { data } = await res.json();
return data; // { valid, brand, type }
}
const result = await validateCard("4111 1111 1111 1111"); // spaces are fine
if (!result.valid) {
throw new Error("That card number is not valid. Check for a typo.");
}
console.log(`Detected ${result.brand} ${result.type} card`); Block bad input in a React form
React Hook Form runs an async validator before submit, so an invalid number never fires the
order. Wire the call straight into register:
// React Hook Form validator: block invalid cards before submit.
import { useForm } from "react-hook-form";
export function CheckoutForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
const validateNumber = async (number) => {
const res = await fetch("https://api.botoi.com/v1/validate/credit-card", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ number }),
});
const { data } = await res.json();
return data.valid || "Enter a valid card number";
};
return (
<form onSubmit={handleSubmit(submitOrder)}>
<input
{...register("number", { validate: validateNumber })}
inputMode="numeric"
placeholder="Card number"
/>
{errors.number && <span class="error">{errors.number.message}</span>}
</form>
);
} Keep your PCI scope small. The lowest-risk design tokenizes the card in the browser through your processor and only validates the leading digits for the brand logo. Run a full server-side validation when you genuinely need it, not on every keystroke, and never log the full number.
What it does and does not tell you
Luhn validation is a filter, not a guarantee. A passing number is well-formed; it does not prove the card exists or has funds. Only an authorization through your processor confirms that. Use this endpoint to reject obvious garbage cheaply and to drive the card-brand UI, then let your processor handle the real charge.
The credit card validator is one of roughly 200 single-purpose endpoints on botoi, alongside IBAN and VAT validation for the rest of your billing flow. All of them sit behind one API key with 5 req/min free. Try the call in the interactive playground or connect the MCP server to validate cards from Claude.
Frequently asked questions
- What does the Luhn check actually prove?
- The Luhn algorithm is a checksum that catches typos and randomly generated fakes. A number that fails Luhn is guaranteed invalid, so you can reject it before it ever reaches your payment processor. A number that passes Luhn is well-formed, but that does not mean the card exists or has funds. Only an authorization through your processor confirms that. Use Luhn to filter obvious garbage cheaply, then let Stripe or Adyen do the real charge.
- Does the API store or log the card numbers I send?
- No. The number is validated server-side and never stored or logged. That said, the smart pattern is to never send a full PAN over the network at all. Run the brand detection on the first 6 to 8 digits client-side for the UI, and reserve a full validation call for cases where you genuinely need it server-side, away from your PCI scope.
- Which card brands does it detect?
- Visa, Mastercard, American Express, Discover, JCB, Diners Club, and UnionPay, based on the issuer identification number (the leading digits). The response also tells you whether the number looks like a credit or debit product, which is useful for routing or surcharge logic where that distinction matters.
- Can I show the card brand logo as the user types?
- Yes, and this is the highest-value use. Detect the brand from the first few digits and swap in the matching logo before the user finishes typing. It reassures the user the form is working and reduces "is my card accepted?" support tickets. Debounce the call or run brand detection on the prefix locally and reserve the full validation for submit.
- Is running a Luhn check enough for PCI compliance?
- No. A Luhn check is input validation, not a compliance control. If your servers touch a full card number you are in PCI scope regardless of validation. The lowest-scope design is to tokenize through your processor in the browser and only validate the prefix for UI. This API helps with the validation layer; it does not change your scope obligations.
Try this API
Credit Card Validator API — interactive playground and code examples
More tutorial posts
Start building with botoi
150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.