Skip to main content
The Python SDK ships with both synchronous and asynchronous clients, typed response models (built on Pydantic, the standard Python data-validation library), full type hints, and one strongly-typed method per OrigoID endpoint.

Install

pip install origoid
Or with uv / poetry / pdm:
uv add origoid
Package on PyPI: origoid. Source on github.com/origoid/sdk-python (public, for auditing). Requires Python 3.9 or newer.

Initialize the client

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, 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.
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 for the contract.

Methods by resource

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

client.authentication

client.authentication.issue_token(expire_after=1800)

client.renapo

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):
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

client.imss.lookup_nss(curp="PELJ900101HDFRRN09")
client.imss.get_employment_status(
    curp="PELJ900101HDFRRN09",
    nss="92038109713",
)

client.ine

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

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

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

client.email.validate_email(email="user@example.com")

client.proof_of_address

client.proof_of_address.extract_proof_of_address(file="<base64 pdf or jpg>")

Async client

For applications built on FastAPI, aiohttp, Starlette, 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.
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:
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:
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 and Catalogs for the full catalog of type codes.

Transport errors — try/except

For 401, 429, and unrecoverable failures the SDK raises typed exceptions:
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:
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": "..."},
    },
)
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.