> ## Documentation Index
> Fetch the complete documentation index at: https://docs.origoid.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Python

> `origoid` — cliente oficial para Python 3.9+. Sync, async, modelos tipados.

El SDK de Python trae clientes sync y async, modelos de respuesta
tipados (sobre [Pydantic](https://docs.pydantic.dev/), la librería
estándar de validación de datos en Python), type hints completos, y un
método tipado por cada endpoint de OrigoID.

## Instalación

```bash theme={null}
pip install origoid
```

O con [`uv`](https://docs.astral.sh/uv/) / [`poetry`](https://python-poetry.org/) / [`pdm`](https://pdm-project.org/):

<CodeGroup>
  ```bash uv theme={null}
  uv add origoid
  ```

  ```bash poetry theme={null}
  poetry add origoid
  ```

  ```bash pdm theme={null}
  pdm add origoid
  ```
</CodeGroup>

Paquete en PyPI: [`origoid`](https://pypi.org/project/origoid/).
Código en [github.com/origoid/sdk-python](https://github.com/origoid/sdk-python)
(público, para auditar). Requiere Python **3.9 o más reciente**.

## Inicializa el cliente

```python theme={null}
import os
from origoid import OrigoID

client = OrigoID(api_key=os.environ["ORIGOID_API_KEY"])
```

Ese es todo el setup — no hay nada más que configurar. Nunca
hardcodees la API key. Cárgala desde `os.environ`, un `.env` via
[`python-dotenv`](https://pypi.org/project/python-dotenv/), o un
secrets manager (HashiCorp Vault, 1Password, Doppler, etc.).

## Tu primera llamada

El ejemplo usa `PELJ900101HDFRRN09`, un **CURP sintético de los
ejemplos del OpenAPI** — no es CURP de persona real. Reemplázalo con
el CURP que necesites validar.

```python theme={null}
from origoid import OrigoID

client = OrigoID(api_key="...")

envelope = client.renapo.validate_curp(
    curp="PELJ900101HDFRRN09",  # sintético — reemplazar con input real
)

if envelope.status == "OK" and envelope.type == "SUCCESS":
    # envelope.data trae el registro RENAPO.
    print("Titular del CURP:", envelope.data)
else:
    # Otro tipo de resultado — revisa el catálogo de respuestas del endpoint para tu lógica.
    print(envelope.type, "—", envelope.message)
```

Cada método devuelve una instancia `Envelope`: `{ status, type,
message, data, transaction_id, processed_at, billable, errors? }`.
Ver [Envelope de respuesta](/es/envelope) para el contrato.

## Métodos por recurso

Los métodos están agrupados por dominio regulatorio. Los nombres usan
`snake_case` Python.

### `client.authentication`

```python theme={null}
client.authentication.issue_token(expire_after=1800)
```

### `client.renapo`

```python theme={null}
client.renapo.validate_curp(curp="PELJ900101HDFRRN09")
client.renapo.lookup_curp(
    given_names="JUAN",
    first_surname="PEREZ",
    second_surname="LOPEZ",
    date_of_birth="1990-01-01",
    gender="H",
    birth_state_code="DF",
)
```

### `client.sat`

`extract_csf` y `validate_cfdi` aceptan un dict `request=` porque los
schemas subyacentes permiten shapes alternativas (identificadores
directos O upload de documento). Dentro del dict usa los nombres
camelCase del wire JSON:

```python theme={null}
client.sat.validate_rfc(rfc="PEZJ811011KI1")

client.sat.extract_csf(request={
    "rfc": "PEZJ811011KI1",
    "cif": "17060597619",
})

client.sat.validate_cfdi(request={
    "uuid": "7C8BD4EA-AE86-4CB5-88B8-C6E61E988A8B",
    "rfcEmisor": "PEZJ811011KI1",
    "rfcReceptor": "EMP170623KI3",
    "total": "999999.99",
})
```

### `client.imss`

```python theme={null}
client.imss.lookup_nss(curp="PELJ900101HDFRRN09")
client.imss.get_employment_status(
    curp="PELJ900101HDFRRN09",
    nss="92038109713",
)
```

### `client.ine`

```python theme={null}
client.ine.validate_voter_list(request={
    "cic": "123456789",
    "citizenIdentifier": "987654321",
})

client.ine.extract_voter_id_data(
    front="<jpg base64>",
    back="<jpg base64>",
)

client.ine.extract_qr_data(back="<jpg base64 del reverso>")
```

### `client.compliance`

```python theme={null}
client.compliance.search_sat69(request={"rfc": "PEZJ811011KI1"})
client.compliance.search_sat69b(request={"rfc": "PEZJ811011KI1"})
client.compliance.search_ofac(name="John Doe", min_similarity_score=85)
client.compliance.search_peps(request={
    "givenNames": "JUAN",
    "firstSurname": "PEREZ",
    "secondSurname": "LOPEZ",
})
```

### `client.biometrics`

```python theme={null}
client.biometrics.match_faces(
    face="<selfie base64>",
    front="<frente id base64>",
    threshold=80,
    document_type="INE",
)
client.biometrics.check_liveness(selfie="<selfie base64>")
```

### `client.email`

```python theme={null}
client.email.validate_email(email="user@example.com")
```

### `client.proof_of_address`

```python theme={null}
client.proof_of_address.extract_proof_of_address(file="<pdf o jpg base64>")
```

## Cliente async

Para aplicaciones sobre [FastAPI](https://fastapi.tiangolo.com/),
[aiohttp](https://docs.aiohttp.org/), [Starlette](https://www.starlette.io/),
u otros frameworks async, usa `AsyncOrigoID`. Mismos nombres de
método, mismos argumentos, mismos tipos de retorno — sólo agregas
`await` para que la llamada no bloquee el event loop mientras el HTTP
request está en vuelo.

```python theme={null}
import asyncio
from origoid import AsyncOrigoID

async def main():
    client = AsyncOrigoID(api_key="...")
    env = await client.renapo.validate_curp(curp="PELJ900101HDFRRN09")
    if env.status == "OK":
        print(env.type)

asyncio.run(main())
```

Para lanzar varias llamadas en paralelo (ej. validar varios CURPs al
mismo tiempo), usa `asyncio.gather` — el mismo patrón que
`Promise.all` en JavaScript:

```python theme={null}
results = await asyncio.gather(
    client.renapo.validate_curp(curp="PELJ900101HDFRRN09"),
    client.sat.validate_rfc(rfc="PEZJ811011KI1"),
    client.compliance.search_ofac(name="John Doe"),
)
```

Si tu aplicación no está construida sobre un stack async, usa
`OrigoID` — async sólo aporta cuando necesitas manejar muchas
operaciones concurrentes sin spawnear threads.

## Manejo de errores

El SDK distingue entre **errores de negocio** (vienen dentro del
envelope) y **errores de transporte** (se lanzan como excepciones).

### Errores de negocio — inspecciona el envelope

Para cualquier respuesta HTTP 200, incluyendo `INVALID_REQUEST`, el
SDK devuelve un `Envelope`. Revisa `status` y `type` antes de usar
`data`:

```python theme={null}
env = client.renapo.validate_curp(curp="BAD")

if env.status == "ERROR":
    # INVALID_REQUEST, SERVICE_UNAVAILABLE, …
    print(env.type, env.message)
    for err in (env.errors or []):
        print(f"  {err.field}: {err.code} — {err.message}")
else:
    # status == "OK" — puede ser un NOT_FOUND de negocio
    match env.type:
        case "SUCCESS":
            print("Titular del CURP:", env.data)
        case "CURP_NOT_FOUND":
            print("Sin match")
```

Ver [Errores](/es/errors) y [Catálogos](/es/catalogs) para el
catálogo completo de códigos `type`.

### Errores de transporte — `try`/`except`

Para `401`, `429` y fallas irrecuperables el SDK lanza excepciones
tipadas:

```python theme={null}
from origoid import OrigoID
from origoid.core.api_error import ApiError

client = OrigoID(api_key="...")

try:
    env = client.renapo.validate_curp(curp="PELJ900101HDFRRN09")
except ApiError as e:
    # e.status_code — 401, 429, etc.
    # e.body — envelope que devolvió el servidor
    print(e.status_code, e.body)
```

## Configuración por llamada (avanzado)

Pasa `request_options` para override de timeout, retries o headers por
llamada:

```python theme={null}
client.ine.validate_voter_list(
    request={"cic": "123456789", "citizenIdentifier": "987654321"},
    request_options={
        "timeout_in_seconds": 120,   # default 60
        "max_retries": 3,            # default 2
        "additional_headers": {"x-trace-id": "..."},
    },
)
```

<Info>
  **Lee esto antes de tunear timeouts o retries.** El SDK sólo
  reintenta errores `5xx` y fallas de red, nunca respuestas de negocio
  exitosas — entonces los retries **no** crean llamadas duplicadas
  facturables cuando el API respondió correctamente. **Sí** crean
  llamadas extra cuando el request realmente falló: un request que
  hace timeout tres veces puede consumir tres créditos si la llamada
  eventualmente tuvo éxito en un intento posterior.

  * **Los defaults (60 s, 2 reintentos) son correctos para casi
    cualquier workload.** Cambia sólo con razón específica.
  * Combinar timeout largo con `max_retries` alto (ej. 120 s × 5)
    significa que un único request fallando puede ocupar un thread
    hasta 10 minutos — malo para tu throughput y tu infraestructura.
  * Sobrescribe per-call sólo en endpoints con cold starts lentos
    conocidos.
</Info>
