diff --git a/README.md b/README.md
index bdd38d5..af1226f 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,42 @@ await facturapi.invoices.sendByEmail(invoice.id, {
});
```
+### Educational services (IEDU complement)
+
+Schools issuing CFDIs to parents for tuition (colegiaturas) include the IEDU complement so that parents can claim the [Decreto de Deducción de Colegiaturas](https://www.sat.gob.mx/minisitio/DeduccionesPersonales/colegiaturas.html) on their annual ISR return. The SDK exports a typed `IeduComplementInput`, a `buildIeduComplement()` helper that returns the SAT-required XML, and an `IEDU_NAMESPACE` constant to register on the invoice.
+
+```ts
+import Facturapi, { buildIeduComplement, IEDU_NAMESPACE } from 'facturapi';
+
+const invoice = await facturapi.invoices.create({
+ customer: 'YOUR_CUSTOMER_ID',
+ use: Facturapi.InvoiceUse.SERVICIOS_EDUCATIVOS, // D10
+ payment_form: Facturapi.PaymentForm.TRANSFERENCIA_ELECTRONICA_DE_FONDOS,
+ items: [
+ {
+ quantity: 1,
+ product: {
+ description: 'Colegiatura Mayo 2026 - Primaria',
+ product_key: '86121503',
+ unit_key: 'E48',
+ price: 5000,
+ taxes: [],
+ },
+ complement: buildIeduComplement({
+ nombreAlumno: 'JUAN PEREZ GARCIA',
+ CURP: 'PEGJ100515HDFRRN09',
+ nivelEducativo: 'Primaria',
+ autRVOE: 'ABC-123456',
+ rfcPago: 'PEGM800101AB1', // optional, only when payer ≠ receptor
+ }),
+ },
+ ],
+ namespaces: [IEDU_NAMESPACE],
+});
+```
+
+`nivelEducativo` accepts: `Preescolar`, `Primaria`, `Secundaria`, `Profesional Técnico`, `Bachillerato o su equivalente` (university tuition is not deductible and therefore not part of IEDU).
+
## Documentation
Visit [docs.facturapi.io](https://docs.facturapi.io).
diff --git a/src/index.ts b/src/index.ts
index c57eef6..0615b1b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -15,6 +15,7 @@ import ComercioExteriorCatalogs from './tools/comercioExteriorCatalogs';
export * from './enums';
export * from './types';
+export { buildIeduComplement, IEDU_NAMESPACE } from './utils/iedu';
const VALID_API_VERSIONS = ['v1', 'v2'];
diff --git a/src/types/complements.ts b/src/types/complements.ts
index 0a27c15..e37e00c 100644
--- a/src/types/complements.ts
+++ b/src/types/complements.ts
@@ -76,3 +76,46 @@ export interface NominaComplementData {
fecha_pago: string | Date;
num_dias_pagados: number;
}
+
+/**
+ * Education level for the IEDU (Instituciones Educativas) complement,
+ * as accepted by SAT.
+ *
+ * Reference: http://omawww.sat.gob.mx/informacion_fiscal/factura_electronica/Documents/Complementoscfdi/iedu.pdf
+ */
+export type IeduEducationLevel =
+ | 'Preescolar'
+ | 'Primaria'
+ | 'Secundaria'
+ | 'Profesional Técnico'
+ | 'Bachillerato o su equivalente';
+
+/**
+ * Input for the IEDU (Instituciones Educativas) complement, applied per
+ * line item on tuition CFDIs from accredited K-12 schools.
+ *
+ * When provided on `items[].iedu`, the SDK serializes this object into
+ * the SAT-required XML fragment, sets `items[].complement` to that XML,
+ * and registers the iedu namespace on the invoice automatically.
+ *
+ * The IEDU complement is what enables parents to claim the colegiaturas
+ * deduction on their annual ISR return (Decreto 26-Dic-2013). Applies to
+ * Preescolar, Primaria, Secundaria, Profesional Técnico, and Bachillerato.
+ * University tuition is excluded.
+ */
+export interface IeduComplementInput {
+ /** Full name of the student. */
+ nombreAlumno: string;
+ /** Student CURP (18-character SAT identifier). */
+ CURP: string;
+ /** Education level — must match the SAT-accepted enumeration. */
+ nivelEducativo: IeduEducationLevel;
+ /** RVOE authorization number issued by SEP for the school. */
+ autRVOE: string;
+ /**
+ * RFC of the actual payer when different from the receptor of the CFDI
+ * (for example, when a grandparent pays for a child whose parent is the
+ * factura receptor). Optional.
+ */
+ rfcPago?: string;
+}
diff --git a/src/utils/iedu.ts b/src/utils/iedu.ts
new file mode 100644
index 0000000..89aa12b
--- /dev/null
+++ b/src/utils/iedu.ts
@@ -0,0 +1,63 @@
+import type { XmlNamespace } from '../types/common';
+import type { IeduComplementInput } from '../types/complements';
+
+/**
+ * IEDU XML namespace as accepted by FacturAPI when stamping CFDIs that include
+ * the Instituciones Educativas complement. Pass it in the invoice's top-level
+ * `namespaces` array alongside an item-level `complement` built with
+ * {@link buildIeduComplement}.
+ */
+export const IEDU_NAMESPACE: XmlNamespace = {
+ prefix: 'iedu',
+ uri: 'http://www.sat.gob.mx/iedu',
+ schema_location: 'http://www.sat.gob.mx/sitio_interet/cfd/iedu/iedu.xsd',
+};
+
+const IEDU_VERSION = '1.0';
+
+function escapeXmlAttribute(value: string): string {
+ return value
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
+function assertNonEmpty(
+ value: string | undefined,
+ fieldName: string,
+): asserts value is string {
+ if (typeof value !== 'string' || value.trim() === '') {
+ throw new Error(
+ `IEDU complement: required field "${fieldName}" is missing or empty.`,
+ );
+ }
+}
+
+/**
+ * Serializes an {@link IeduComplementInput} into the SAT-required XML fragment
+ * that goes into the per-line-item `complement` field of an invoice. Pair it
+ * with {@link IEDU_NAMESPACE} on the invoice's top-level `namespaces` array.
+ *
+ * @throws Error if any required field is missing or empty.
+ */
+export function buildIeduComplement(input: IeduComplementInput): string {
+ assertNonEmpty(input.nombreAlumno, 'nombreAlumno');
+ assertNonEmpty(input.CURP, 'CURP');
+ assertNonEmpty(input.nivelEducativo, 'nivelEducativo');
+ assertNonEmpty(input.autRVOE, 'autRVOE');
+
+ const attrs: string[] = [
+ `version="${IEDU_VERSION}"`,
+ `nombreAlumno="${escapeXmlAttribute(input.nombreAlumno)}"`,
+ `CURP="${escapeXmlAttribute(input.CURP)}"`,
+ `nivelEducativo="${escapeXmlAttribute(input.nivelEducativo)}"`,
+ `autRVOE="${escapeXmlAttribute(input.autRVOE)}"`,
+ ];
+ if (input.rfcPago !== undefined && input.rfcPago !== '') {
+ attrs.push(`rfcPago="${escapeXmlAttribute(input.rfcPago)}"`);
+ }
+
+ return ``;
+}
diff --git a/test/invoice.spec.ts b/test/invoice.spec.ts
index 47b911f..20ddf25 100644
--- a/test/invoice.spec.ts
+++ b/test/invoice.spec.ts
@@ -1,4 +1,8 @@
-import Facturapi from '..';
+import Facturapi, {
+ buildIeduComplement,
+ IEDU_NAMESPACE,
+ IeduComplementInput,
+} from '..';
const facturapi = new Facturapi('YOUR_API_KEY_HERE');
@@ -55,3 +59,35 @@ const createInvoice = async () => {
createInvoice()
.then(() => console.log('invoice done'))
.catch(console.error);
+
+// IEDU complement type-check example (consumed by tsd):
+const ieduInput: IeduComplementInput = {
+ nombreAlumno: 'JUAN PEREZ GARCIA',
+ CURP: 'PEGJ100515HDFRRN09',
+ nivelEducativo: 'Primaria',
+ autRVOE: 'ABC-123456',
+ rfcPago: 'PEGM800101AB1',
+};
+
+const createTuitionInvoice = async () => {
+ await facturapi.invoices.create({
+ customer: 'YOUR_CUSTOMER_ID',
+ use: Facturapi.InvoiceUse.SERVICIOS_EDUCATIVOS,
+ payment_form: Facturapi.PaymentForm.TRANSFERENCIA_ELECTRONICA_DE_FONDOS,
+ items: [
+ {
+ quantity: 1,
+ product: {
+ description: 'Colegiatura Mayo 2026 - Primaria',
+ product_key: '86121503',
+ unit_key: 'E48',
+ price: 5000,
+ },
+ complement: buildIeduComplement(ieduInput),
+ },
+ ],
+ namespaces: [IEDU_NAMESPACE],
+ });
+};
+
+createTuitionInvoice().catch(console.error);
diff --git a/test/node/iedu.node.test.ts b/test/node/iedu.node.test.ts
new file mode 100644
index 0000000..4b9fccd
--- /dev/null
+++ b/test/node/iedu.node.test.ts
@@ -0,0 +1,116 @@
+import { describe, expect, it } from 'vitest'
+
+import { buildIeduComplement, IEDU_NAMESPACE } from '../../src'
+
+describe('buildIeduComplement', () => {
+ it('serializes a typed iedu input into the SAT-shaped XML fragment', () => {
+ const xml = buildIeduComplement({
+ nombreAlumno: 'JUAN PEREZ GARCIA',
+ CURP: 'PEGJ100515HDFRRN09',
+ nivelEducativo: 'Primaria',
+ autRVOE: 'ABC-123456',
+ })
+
+ expect(xml).toContain('$/)
+ })
+
+ it('includes rfcPago when provided', () => {
+ const xml = buildIeduComplement({
+ nombreAlumno: 'A',
+ CURP: 'B',
+ nivelEducativo: 'Bachillerato o su equivalente',
+ autRVOE: 'C',
+ rfcPago: 'PEGM800101AB1',
+ })
+
+ expect(xml).toContain('rfcPago="PEGM800101AB1"')
+ })
+
+ it('omits rfcPago when explicitly empty string', () => {
+ const xml = buildIeduComplement({
+ nombreAlumno: 'A',
+ CURP: 'B',
+ nivelEducativo: 'Primaria',
+ autRVOE: 'C',
+ rfcPago: '',
+ })
+
+ expect(xml).not.toContain('rfcPago')
+ })
+
+ it('escapes XML special characters in attribute values', () => {
+ const xml = buildIeduComplement({
+ nombreAlumno: 'O\'Brien & Sons "Ltd"',
+ CURP: 'X',
+ nivelEducativo: 'Primaria',
+ autRVOE: 'Y',
+ })
+
+ expect(xml).toContain(
+ 'nombreAlumno="O'Brien & Sons "Ltd""',
+ )
+ expect(xml).toContain('autRVOE="Y<Z>"')
+ })
+
+ it.each([
+ [
+ 'nombreAlumno',
+ {
+ nombreAlumno: '',
+ CURP: 'X',
+ nivelEducativo: 'Primaria' as const,
+ autRVOE: 'Y',
+ },
+ ],
+ [
+ 'CURP',
+ {
+ nombreAlumno: 'A',
+ CURP: ' ',
+ nivelEducativo: 'Primaria' as const,
+ autRVOE: 'Y',
+ },
+ ],
+ [
+ 'nivelEducativo',
+ {
+ nombreAlumno: 'A',
+ CURP: 'B',
+ nivelEducativo: '' as never,
+ autRVOE: 'Y',
+ },
+ ],
+ [
+ 'autRVOE',
+ {
+ nombreAlumno: 'A',
+ CURP: 'B',
+ nivelEducativo: 'Primaria' as const,
+ autRVOE: '',
+ },
+ ],
+ ])(
+ 'throws when required field "%s" is missing or empty',
+ (fieldName, input) => {
+ expect(() => buildIeduComplement(input)).toThrow(new RegExp(fieldName))
+ },
+ )
+})
+
+describe('IEDU_NAMESPACE', () => {
+ it('exposes the iedu prefix, uri and schema_location used by FacturAPI', () => {
+ expect(IEDU_NAMESPACE).toEqual({
+ prefix: 'iedu',
+ uri: 'http://www.sat.gob.mx/iedu',
+ schema_location: 'http://www.sat.gob.mx/sitio_interet/cfd/iedu/iedu.xsd',
+ })
+ })
+})
diff --git a/test/web/iedu.web.test.ts b/test/web/iedu.web.test.ts
new file mode 100644
index 0000000..e75e9d3
--- /dev/null
+++ b/test/web/iedu.web.test.ts
@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest'
+
+import { buildIeduComplement, IEDU_NAMESPACE } from '../../src'
+
+describe('IEDU helpers (web simulation)', () => {
+ it('builds IEDU XML in browser-like runtimes', () => {
+ const xml = buildIeduComplement({
+ nombreAlumno: 'JUAN PEREZ',
+ CURP: 'PEGJ100515HDFRRN09',
+ nivelEducativo: 'Primaria',
+ autRVOE: 'ABC-123',
+ })
+
+ expect(xml).toContain(' {
+ expect(IEDU_NAMESPACE.prefix).toBe('iedu')
+ expect(IEDU_NAMESPACE.uri).toBe('http://www.sat.gob.mx/iedu')
+ })
+})