Crear y reutilizar clientes
Un cliente guarda los datos del receptor, la persona o empresa que recibe tus facturas. Crearlo una vez permite usar su customerId en las siguientes ventas. El emisor sigue siendo el RFC configurado en tu cuenta.
Necesitas customer:read para consultar y customer:write para crear. Si aún no conectaste tu sistema, empieza por Tu primera solicitud.
1. Reúne los datos fiscales
Sección titulada «1. Reúne los datos fiscales»| Dato del cliente | Campo de la API | Cómo obtenerlo |
|---|---|---|
| Nombre o razón social fiscal | legalName |
El nombre registrado ante el SAT, no un nombre comercial |
| RFC | rfc |
El RFC del receptor |
| Código postal fiscal | postalCode |
El domicilio fiscal del receptor, no la dirección de entrega |
| Régimen fiscal | taxRegime |
La clave del régimen aplicable a la factura |
Solicita estos datos al cliente. Puede consultarlos en su información fiscal, pero no condiciones la factura a que entregue su Constancia de Situación Fiscal. Orientación del SAT sobre los datos para facturar.
email, whatsappPhone, idioma y moneda son datos adicionales admitidos. El uso de CFDI se guarda en cada factura mediante cfdiUse, porque puede cambiar entre operaciones.
Esta guía cubre receptores nacionales con RFC propio. Los casos de receptor extranjero o público en general tienen reglas fiscales adicionales; no sustituyas sus datos por los de este ejemplo.
2. Busca antes de crear
Sección titulada «2. Busca antes de crear»Si ya guardaste el customerId, consulta GET /customers/{id}. Si no lo tienes, busca por RFC:
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
curl --fail-with-body --request GET \ --url "$FACTUCAT_API_URL/customers?query=$RECEPTOR_RFC&limit=50&offset=0" \ --header "X-API-Key: $FACTUCAT_API_KEY"const baseUrl = process.env.FACTUCAT_API_URL || "https://sandbox.factucat.com/api/v1"const rfc = process.env.RECEPTOR_RFCconst response = await fetch(`${baseUrl}/customers?query=${rfc}&limit=50&offset=0`, { method: "GET", headers: { 'X-API-Key': process.env.FACTUCAT_API_KEY, },})const text = await response.text()if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`)console.log(JSON.parse(text))import jsonimport osimport urllib.errorimport urllib.request
base_url = os.environ.get("FACTUCAT_API_URL", "https://sandbox.factucat.com/api/v1")rfc = os.environ["RECEPTOR_RFC"]request = urllib.request.Request( f"{base_url}/customers?query={rfc}&limit=50&offset=0", method="GET", headers={ "X-API-Key": os.environ["FACTUCAT_API_KEY"], },)try: with urllib.request.urlopen(request) as response: text = response.read().decode("utf-8")except urllib.error.HTTPError as error: raise RuntimeError(f"HTTP {error.code}: {error.read().decode('utf-8')}") from errorprint(json.loads(text))package main
import ( "fmt" "io" "net/http" "os")
func main() { baseURL := os.Getenv("FACTUCAT_API_URL") if baseURL == "" { baseURL = "https://sandbox.factucat.com/api/v1" } rfc := os.Getenv("RECEPTOR_RFC") request, err := http.NewRequest("GET", baseURL + "/customers?query=" + rfc + "&limit=50&offset=0", nil) if err != nil { panic(err) } request.Header.Set("X-API-Key", os.Getenv("FACTUCAT_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() text, _ := io.ReadAll(response.Body) if response.StatusCode < 200 || response.StatusCode >= 300 { panic(fmt.Sprintf("HTTP %d: %s", response.StatusCode, text)) } fmt.Print(string(text))}Define RECEPTOR_RFC con el RFC real antes de ejecutar la búsqueda. Los RFC y los ID de estas rutas ya son valores URL-safe; los ejemplos los interpolan sin codificar. data es un arreglo. El filtro busca coincidencias parciales por RFC o nombre: compara el RFC completo y no tomes el primer resultado sin revisarlo. Si meta.pagination.hasMore es verdadero, continúa con la siguiente página. Cómo paginar.
Guarda la relación entre tu cliente y el data[].id correspondiente. Ese ID pertenece a tu cuenta y ambiente. Para continuar el tutorial con un cliente existente, define CUSTOMER_ID con ese ID y ejecuta esta consulta. Guarda su respuesta en cliente-respuesta.json, igual que al crear uno nuevo:
FACTUCAT_API_URL="${FACTUCAT_API_URL:-https://sandbox.factucat.com/api/v1}"
curl --fail-with-body --request GET \ --url "$FACTUCAT_API_URL/customers/$CUSTOMER_ID" \ --header "X-API-Key: $FACTUCAT_API_KEY" \ --output 'cliente-respuesta.json'import { readFile, writeFile } from 'node:fs/promises'
const baseUrl = process.env.FACTUCAT_API_URL || "https://sandbox.factucat.com/api/v1"const id = process.env.CUSTOMER_IDconst response = await fetch(`${baseUrl}/customers/${id}`, { method: "GET", headers: { 'X-API-Key': process.env.FACTUCAT_API_KEY, },})const text = await response.text()if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`)await writeFile("cliente-respuesta.json", text)import jsonimport osfrom pathlib import Pathimport urllib.errorimport urllib.request
base_url = os.environ.get("FACTUCAT_API_URL", "https://sandbox.factucat.com/api/v1")id = os.environ["CUSTOMER_ID"]request = urllib.request.Request( f"{base_url}/customers/{id}", method="GET", headers={ "X-API-Key": os.environ["FACTUCAT_API_KEY"], },)try: with urllib.request.urlopen(request) as response: text = response.read().decode("utf-8")except urllib.error.HTTPError as error: raise RuntimeError(f"HTTP {error.code}: {error.read().decode('utf-8')}") from errorPath("cliente-respuesta.json").write_text(text, encoding="utf-8")package main
import ( "fmt" "io" "net/http" "os")
func main() { baseURL := os.Getenv("FACTUCAT_API_URL") if baseURL == "" { baseURL = "https://sandbox.factucat.com/api/v1" } id := os.Getenv("CUSTOMER_ID") request, err := http.NewRequest("GET", baseURL + "/customers/" + id, nil) if err != nil { panic(err) } request.Header.Set("X-API-Key", os.Getenv("FACTUCAT_API_KEY")) response, err := http.DefaultClient.Do(request) if err != nil { panic(err) } defer response.Body.Close() text, _ := io.ReadAll(response.Body) if response.StatusCode < 200 || response.StatusCode >= 300 { panic(fmt.Sprintf("HTTP %d: %s", response.StatusCode, text)) } _ = os.WriteFile("cliente-respuesta.json", text, 0600)}3. Crea el cliente cuando no exista
Sección titulada «3. Crea el cliente cuando no exista»Prepara cliente.json con los datos reales reunidos en el primer paso. El tutorial de CFDI completo incluye el body y la solicitud listos para adaptar. POST /customers devuelve 201, con el cliente creado en data.
Usa una Idempotency-Key que identifique esa alta y consérvala hasta confirmar el resultado. Si la respuesta se pierde, reintenta la misma solicitud con la misma llave y clave de idempotencia.
FactuCat no permite dos clientes con el mismo RFC en la misma cuenta emisora. Si POST /customers responde 409 con error.code: "conflict" y error.reason: "duplicate_rfc", el cliente ya existe: búscalo y guarda su ID. error.reason es opcional y no viene en todos los errores 409. No confundas este caso con error.code: "idempotency_conflict", que indica que reutilizaste una clave de idempotencia con otro body. El mismo RFC sí puede existir en cuentas emisoras distintas, aunque sean del mismo propietario.
{ "error": { "code": "conflict", "message": "Ya existe un cliente con ese RFC", "requestId": "identificador-de-la-solicitud", "retryable": false, "reason": "duplicate_rfc" }}Crear un cliente no timbra ni valida sus datos ante el SAT. Tampoco genera consumo de timbres.
4. Reutiliza sus datos en el borrador
Sección titulada «4. Reutiliza sus datos en el borrador»Al enviar POST /invoice-drafts con customerId, FactuCat copia los datos al receptor del nuevo borrador. Revisa customerId, receiverRfc, receiverLegalName, receiverPostalCode y receiverTaxRegime en la respuesta.
No necesitas volver a crear al cliente para cada venta. Continúa con Crear el borrador y timbrar.
Si los datos cambian
Sección titulada «Si los datos cambian»La API pública permite listar, consultar y crear clientes; no expone una operación para editar o eliminar su registro.
Para corregir el receptor de una factura todavía editable, usa PATCH /invoice-drafts/{id}/receiver con los campos receiver* correspondientes. Afecta al borrador, no al cliente guardado ni a facturas emitidas. Cambiar solo customerId en ese PATCH no copia los datos fiscales: envíalos explícitamente.
Si conservas un cliente con datos anteriores, proporciona los receiver* actualizados al crear los siguientes borradores. No supongas que corregir una factura actualiza todo el historial del cliente.