endpoint.pe

Cómo consultar un DNI con Python paso a paso

Consulta DNI con Python: una función con requests y manejo de errores, validación de formato previa, uso en Django o FastAPI y un ejemplo de lote con pausas.

eEquipo endpoint.pe•29 ago. 20268 min de lectura

Tienes una lista de clientes con DNI y nombres escritos a mano, un formulario donde la gente se equivoca al tipear, o un sistema de boletas que necesita el nombre correcto del comprador. En los tres casos la solución es la misma: consultar el DNI contra RENIEC y quedarte con los nombres y apellidos oficiales.

En esta guía vas a hacerlo con Python. Empezamos por validar el formato antes de gastar una consulta, seguimos con una llamada simple usando requests, la convertimos en una función con manejo de errores, la exponemos desde Django y FastAPI, y terminamos con un script para procesar un lote de DNI con pausas entre consultas.

Los ejemplos usan la API de consulta DNI de endpoint.pe. Devuelve nombres, apellido paterno y apellido materno en JSON, que es justo lo que necesitas para un comprobante o un registro de cliente.


Lo que necesitas

Python 3.10 o superior (usamos la sintaxis dict | None en las anotaciones), la librería requests y un token de la API. Instala requests con pip install requests si no lo tienes. Si aún no tienes token, crea una cuenta: incluye 150 consultas gratis al mes sin tarjeta, suficiente para desarrollar y probar.

El token va en una variable de entorno, nunca en el código ni en el repositorio. En tu terminal, antes de ejecutar los ejemplos:

export ENDPOINT_PE_TOKEN="tu_token"

En producción lo defines en el servidor, en el contenedor o en el gestor de secretos que uses. Lo que importa es que os.environ lo encuentre cuando arranca la aplicación.

Un recordatorio antes de escribir código: el DNI y los nombres de una persona son datos personales protegidos por la Ley 29733. Consulta solo cuando tengas una finalidad concreta (emitir una boleta, registrar un cliente) y no guardes más de lo que necesitas. En la guía de protección de datos para desarrolladores explicamos qué implica en la práctica.

Valida el formato antes de gastar una consulta

Un DNI tiene exactamente 8 dígitos. Parece trivial, pero la mayoría de consultas fallidas vienen de aquí: alguien pegó el número con un espacio al final, escribió 7 dígitos, o el DNI empezaba en cero y Excel se lo comió al abrir el CSV.

La regla es tratar el DNI como texto, nunca como número. Si lo conviertes a int, el DNI 01234567 se convierte en 1234567 y ya no coincide con nada. Esta función resuelve la validación:

import re


def dni_valido(dni: str) -> bool:
    """Devuelve True si el texto tiene exactamente 8 dígitos."""
    return re.fullmatch(r"\d{8}", dni) is not None

Antes de validar, limpia la entrada con dni.strip(). Si trabajas con datos que vienen de una hoja de cálculo y sospechas que se perdieron ceros a la izquierda, dni.zfill(8) los repone, pero úsalo solo cuando estés seguro de que ese fue el problema: rellenar con ceros un número que en realidad tenía 7 dígitos por un error de tipeo te da un DNI de otra persona.

Validar primero tiene una ventaja concreta: cada consulta a la API cuesta un crédito, y un DNI de 7 dígitos nunca va a existir. Rechazarlo localmente es gratis e instantáneo. Los errores de formato más comunes y cómo tratarlos están en cómo validar un DNI en tu sistema.

Tu primera consulta con requests

La llamada mínima es esta. Usamos 12345678 como DNI de ejemplo; es un número ficticio para ilustrar la respuesta, no corresponde a una persona real.

import os

import requests

TOKEN = os.environ["ENDPOINT_PE_TOKEN"]

respuesta = requests.get(
    "https://api.endpoint.pe/v1/dni/12345678",
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=10,
)

print(respuesta.status_code)
print(respuesta.json())

Si el token es correcto y el DNI existe, vas a ver un 200 y un cuerpo con esta forma:

{
  "success": true,
  "message": "Consulta exitosa",
  "data": {
    "dni": "12345678",
    "nombres": "JUAN CARLOS",
    "apellido_paterno": "PÉREZ",
    "apellido_materno": "QUISPE"
  },
  "meta": {
    "fuente": "RENIEC",
    "consultado_en": "2026-10-12T07:40:12Z",
    "creditos_cobrados": 1,
    "creditos_disponibles": 14621
  }
}

Tres cosas a notar. os.environ["..."] con corchetes falla de inmediato si la variable no existe, que es lo que quieres: mejor un error claro al arrancar que una petición con token vacío. El timeout=10 es obligatorio en cualquier código que vaya a producción; sin él, requests puede esperar para siempre. Y respuesta.json() ya te devuelve un diccionario de Python, sin que tengas que importar json.

Una función con manejo de errores

El snippet anterior sirve para probar, pero no para integrarlo en un sistema. Necesitas distinguir "el DNI no existe" de "la API no respondió" o "me quedé sin créditos", porque cada caso se trata distinto. Esta función lo hace:

import os

import requests

BASE_URL = "https://api.endpoint.pe/v1"
TOKEN = os.environ["ENDPOINT_PE_TOKEN"]


class ErrorConsultaDni(Exception):
    """Fallo que no depende del DNI consultado: token, créditos, red."""


def consultar_dni(dni: str) -> dict | None:
    if not dni_valido(dni):
        raise ValueError(f"DNI con formato inválido: {dni!r}")

    try:
        respuesta = requests.get(
            f"{BASE_URL}/dni/{dni}",
            headers={
                "Authorization": f"Bearer {TOKEN}",
                "Accept": "application/json",
            },
            timeout=10,
        )
    except requests.Timeout as e:
        raise ErrorConsultaDni("La API no respondió a tiempo") from e
    except requests.ConnectionError as e:
        raise ErrorConsultaDni("No se pudo conectar con la API") from e

    if respuesta.status_code == 404:
        return None
    if respuesta.status_code == 401:
        raise ErrorConsultaDni("Token inválido o vencido")
    if respuesta.status_code == 402:
        raise ErrorConsultaDni("Sin créditos disponibles")
    if respuesta.status_code == 429:
        raise ErrorConsultaDni("Límite de velocidad superado, espera y reintenta")

    respuesta.raise_for_status()
    return respuesta.json()["data"]

La función tiene tres salidas posibles y cada una significa algo distinto para quien la llama. Devuelve un diccionario cuando el DNI existe. Devuelve None cuando la API responde 404: el número está bien formado pero no figura, y eso es una respuesta legítima que tu sistema debe mostrar como tal (además, las consultas sin resultado no se cobran). Y lanza ErrorConsultaDni cuando el problema no tiene que ver con el DNI sino con tu integración: token, créditos, red o límite de velocidad.

El ValueError para formato inválido es deliberado. Es un error de programación de quien llama (debió validar antes), no un fallo en tiempo de ejecución, así que se trata como una excepción distinta. Si prefieres que la función sea tolerante, cambia el raise por un return None, pero entonces pierdes la distinción entre "mal escrito" y "no existe".

El 429 merece una pausa y un reintento con espera creciente, y los 5xx uno o dos reintentos antes de rendirse. No lo metimos en la función para mantenerla corta; en manejo de errores y reintentos al consumir APIs está el patrón completo con backoff exponencial.

Usarla en Django o FastAPI

En una aplicación web, el navegador nunca llama a la API directamente: lo hace tu backend, que es quien tiene el token. Pon dni_valido, consultar_dni y ErrorConsultaDni en un módulo propio (por ejemplo servicios.py) y expón un endpoint interno que el frontend consuma.

En Django, una vista de función es suficiente:

# clientes/views.py
from django.http import JsonResponse
from django.views.decorators.http import require_GET

from .servicios import ErrorConsultaDni, consultar_dni, dni_valido


@require_GET
def buscar_dni(request, dni):
    if not dni_valido(dni):
        return JsonResponse({"error": "formato_invalido"}, status=400)

    try:
        persona = consultar_dni(dni)
    except ErrorConsultaDni:
        return JsonResponse({"error": "servicio_no_disponible"}, status=503)

    if persona is None:
        return JsonResponse({"error": "not_found"}, status=404)

    return JsonResponse(persona)

En urls.py la ruta recibe el DNI como parámetro de texto:

# clientes/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path("api/dni/<str:dni>/", views.buscar_dni),
]

Recuerda proteger esta vista con login_required o el mecanismo de autenticación que uses: si queda abierta, cualquiera puede gastar tus créditos desde fuera.

En FastAPI el equivalente es casi idéntico:

from fastapi import FastAPI, HTTPException

from servicios import ErrorConsultaDni, consultar_dni, dni_valido

app = FastAPI()


@app.get("/api/dni/{dni}")
def buscar_dni(dni: str):
    if not dni_valido(dni):
        raise HTTPException(status_code=400, detail="formato_invalido")

    try:
        persona = consultar_dni(dni)
    except ErrorConsultaDni:
        raise HTTPException(status_code=503, detail="servicio_no_disponible")

    if persona is None:
        raise HTTPException(status_code=404, detail="not_found")

    return persona

Fíjate en que la función del endpoint es def y no async def. Como requests es una librería bloqueante, FastAPI ejecuta las funciones síncronas en un hilo aparte y no bloquea el bucle de eventos. Si quieres un endpoint async, cambia requests por un cliente asíncrono como httpx; la lógica de códigos de estado es la misma.

En ambos frameworks el patrón es el mismo: el backend valida el formato, consulta con el token que solo él conoce, traduce el resultado a un código HTTP propio y devuelve únicamente los campos que el frontend necesita.

Consultar un lote de DNI con pausas

El otro escenario típico es una migración o una limpieza de datos: tienes un CSV con miles de DNI y quieres completar o corregir los nombres. Aquí importan tres cosas: no disparar todas las consultas a la vez, poder retomar si el script se corta y dejar registro de qué pasó con cada fila.

import csv
import time

from servicios import ErrorConsultaDni, consultar_dni, dni_valido

PAUSA = 0.5  # segundos entre consultas
CAMPOS = ["dni", "nombres", "apellido_paterno", "apellido_materno", "estado"]

with open("dnis.csv", newline="", encoding="utf-8") as entrada, open(
    "resultado.csv", "w", newline="", encoding="utf-8"
) as salida:
    lector = csv.DictReader(entrada)
    escritor = csv.DictWriter(salida, fieldnames=CAMPOS, extrasaction="ignore")
    escritor.writeheader()

    for fila in lector:
        dni = fila["dni"].strip()

        if not dni_valido(dni):
            escritor.writerow({"dni": dni, "estado": "formato_invalido"})
            continue

        try:
            persona = consultar_dni(dni)
        except ErrorConsultaDni as e:
            escritor.writerow({"dni": dni, "estado": f"error: {e}"})
            time.sleep(5)
            continue

        if persona is None:
            escritor.writerow({"dni": dni, "estado": "no_encontrado"})
        else:
            escritor.writerow({**persona, "estado": "ok"})

        time.sleep(PAUSA)

La pausa de medio segundo entre consultas es conservadora a propósito. Un lote no tiene apuro: procesar 2 000 DNI en veinte minutos es mejor que recibir un 429 a los diez segundos y tener que reiniciar. Cuando ocurre un error de integración, el script espera cinco segundos antes de seguir, lo que suele bastar para que un límite de velocidad se libere.

Antes de correr el lote, elimina duplicados: no tiene sentido pagar dos veces por el mismo DNI. Y si el archivo es grande, haz que el script lea resultado.csv al arrancar y salte los DNI que ya tienen estado ok, así puedes relanzarlo sin repetir consultas. Para algo más elaborado, con los resultados en base de datos y tiempos de vida por tipo de dato, revisa caché de consultas RUC y DNI.

En resumen

  • Trata el DNI como texto de 8 dígitos y valídalo con una expresión regular antes de consultar.
  • Usa requests.get con cabecera Bearer y timeout; el token siempre viene de una variable de entorno.
  • Distingue tres resultados: diccionario (existe), None (404, no existe) y excepción (token, créditos, red).
  • En Django o FastAPI, el backend consulta y el navegador solo habla con tu endpoint interno.
  • En lotes, pausa entre consultas, elimina duplicados y deja el script preparado para retomar.
  • Los datos que devuelve la consulta son personales: guarda lo mínimo y por el tiempo necesario.

Preguntas frecuentes

¿Qué devuelve la consulta de DNI y qué no?

Devuelve el número, los nombres y los dos apellidos, que es lo necesario para un comprobante o un registro de cliente. No esperes dirección, fecha de nacimiento ni foto: ese tipo de datos no forma parte de la respuesta.

¿Puedo usar httpx o aiohttp en lugar de requests?

Sí. La lógica de la función no cambia: cabecera Authorization, timeout, y traducir 404 a None y los demás códigos a excepciones. Con httpx incluso puedes mantener la misma estructura y agregar async cuando lo necesites en FastAPI.

¿Cuántas consultas gasta un lote de 1 000 DNI?

Como máximo 1 000, una por DNI distinto que tenga formato válido. Los DNI mal formados se descartan antes de llamar y los que responden 404 no se cobran. Si eliminas duplicados y reutilizas resultados anteriores, el número real suele ser bastante menor.

¿Cómo pruebo el código sin gastar créditos?

En tests unitarios, reemplaza requests.get con unittest.mock.patch y devuelve respuestas simuladas con los códigos 200, 404 y 429. Así verificas que cada rama de consultar_dni hace lo correcto sin tocar la red.

Deja de copiar datos a mano. Automatízalo hoy mismo

Crea tu cuenta, copia tu token y empieza hoy con 150 consultas gratis al mes.