temsor API
← Guides

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.

Updated 2026-07-26

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

  1. 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.
  2. Structure. After TR and the two check digits comes a 5-digit bank code, one reserved digit (always 0 in practice), then the 16-character account number. If your validator ignores the national structure, a transposed digit block still looks fine.
  3. 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.
  4. Bank code lookup. This is the step almost everyone skips. 00061 is İş Bankası, 00046 is Akbank. A syntactically perfect IBAN with an unassigned bank code will still fail at the bank.
The trap: mod-97 is a typo detector, not an existence check. An IBAN that a customer invented at random has a ~1-in-97 chance of passing it. That is not rare when you process thousands of records.

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

SymptomCauseCheck that catches it
Transfer rejected next dayBank code not assignedBank code lookup
"Invalid IBAN" on a correct accountSpaces or lower case not normalisedNormalise before validating
Euro transfer never arrivesTreated as SEPACountry metadata
Random string acceptedOnly regex, no checksummod-97
Right IBAN, wrong personName/IBAN mismatchNothing 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.

Endpoints used here

/v1/iban/validate

Open the reference, run it in the browser, no key required.

/v1/tr/validate

Open the reference, run it in the browser, no key required.