> ## 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.

# Python SDK

> `origoid` — official client for Python 3.9+. Sync, async, typed response models.

The Python SDK ships with both synchronous and asynchronous clients,
typed response models (built on [Pydantic](https://docs.pydantic.dev/),
the standard Python data-validation library), full type hints, and one
strongly-typed method per OrigoID endpoint.

## Install

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

Or with [`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>

Package on PyPI: [`origoid`](https://pypi.org/project/origoid/). Source
on [github.com/origoid/sdk-python](https://github.com/origoid/sdk-python)
(public, for auditing). Requires Python **3.9 or newer**.

## Initialize the client

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

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

That's the whole setup — there is nothing else to configure. Never
hardcode the API key. Load from `os.environ`, a `.env` file via
[`python-dotenv`](https://pypi.org/project/python-dotenv/), or a
secrets manager (HashiCorp Vault, 1Password, Doppler, etc.).

## Your first call

The example below uses `PELJ900101HDFRRN09`, a **synthetic CURP from
the OpenAPI examples** — not a real person's CURP. Replace it with the
CURP you need to validate.

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

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

envelope = client.renapo.validate_curp(
    curp="PELJ900101HDFRRN09",  # synthetic — replace with real input
)

if envelope.status == "OK" and envelope.type == "SUCCESS":
    # envelope.data holds the RENAPO record.
    print("CURP holder:", envelope.data)
else:
    # Other result type — check the endpoint's response catalog to drive your logic.
    print(envelope.type, "—", envelope.message)
```

Every method returns an `Envelope` instance: `{ status, type, message,
data, transaction_id, processed_at, billable, errors? }`. See
[Response envelope](/en/envelope) for the contract.

## Methods by resource

Methods are grouped by regulatory domain. Names use Python
`snake_case`.

### `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` and `validate_cfdi` accept a `request=` dict because the
underlying schemas allow alternative shapes (direct identifiers OR a
document upload). Inside that dict use the JSON wire names
(camelCase):

```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="<base64 jpg>",
    back="<base64 jpg>",
)

client.ine.extract_qr_data(back="<base64 jpg of credential back>")
```

### `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="<base64 selfie>",
    front="<base64 id front>",
    threshold=80,
    document_type="INE",
)
client.biometrics.check_liveness(selfie="<base64 selfie>")
```

### `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="<base64 pdf or jpg>")
```

## Async client

For applications built on [FastAPI](https://fastapi.tiangolo.com/),
[aiohttp](https://docs.aiohttp.org/), [Starlette](https://www.starlette.io/),
or other async frameworks, use `AsyncOrigoID`. Same method names, same
arguments, same return types — you just `await` each call so it does
not block the event loop while the HTTP request is in flight.

```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())
```

To fire several calls in parallel (e.g. validating multiple CURPs at
once), use `asyncio.gather` — the same pattern as `Promise.all` in
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"),
)
```

If your application is not built on an async stack, use `OrigoID` —
async only adds value when you need to handle many concurrent
operations without spawning threads.

## Error handling

The SDK distinguishes between **business errors** (returned inside the
envelope) and **transport errors** (raised as exceptions).

### Business errors — inspect the envelope

For any HTTP 200, including `INVALID_REQUEST`, the SDK returns an
`Envelope`. Check `status` and `type` before using `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" — could still be a business-level NOT_FOUND
    match env.type:
        case "SUCCESS":
            print("CURP holder:", env.data)
        case "CURP_NOT_FOUND":
            print("No match")
```

See [Errors](/en/errors) and [Catalogs](/en/catalogs) for the full
catalog of `type` codes.

### Transport errors — `try`/`except`

For `401`, `429`, and unrecoverable failures the SDK raises typed
exceptions:

```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 returned by the server
    print(e.status_code, e.body)
```

## Per-call configuration (advanced)

Pass `request_options` to override timeout, retries, or headers per
call:

```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>
  **Read this before tuning timeouts or retries.** The SDK only retries
  `5xx` and network failures, never successful business responses — so
  retries do **not** create duplicate billable calls when the API
  responded correctly. They **do** create extra calls when the request
  actually failed: a request that times out three times can consume
  three credits if the call eventually succeeded on a later attempt.

  * **Defaults (60 s, 2 retries) are right for almost every workload.**
    Change only with a specific reason.
  * Combining a long timeout with high `max_retries` (e.g. 120 s × 5)
    means a single failing request can occupy a thread for up to 10
    minutes — bad for your throughput and your infrastructure.
  * Override per-call only on endpoints with known slow cold starts.
</Info>
