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. Pasaidempotency_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 delist()ylimit=como tamaño de cada página.- Los errores son
CordErrorconstatus,code,request_id,doc_url,headersyretry_after. Un error de tu código, como unTypeError, sale tal cual y no se reintenta. - Manda
Cord-Version: 2026-10-01; cámbiala conCord(sk, api_version=...). - Un
Decimalse manda como número y undateodatetimecomo fecha ISO 8601, en el cuerpo y en los filtros. Los tipos siguen la spec (floatystr), salvorecord_paymentycredit_note, que aceptanDecimal.
Reintentos
- Si la respuesta trae
Cord-Should-Retry, manda ese header:falseno se reintenta (una cuota agotada, el tope de envíos de una factura, un rechazo del PAC al emitir) ytruesí. - Sin él se reintenta la red caída (
ConnectionError,TimeoutErroroURLError), 408, 425 y 5xx. Un 429 solo si es de ritmo (coderate_limited) o llega sincode, 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 elCordError, conretry_afterpara 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í unfinalizelento termina con la factura.max_retries=0apaga todo reintento.- Nunca sigue una redirección: un 3xx es
CordErrorconcodeunexpected_redirecty la dirección a la que apunta. Revisabase_url(la de producción eshttps://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 ylast_responsecon el original.last_responsees unCordResponse(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 unRawResponse. La ruta es relativa a/api/v1(/api/v1/…también vale); una URL absoluta,//host,.o..son unValueError: 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.