Skip to main content

cord-sdk (Python)

SDK de Python para la API de Cord. Sin dependencias, Python 3.10+, con tipos y cliente async.

pip install cord-sdk
pip install 'cord-sdk[async]'   # AsyncCord, con httpx
from cord import Cord

cord = Cord("sk_test_...")
quote = cord.quotes.create(
    cliente={"empresa": "Acme"},
    items=[{"descripcion": "Instalación", "cantidad": 1, "precio_unitario": 12500, "tax_rate": 0.16}],
    base_currency="MXN",
)
print(quote["folio"], quote["link_publico"])

for invoice in cord.invoices.list_all(estado="open"):
    print(invoice["numero"], invoice["saldo"], invoice["moneda"])
  • Toda mutación lleva Idempotency-Key, la misma en cada reintento: un corte de red nunca duplica nada. Pasa idempotency_key= (siempre por nombre) para atarla a tu propio identificador.
  • list_all() recorre por cursor: no salta ni repite registros aunque alguien escriba mientras recorres. Acepta los filtros de list() y limit= como tamaño de cada página.
  • Los errores son CordError con status, code, request_id, doc_url, headers y retry_after. Un error de tu código, como un TypeError, sale tal cual y no se reintenta.
  • Manda Cord-Version: 2026-10-01; cámbiala con Cord(sk, api_version=...).
  • Un Decimal se manda como número y un date o datetime como fecha ISO 8601, en el cuerpo y en los filtros. Los tipos siguen la spec (float y str), salvo record_payment y credit_note, que aceptan Decimal.

Reintentos

  • Si la respuesta trae Cord-Should-Retry, manda ese header: false no se reintenta (una cuota agotada, el tope de envíos de una factura, un rechazo del PAC al emitir) y true sí.
  • Sin él se reintenta la red caída (ConnectionError, TimeoutError o URLError), 408, 425 y 5xx. Un 429 solo si es de ritmo (code rate_limited) o llega sin code, como el de un proxy; uno de cuota agotada (api_quota_exceeded, ai_quota_exceeded) sale de inmediato.
  • Se espera lo que diga Retry-After (segundos o fecha HTTP) o un backoff exponencial, y todas las esperas de una llamada suman como mucho 60 s. Si la siguiente no cabe, sale el CordError, con retry_after para que decidas tú cuándo volver.
  • 409 idempotency_in_progress (la misma clave sigue en curso) no gasta reintentos: el SDK vuelve a preguntar hasta recibir la respuesta guardada, así un finalize lento termina con la factura. max_retries=0 apaga todo reintento.
  • Nunca sigue una redirección: un 3xx es CordError con code unexpected_redirect y la dirección a la que apunta. Revisa base_url (la de producción es https://cordhq.app).

Opciones por llamada, respuesta y rutas sin método

cord = Cord("sk_test_...", app_info={"name": "mi-erp", "version": "4.2.0"})  # User-Agent: cord-python/1.1.0 mi-erp/4.2.0

rapido = cord.with_options(timeout=5.0, max_retries=0)
rapido.invoices.finalize(factura_id)
print(cord.last_response.request_id)  # búscalo en Desarrolladores › Registros

r = cord.raw_request("GET", "/cotizaciones", query={"status": "sent", "limit": 5})
print(r.data, r.meta, r.request_id)
  • with_options(timeout=, max_retries=) devuelve un cliente con esas opciones que comparte transporte y last_response con el original.
  • last_response es un CordResponse (status, headers, request_id, idempotent_replayed, api_version) de la última llamada exitosa de ese hilo o esa tarea de asyncio.
  • raw_request(method, path, query=, body=, idempotency_key=, headers=) llama una ruta que el SDK aún no tiene como método, con la llave, Cord-Version, la idempotencia, los reintentos y los errores ya puestos, y devuelve un RawResponse. La ruta es relativa a /api/v1 (/api/v1/… también vale); una URL absoluta, //host, . o .. son un ValueError: la llave nunca sale a otro host.

Tipos

Cada respuesta es un TypedDict de cord.types, generado de la spec OpenAPI con los mismos nombres que el SDK de Node (Quote, QuoteDetail, Invoice, InvoiceDetail, Client, Product…). Los argumentos de create, update, list y list_all también están tipados: mypy o Pyright marcan un campo mal escrito antes de llamar a Cord.

from cord.types import InvoiceDetail

factura: InvoiceDetail = cord.invoices.retrieve(factura_id)
print(factura["saldo"], factura["moneda"])

Async

from cord import AsyncCord

async with AsyncCord("sk_test_...") as cord:
    quote = await cord.quotes.create(items=[{"descripcion": "Instalación", "cantidad": 1, "precio_unitario": 12500}])
    async for invoice in cord.invoices.list_all(estado="open"):
        print(invoice["numero"])

AsyncCord tiene los mismos métodos que Cord, con la misma idempotencia y los mismos reintentos. Usa httpx; con transport= puedes pasar el tuyo (una función async que recibe method, url, headers, body, timeout y devuelve status, headers, body).

Recursos

Atributo Métodos
cord.me(), cord.collections(), cord.create_task(...) GET /me, GET /cobranza, POST /tareas
cord.quotes list, list_all, retrieve, create, update, send, resend, approve, reject, mark_paid, delete, draft_from_text
cord.clients list, list_all, retrieve, create, update
cord.products list, list_all, retrieve, create, update, delete
cord.invoices list, list_all, retrieve, create, finalize, send, void, record_payment, credit_note
cord.events list, list_all
cord.workflows invoke
cord.setup propose, retrieve
cord.elements config
cord.webhook_endpoints list, create, update, rotate_secret, delete
cord.test_helpers reset, set_next_fiscal_outcome, view_quote, expire_quote, trigger_webhook (solo sk_test_)
cord.webhooks construct_event, construct_workflow_delivery, verify_signature, sign_payload
factura = cord.invoices.create(cliente_id=cliente_id, items=[...], currency="MXN")
# Un CFDI a un cliente extranjero sin mercancías: confírmalo al emitir.
cord.invoices.finalize(factura["id"], sin_comercio_exterior=True)

cord.workflows.invoke("wf_…", "invoice", [factura["id"]])

plan = cord.setup.propose(sitio="tunegocio.com", descripcion="Instalaciones eléctricas, cobro a 30 días")
print(plan["review_url"])  # una persona la aprueba ahí

cord.webhook_endpoints.update(endpoint_id, activo=False)  # lo pausa
nuevo = cord.webhook_endpoints.rotate_secret(endpoint_id, 72)
print(nuevo["secret"], nuevo["secreto_anterior_expira"])  # el anterior sigue firmando 72 horas

Webhooks

Verifica siempre sobre el cuerpo crudo, tal como llegó.

Flask:

import os
from flask import Flask, request
from cord import construct_event, WebhookSignatureError

app = Flask(__name__)

@app.post("/webhooks/cord")
def cord_webhook():
    try:
        event = construct_event(request.get_data(), request.headers, os.environ["CORD_WEBHOOK_SECRET"])
    except WebhookSignatureError:
        return "", 400
    if event["event"] == "invoice.paid":
        ...
    return "ok"

FastAPI:

import os
from fastapi import FastAPI, Request, Response
from cord import construct_event, WebhookSignatureError

app = FastAPI()

@app.post("/webhooks/cord")
async def cord_webhook(request: Request) -> Response:
    try:
        event = construct_event(await request.body(), request.headers, os.environ["CORD_WEBHOOK_SECRET"])
    except WebhookSignatureError:
        return Response(status_code=400)
    if event["event"] == "invoice.paid":
        ...
    return Response("ok")

En Django, el cuerpo crudo es request.body y la vista va con @csrf_exempt.

Usa el cuerpo crudo. Acepta X-Cord-Signature-V1 o Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature), ambas con timestamp (5 minutos de tolerancia, tolerance=); con las dos basta con que cuadre una. La firma legacy solo con allow_legacy_signature=True. Deduplica por event["id"].

WebhookSignatureError.code: missing_secret, missing_signature, invalid_signature_header, timestamp_out_of_tolerance, signature_mismatch, legacy_signature_rejected o invalid_payload.

El POST de un workflow

La acción "Mandar los datos a una URL" de Cord Workflows firma su POST igual, pero su cuerpo es { workflow, workflow_id, evento, objeto, objeto_id, enviado_en, mensaje?, datos }:

from cord import construct_workflow_delivery

envio = construct_workflow_delivery(request.get_data(), request.headers, os.environ["CORD_WORKFLOW_SECRET"])
print(envio["evento"], envio["datos"])

verify_signature(payload, headers, secret) solo comprueba la firma, sin leer el cuerpo.

Modo prueba

cord = Cord("sk_test_...")
cord.test_helpers.set_next_fiscal_outcome("pac_caido")
cord.test_helpers.trigger_webhook("invoice.paid")
cord.test_helpers.reset()  # vacía el entorno de prueba; conserva llaves y endpoints

Metadata

Release files for cord-sdk 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cord-sdk 1.1.0
File Size Uploaded Provenance Actions
cord_sdk-1.1.0.tar.gz 35.3 kB With provenance Details for cord_sdk-1.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cord-sdk 1.1.0
File Interpreter ABI Platform Actions
cord_sdk-1.1.0-py3-none-any.whl Python 3 none any Details for cord_sdk-1.1.0-py3-none-any.whl

Total release size: 73.5 kB

Release files / cord_sdk-1.1.0.tar.gz

Download URL cord_sdk-1.1.0.tar.gz
Size 35.3 kB
Tags Source
SHA-256 checksum
How to use checksums
67493134b66566a24c78c20928c0e65c6a759fd020bd2e334e859d7b7b662d40
BLAKE2b-256 checksum
How to use checksums
38b9a514c39e141dc4dda916012d09d76da985207d23770810c57896860a5db6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Release files / cord_sdk-1.1.0-py3-none-any.whl

Download URL cord_sdk-1.1.0-py3-none-any.whl
Size 38.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fb20a53494d0ff34959668833064e453e3cd80c46de7c1fc8ee5b9f128bc480c
BLAKE2b-256 checksum
How to use checksums
b0c6e1f7356d1c73093ea23240e5a612785556086804eaa084989c47035d3902
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

Security for cord-sdk 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cord_sdk-1.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page