A credit card expiry date occupies four characters on the plastic and causes a disproportionate number of bugs in the software that reads it. The printed value is short, but it carries two separate rules at once: which month the card stops working, and what time on the last day of that month it actually stops. Getting the boundary wrong is one of the classic ways a checkout rejects a card that is still perfectly usable.
The following sections explain what the printed date means, how the end-of-month rule works, why forms and databases rarely store the same thing the card shows, and how to test the awkward cases.
What the printed date means
The date on a card is a month and a year, each written as two digits, with the month first: month nine and year twenty-six, for example, or month one and year thirty. There is no day on the card, which is the first source of confusion — people look for one because every other date they handle has one.
The month and year describe when the card ceases to be valid, not when it was issued. A card printed with a date several years ahead is normal, because issuers typically replace a card well before it expires.
The value is not derived from anything else on the card. It is not a checksum and it does not interact with the number. That matters for testing: an expiry date cannot be invented by arithmetic, so it has to be supplied, which is why generated cards come with a date attached.
Is a card valid for the whole month?
Yes. This is the rule that catches people out. A card printed with a given month and year is accepted through the last day of that month, up to the final moment of that day, in the time zone the issuer’s authorisation system uses.
A card printed with month nine and year twenty-six is therefore usable on the thirtieth of September in that year, and it stops being usable at the first moment of October. Any system that cuts the card off at the beginning of September, or at midnight on the last day, is rejecting valid payments for a period measured in hours or days.
| Printed month and year | First day it is valid | Last day it is valid |
|---|---|---|
| Month 1, year 2026 | 1 January 2026 | 31 January 2026 |
| Month 9, year 2026 | 1 September 2026 | 30 September 2026 |
| Month 12, year 2026 | 1 December 2026 | 31 December 2026 |
The table also shows why the day count matters. February is the awkward one, because the last day depends on the year, and a form or a scheduled job that assumes twenty-eight days will get the boundary wrong in a leap year.
Why do forms ask for two digits instead of four?
Cards print only two digits for the year to save space, and forms usually ask for the same two because that is what the user is reading off. That convention shifts a decision onto the software: which century does the value belong to?
Writing the century out in prose rather than in code, the common approach is to treat a two-digit year as belonging to the current or next century based on how close it is to today. A year value that looks like it is far in the past is almost always a typing error, not a card from an earlier era, and a value decoded to the wrong century will either be rejected as expired or accepted forever.
The safest approach for a form is to accept what the user has, convert it once to an unambiguous representation — a month and a full four-digit year, or a first day and a last day — and use that representation everywhere afterwards. Never compare two-digit years directly, because the comparison silently redefines the meaning of the century at every rollover.
Display format and stored format are different things
The string a customer sees and the value a system keeps are rarely the same, and mixing them up is a reliable source of defects.
On the card, and in an input field, the value appears as two digits for the month and two for the year. In a database it is often stored as a first-of-month date, or as separate integer columns for month and year, or as a timestamp of the last moment of validity. Each choice has consequences. A first-of-month timestamp is easy to sort and easy to compare, but it does not tell you when the card expires unless you also know the end-of-month rule. A last-moment timestamp encodes the correct boundary but looks alarming in an administrative screen where nobody expects to see a time of day next to an expiry.
Whichever representation you choose, keep the conversion in one place. The bug that appears in production is usually a second conversion written by someone who did not know the first one existed, and the two disagree by a month.
Common mistakes when typing an expiry date
The errors people make on this field follow a short list, and each of them has a sensible response:
- Typing the year as four digits into a two-digit field. Accept it if you can map it unambiguously, and do not silently drop the leading characters.
- Swapping the month and the year. Month thirteen and year twenty-eight are both invalid, so validate each part against its own range rather than only their combination.
- Entering a month with a leading zero in a field that expects one digit. Treat one and zero-one as the same month.
- Pasting a value with a separator that the field does not expect, such as a hyphen where a slash belongs.
- Entering a date that has already passed, which is a rejection the form should explain rather than merely flag.
For developers: input controls and boundary tests
Two small controls prevent most of this pain. First, separate month and year into distinct inputs, or use a single field that visibly guides the expected shape, so that a swapped value is unlikely rather than merely detectable. Second, validate each component against its own range before validating the combination, so the user learns which part is wrong.
For the boundary logic, test the ends of the range rather than the middle. The four cases worth writing down are the first day of the printed month, the last day of the printed month, the day after the last day, and the same last-day case in a leap year. A test written against the middle of a month proves almost nothing about the rule you are trying to protect.
Then check how your system behaves when the clock moves. If a stored expiry is converted once at the moment of submission and never revisited, a form left open across a month boundary will submit a date that has just become invalid. Decide deliberately whether that is an error worth surfacing or an acceptable edge, and make sure the payment request and your own records agree about it.
Getting a matching expiry from the card tool
Every card produced by the card number generator comes with an expiry date that fits the same convention: a month and a year in the future, consistent across a whole batch. That consistency is what makes a set of fixtures usable — a test that pairs a number with a date expects the pair to remain valid for as long as the suite is in use, which is one reason to keep generated data structurally correct and never issued rather than inventing values by hand.
If you need several cards that all share one expiry, generate a batch and select from it rather than retyping the date, because a single mistyped year is hard to spot in a fixture file. The payment form checklist covers the wider set of flows around this field, and the security code guide explains the other four characters a customer reads off the card.
Next steps
Decide, in writing, what your system means by an expiry date — a month, a first day or a last moment — and put the conversion behind a single function. Then add the four boundary tests described above, including the leap-year case, and confirm that a card printed with the current month is still accepted on its final day.