How to validate a Turkish IBAN (and why mod-97 alone is not enough)
Turkish IBANs are 26 characters and pass mod-97 even when the bank code is nonsense. Here is what to check, in what order, with runnable code.
If you take payouts, refunds or supplier payments in Türkiye, a wrong IBAN costs you twice: the transfer bounces days later, and someone has to chase the customer. Most teams paste a regex, ship it, and keep the problem. This guide shows what actually has to be checked, in what order, and what each check can and cannot catch.
The four checks, in order
- Country and length. A Turkish IBAN is always
TR+ 24 digits = 26 characters. Length is country-specific: Germany is 22, the UK 22, Türkiye 26. A generic "IBAN is 15–34 chars" rule passes garbage. - Structure. After
TRand the two check digits comes a 5-digit bank code, one reserved digit (always0in practice), then the 16-character account number. If your validator ignores the national structure, a transposed digit block still looks fine. - Checksum (mod-97). Move the first four characters to the end, replace letters with numbers (A=10 … Z=35), and the whole number mod 97 must equal 1. This catches single-digit typos and most transpositions — but nothing about whether the bank exists.
- Bank code lookup. This is the step almost everyone skips.
00061is İş Bankası,00046is Akbank. A syntactically perfect IBAN with an unassigned bank code will still fail at the bank.
Doing it yourself: mod-97 in plain JavaScript
You do not need a library for the checksum. This is the whole algorithm — use it if all you need is step 3:
function ibanChecksumOk(raw) {
const s = raw.replace(/[\s-]/g, '').toUpperCase();
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/.test(s)) return false;
const rearranged = s.slice(4) + s.slice(0, 4);
const digits = rearranged.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55));
// The number is far beyond Number.MAX_SAFE_INTEGER — reduce it in chunks.
let remainder = 0;
for (const d of digits) remainder = (remainder * 10 + Number(d)) % 97;
return remainder === 1;
}
ibanChecksumOk('TR33 0006 1005 1978 6457 8413 26'); // true
Note the chunked remainder. Converting the rearranged string with BigInt works too, but
naive parseInt silently loses precision and returns wrong answers — a classic source of
"it validates on my machine" bugs.
What you still do not have
The snippet above cannot tell you the bank, cannot tell you that TR is not in SEPA (so
a euro SEPA transfer will not reach it), and cannot suggest a correction. That is the part that needs a
maintained table rather than an algorithm.
Doing it in one call
The endpoint runs all four checks and tells you which one failed, instead of a bare boolean:
curl "https://api.temsor.com/v1/iban/validate?iban=TR330006100519786457841326"
{
"valid": true,
"formatted": "TR33 0006 1005 1978 6457 8413 26",
"country": { "code": "TR", "name": "Türkiye", "sepa": false },
"bankCode": "00061",
"accountNumber": "0519786457841326",
"checks": {
"countryKnown": true,
"lengthValid": true,
"structureValid": true,
"checksumValid": true
},
"suggestion": null,
"warnings": []
}
The checks object is the point: when something is wrong you know whether to tell the user
"this is not a Turkish IBAN", "one character is mistyped" or "this bank code does not exist" — three very
different messages. sepa: false is there because Türkiye is not in the SEPA zone; teams
routinely discover this in production when a euro transfer is rejected.
From Node, with the client
npm install temsor-api
import Client from 'temsor-api';
const api = new Client(); // works without a key on a demo quota
const r = await api.ibanValidate({ iban: 'TR330006100519786457841326' });
if (!r.valid) console.log('rejected because:', r.reason);
else console.log(r.country.name, r.bankName ?? r.bankCode);
Where the remaining errors come from
| Symptom | Cause | Check that catches it |
|---|---|---|
| Transfer rejected next day | Bank code not assigned | Bank code lookup |
| "Invalid IBAN" on a correct account | Spaces or lower case not normalised | Normalise before validating |
| Euro transfer never arrives | Treated as SEPA | Country metadata |
| Random string accepted | Only regex, no checksum | mod-97 |
| Right IBAN, wrong person | Name/IBAN mismatch | Nothing here — only the bank knows |
That last row matters. No IBAN validator on earth can tell you whether the account belongs to the person you think it does. Validation reduces failed transfers; it does not authenticate a counterparty.
Frequently asked
How many characters is a Turkish IBAN?
26 — the country code TR, two check digits, a five-digit bank code, one reserved digit and a 16-character account number.
Is Türkiye part of SEPA?
No. Turkish IBANs are valid IBANs but they are outside the SEPA zone, so a SEPA euro credit transfer will not reach them. Use an international transfer instead.
Can IBAN validation confirm the account holder’s name?
No. Structure, checksum and bank code can all be correct for an account that belongs to someone else. Name matching is only possible through the bank or a confirmation-of-payee scheme.
Does the API need an API key?
No key is needed to try it — anonymous calls run on a narrow per-IP demo quota. A free key raises the quota.