diff --git a/docs/index.rst b/docs/index.rst
index 7cb0b702..0f8d231a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -244,6 +244,7 @@ Available formats
in_.gstin
in_.pan
in_.vid
+ ir.nid
is_.kennitala
is_.vsk
isan
diff --git a/docs/stdnum.ir.nid.rst b/docs/stdnum.ir.nid.rst
new file mode 100644
index 00000000..aa1a2177
--- /dev/null
+++ b/docs/stdnum.ir.nid.rst
@@ -0,0 +1,5 @@
+stdnum.ir.nid
+=============
+
+.. automodule:: stdnum.ir.nid
+ :members:
\ No newline at end of file
diff --git a/stdnum/ir/__init__.py b/stdnum/ir/__init__.py
new file mode 100644
index 00000000..39cc587a
--- /dev/null
+++ b/stdnum/ir/__init__.py
@@ -0,0 +1,21 @@
+# __init__.py - collection of Iranian numbers
+# coding: utf-8
+#
+# Copyright (C) 2026 Amir Douzandeh
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, see .
+
+"""Collection of Iranian numbers."""
+
+from __future__ import annotations
diff --git a/stdnum/ir/nid.py b/stdnum/ir/nid.py
new file mode 100644
index 00000000..dc86ffce
--- /dev/null
+++ b/stdnum/ir/nid.py
@@ -0,0 +1,96 @@
+# nid.py - functions for handling Iranian national identification numbers
+# coding: utf-8
+#
+# Copyright (C) 2026 Amir Douzandeh
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, see .
+
+"""National ID Number (Iranian national identification number).
+
+The Iranian National ID Number (Code Melli) is a 10-digit number assigned to
+every Iranian citizen. The last digit is a check digit. Leading zeros are
+significant but are often lost when the number is stored as an integer, so
+numbers of 8 or 9 digits are padded with zeros.
+
+More information:
+
+* https://en.wikipedia.org/wiki/National_identification_number#Iran
+
+>>> validate('0499370899')
+'0499370899'
+>>> validate('84575948') # leading zeros are restored
+'0084575948'
+>>> validate('0499370898')
+Traceback (most recent call last):
+ ...
+InvalidChecksum: ...
+>>> validate('04993708')
+Traceback (most recent call last):
+ ...
+InvalidChecksum: ...
+>>> validate('049937')
+Traceback (most recent call last):
+ ...
+InvalidLength: ...
+>>> validate('1111111111') # repeated digits are never valid
+Traceback (most recent call last):
+ ...
+InvalidFormat: ...
+"""
+
+from __future__ import annotations
+
+from stdnum.exceptions import *
+from stdnum.util import clean, isdigits
+
+
+def compact(number: str) -> str:
+ """Convert the number to the minimal representation. This strips the
+ number of any valid separators, removes surrounding whitespace and pads
+ numbers of 8 or 9 digits with leading zeros to 10 digits."""
+ number = clean(number, ' -').strip()
+ if isdigits(number) and 8 <= len(number) < 10:
+ number = number.zfill(10)
+ return number
+
+
+def calc_check_digits(number: str) -> str:
+ """Calculate the check digit for the specified number. The number
+ passed should not have the check digit included."""
+ remainder = sum(
+ int(n) * w for n, w in zip(number[:9], range(10, 1, -1))) % 11
+ return str(remainder if remainder < 2 else 11 - remainder)
+
+
+def validate(number: str) -> str:
+ """Check if the number is a valid Iranian national ID number. This
+ checks the length, formatting and check digit."""
+ number = compact(number)
+ if not isdigits(number):
+ raise InvalidFormat()
+ if len(number) != 10:
+ raise InvalidLength()
+ if len(set(number)) == 1:
+ raise InvalidFormat()
+ if calc_check_digits(number) != number[-1]:
+ raise InvalidChecksum()
+ return number
+
+
+def is_valid(number: str) -> bool:
+ """Check if the number is a valid Iranian national ID number."""
+ try:
+ return bool(validate(number))
+ except ValidationError:
+ return False
diff --git a/tests/test_ir_nid.doctest b/tests/test_ir_nid.doctest
new file mode 100644
index 00000000..88f3d142
--- /dev/null
+++ b/tests/test_ir_nid.doctest
@@ -0,0 +1,108 @@
+test_ir_nid.doctest - more detailed doctests for stdnum.ir.nid module
+
+Copyright (C) 2026 Amir Douzandeh
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, see .
+
+
+This file contains more detailed doctests for the stdnum.ir.nid module.
+
+>>> from stdnum.ir import nid
+>>> from stdnum.exceptions import *
+
+
+Numbers that are published as examples of valid national ID numbers.
+
+>>> numbers = '''
+...
+... 0499370899
+... 0084575948
+... 0067749828
+... 0012345679
+...
+... '''
+>>> [x for x in numbers.splitlines() if x and not nid.is_valid(x)]
+[]
+
+
+Leading zeros are often lost when the number is stored as an integer, so
+shorter numbers are padded.
+
+>>> nid.compact('84575948')
+'0084575948'
+>>> nid.validate('84575948')
+'0084575948'
+>>> nid.validate('12345679')
+'0012345679'
+>>> nid.validate('1234567') # too short
+Traceback (most recent call last):
+ ...
+InvalidLength: ...
+>>> nid.validate('04993708990') # too long
+Traceback (most recent call last):
+ ...
+InvalidLength: ...
+
+
+Separators and surrounding whitespace are removed.
+
+>>> nid.validate(' 049-937089-9 ')
+'0499370899'
+
+
+Numbers consisting of a single repeated digit are not valid, even though some
+of them pass the checksum.
+
+>>> nid.is_valid('0000000000')
+False
+>>> nid.is_valid('1111111111')
+False
+>>> nid.is_valid('2222222222')
+False
+>>> nid.validate('1111111111')
+Traceback (most recent call last):
+ ...
+InvalidFormat: ...
+
+
+Other invalid numbers.
+
+>>> nid.validate('0499370898') # wrong check digit
+Traceback (most recent call last):
+ ...
+InvalidChecksum: ...
+>>> nid.validate('04993708A9') # not numeric
+Traceback (most recent call last):
+ ...
+InvalidFormat: ...
+>>> nid.validate('\u0660\u0664\u0669\u0669\u0663\u0667\u0660\u0668\u0669\u0669') # non-ASCII digits
+Traceback (most recent call last):
+ ...
+InvalidFormat: ...
+>>> nid.validate('')
+Traceback (most recent call last):
+ ...
+InvalidFormat: ...
+>>> nid.is_valid('0499370898')
+False
+
+
+Check digit calculation.
+
+>>> nid.calc_check_digits('049937089')
+'9'
+>>> nid.calc_check_digits('008457594')
+'8'
+>>> nid.calc_check_digits('000000000')
+'0'