Empezar

Convenciones de la API

URL base, formato de errores, paginación por cursor, request ids y tipos de dato.

URL base

https://facty.mx/api/authenticated/{orgSlug}/...

{orgSlug} es el identificador de tu organización en Facty (lo ves en la URL de la app y en la respuesta de /context). Debe coincidir con la organización de tu llave, si no: 403 ORG_MISMATCH.

Hay dos rutas sin organización, porque sirven datos de referencia del SAT que son públicos:

GET /api/authenticated/catalogs/sat?type=...
GET /api/authenticated/catalogs/clave-unidad?q=...
GET /api/authenticated/catalogs/clave-prod-serv?q=...

Petición

  • Los cuerpos van en JSON: Content-Type: application/json.
  • Los identificadores de Facty (clientId, invoiceId, productId, paymentId, accountId, empleadoId) son cuids — cadenas como clx9aa3k80000abcd1234efgh. No son numéricos ni UUIDs.
  • El uuid de un CFDI (folio fiscal del SAT) sí es un UUID.
  • Los importes se mandan como números, no como cadenas: 10000.5, no "10,000.50".
  • Las tasas de impuesto van en decimal, no en porcentaje: 0.16 es 16%.
  • Las fechas de periodo van como "YYYY-MM-DD". Las marcas de tiempo que devolvemos son ISO-8601 en UTC.

Respuesta

Los 2xx devuelven JSON. Los códigos que usamos:

CódigoCuándo
200Lectura correcta, o escritura que resolvió a un registro existente (upsert).
201Se creó algo nuevo: una factura timbrada, un cliente, un producto.

Dos endpoints devuelven binario en vez de JSON: /invoices/{id}/cfdi (un ZIP con el XML y el PDF) y /invoices/{id}/acuse (el PDF del acuse de cancelación).

Errores

Todo 4xx y 5xx sale con la misma envoltura, para que un cliente que no es un navegador nunca tenga que comparar contra el texto en español (que nos reservamos el derecho de reescribir):

{
  "error": "Forbidden: falta el permiso `invoices.create`.",
  "code": "MISSING_SCOPE",
  "requestId": "0f9c2a1b…",
  "required": "invoices.create"
}
  • code es estable y legible por máquina. Ramifica sobre esto.
  • requestId coincide con el header X-Request-Id. Cítalo al reportar un problema.
  • Los 422 traen además fieldErrors, un mapa plano de ruta → mensaje, listo para pintar junto al campo culpable:
{
  "error": "Datos inválidos.",
  "code": "VALIDATION_FAILED",
  "requestId": "8de1…",
  "fieldErrors": {
    "items.0.claveProdServ": "Clave SAT inválida (8 dígitos)",
    "usoCfdi": "Uso CFDI inválido"
  }
}

La lista completa de códigos y qué hacer con cada uno está en Errores.

X-Request-Id

Toda respuesta lleva el header X-Request-Id. Si tú mandas uno en la petición, te lo devolvemos tal cual, para que puedas correlacionar tus logs con los nuestros:

curl -s "$FACTY_HOST/api/authenticated/$FACTY_ORG/invoices" \
  -H "X-API-Key: $FACTY_KEY" \
  -H "X-Request-Id: erp-job-2026-08-28-0042" -D -

Guárdalo junto a cada operación fiscal que registres de tu lado. Es lo primero que te vamos a pedir en soporte.

Paginación

Las listas paginan por cursor, no por número de página: en un catálogo que se está escribiendo al mismo tiempo, un ?page=2 se salta o repite registros.

GET /clients?limit=50
GET /clients?limit=50&cursor=clx9aa…

La respuesta trae nextCursor. Cuando es null, llegaste al final.

{
  "items": [ /* … */ ],
  "nextCursor": "clx9zz…"
}

Límites de limit:

RecursoMáximoDefault
/clients, /products, /empleados50050
/invoices, /payments, /retenciones10025

Recorrer una lista completa:

cursor=""
while :; do
  resp=$(curl -s "$FACTY_HOST/api/authenticated/$FACTY_ORG/invoices?limit=100${cursor:+&cursor=$cursor}" \
    -H "X-API-Key: $FACTY_KEY")
  echo "$resp" | jq -c '.invoices[]'
  cursor=$(echo "$resp" | jq -r '.nextCursor // empty')
  [ -z "$cursor" ] && break
done

Impuestos: cómo se expresan

CampoSignificadoEjemplo
ivaTasa de IVA trasladado. Solo 0, 0.08 o 0.16.0.16
ivaExentoBooleano. El acto es objeto del impuesto pero no lo causa (educación, servicios médicos, casa habitación). Excluyente con una tasa de iva distinta de cero.true
iepsTasa de IEPS.0.08
iepsCuotaIEPS de cuota: pesos por unidad de medida, no un porcentaje. Excluyente con ieps.1.6
retIsr, retIva, retIepsTasas de retención. retIsr tope 0.35.0.106667
descuentoTasa de descuento sobre el concepto. 0.10 es 10%.0.10

ivaExento no es "IVA en cero": son cosas distintas ante el SAT. Cero es una tasa (TipoFactor Tasa, TasaOCuota 0.000000); exento es otro TipoFactor, sin tasa ni importe. Usar uno por el otro es un CFDI mal emitido aunque el total cuadre.

Fechas y zona horaria

Los CFDI se timbran contra la hora del PAC. Si mandas una fecha con offset, se normaliza a UTC antes de enviarla. Para campos de periodo (nómina, retenciones) usa "YYYY-MM-DD" a secas y evítate la ambigüedad.