Sincronizar clientes con un CRM o ERP
El cliente existe en su CRM o ERP y en Faturei Hoje. Esta guía muestra cómo llevar el registro de un lado al otro sin crear clientes repetidos.
Son dos sentidos, y cada uno tiene su herramienta:
| Sentido | Cómo |
|---|---|
| De su sistema a Faturei | Buscar, y después crear o editar. Es lo que muestra esta página |
| De Faturei a su sistema | Los eventos client.created, client.updated y client.deleted avisan en el momento, y la sincronización incremental garantiza que nada quedó atrás |
La clave
Sección titulada «La clave»| Permiso | Para qué |
|---|---|
clients:read | Buscar el cliente antes de grabar |
clients:create | Registrar a quien no existe |
clients:update | Actualizar a quien ya existe |
Guarde el id de Faturei en su sistema
Sección titulada «Guarde el id de Faturei en su sistema»La forma más segura de vincular los dos registros es el id. La primera vez que encuentre o cree el cliente, guarde el id de Faturei Hoje en el registro de su sistema. De ahí en adelante, edite directamente por él, con PATCH /public/v1/clients/{id}, sin buscar de nuevo.
Buscar por correo, teléfono o documento sirve para el primer vínculo, cuando todavía no tiene el id.
Buscar antes de crear
Sección titulada «Buscar antes de crear»POST /public/v1/clients no verifica si el cliente ya existe: dos llamadas con el mismo correo crean dos registros. Así que busque primero. GET /public/v1/clients acepta tres filtros de coincidencia exacta:
| Filtro | Cómo compara |
|---|---|
email | El mismo correo, sin distinguir mayúsculas de minúsculas |
phone | El mismo teléfono, con o sin el noveno dígito. Un teléfono inválido responde 422 |
cpf_cnpj | Los mismos dígitos, con o sin puntuación |
También está search, la misma búsqueda de la pantalla de clientes, por nombre, correo, teléfono o documento. Sirve para personas que buscan, no para decidir sola si el cliente es el mismo.
La base puede tener registros repetidos de antes de la integración. Si la búsqueda trae más de uno, no elija a ciegas: registre el caso y resuélvalo con la empresa.
Crear o actualizar
Sección titulada «Crear o actualizar»# 1. Buscar por el correocurl "https://api.fatureihoje.com/public/v1/clients?email=contato%40marcenariasouza.com.br&limit=2" \ -H "Authorization: Bearer $FH_API_KEY"
# 2a. No lo encontró: crear# Genere la clave una vez y guárdela: en un reintento, repita con el mismo valor.CREATE_KEY=$(uuidgen)curl -X POST "https://api.fatureihoje.com/public/v1/clients" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CREATE_KEY" \ -d '{ "name": "Marcenaria Souza", "email": "contato@marcenariasouza.com.br", "phone": "+5511988887777", "cpf_cnpj": "12345678000190", "person_type": "pj" }'
# 2b. Lo encontró: actualizar por el id que vino en la búsquedaCLIENT_ID="0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d"curl -X PATCH "https://api.fatureihoje.com/public/v1/clients/$CLIENT_ID" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "phone": "+5511988887777" }'import { randomUUID } from 'node:crypto';
const API = 'https://api.fatureihoje.com/public/v1';
async function call(method, path, body, idempotencyKey) { const headers = { Authorization: `Bearer ${process.env.FH_API_KEY}` }; if (body !== undefined) headers['Content-Type'] = 'application/json'; if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
const res = await fetch(`${API}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }); const data = await res.json(); if (!res.ok) { throw new Error(`${res.status} ${data.error.code}: ${data.error.message}`); } return data;}
// El cliente como está en su CRM.const crm = { name: 'Marcenaria Souza', email: 'contato@marcenariasouza.com.br', phone: '+5511988887777', document: '12345678000190',};
const fields = { name: crm.name, email: crm.email, phone: crm.phone, cpf_cnpj: crm.document, person_type: 'pj',};
// Una clave por operación, generada antes del primer intento. En un reintento,// reutilícela: es lo que hace que la API devuelva la respuesta guardada en vez// de grabar de nuevo.const createKey = randomUUID();
// limit=2: con dos resultados, la base tiene un registro repetido.const found = await call('GET', `/clients?email=${encodeURIComponent(crm.email)}&limit=2`);if (found.data.length > 1) throw new Error('Más de un registro con este correo: resuélvalo con la empresa antes de grabar.');
const client = found.data.length === 1 ? await call('PATCH', `/clients/${found.data[0].id}`, fields) : await call('POST', '/clients', fields, createKey);
// Guarde client.id en el registro de su CRM.console.log(client.id);<?php
const API = 'https://api.fatureihoje.com/public/v1';
function call(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null): array{ $headers = ['Authorization: Bearer ' . getenv('FH_API_KEY')]; if ($body !== null) { $headers[] = 'Content-Type: application/json'; } if ($idempotencyKey !== null) { $headers[] = 'Idempotency-Key: ' . $idempotencyKey; }
$ch = curl_init(API . $path); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, ]); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); }
$data = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($status >= 400) { throw new RuntimeException($status . ' ' . $data['error']['code'] . ': ' . $data['error']['message']); } return $data;}
// El cliente como está en su CRM.$crm = [ 'name' => 'Marcenaria Souza', 'email' => 'contato@marcenariasouza.com.br', 'phone' => '+5511988887777', 'document' => '12345678000190',];
$fields = [ 'name' => $crm['name'], 'email' => $crm['email'], 'phone' => $crm['phone'], 'cpf_cnpj' => $crm['document'], 'person_type' => 'pj',];
// Una clave por operación, generada antes del primer intento. En un reintento,// reutilícela: es lo que hace que la API devuelva la respuesta guardada en vez// de grabar de nuevo.$createKey = bin2hex(random_bytes(16));
// limit=2: con dos resultados, la base tiene un registro repetido.$found = call('GET', '/clients?email=' . rawurlencode($crm['email']) . '&limit=2');if (count($found['data']) > 1) { throw new RuntimeException('Más de un registro con este correo: resuélvalo con la empresa antes de grabar.');}
$client = count($found['data']) === 1 ? call('PATCH', '/clients/' . $found['data'][0]['id'], $fields) : call('POST', '/clients', $fields, $createKey);
// Guarde el id en el registro de su CRM.echo $client['id'], PHP_EOL;import jsonimport osimport urllib.errorimport urllib.parseimport urllib.requestimport uuid
API = "https://api.fatureihoje.com/public/v1"
def call(method, path, body=None, idempotency_key=None): headers = {"Authorization": f"Bearer {os.environ['FH_API_KEY']}"} data = None if body is not None: headers["Content-Type"] = "application/json" data = json.dumps(body).encode() if idempotency_key: headers["Idempotency-Key"] = idempotency_key
request = urllib.request.Request(API + path, data=data, method=method, headers=headers) try: with urllib.request.urlopen(request) as response: return json.load(response) except urllib.error.HTTPError as failure: error = json.load(failure)["error"] raise SystemExit(f"{failure.code} {error['code']}: {error['message']}")
# El cliente como está en su CRM.crm = { "name": "Marcenaria Souza", "email": "contato@marcenariasouza.com.br", "phone": "+5511988887777", "document": "12345678000190",}
fields = { "name": crm["name"], "email": crm["email"], "phone": crm["phone"], "cpf_cnpj": crm["document"], "person_type": "pj",}
# Una clave por operación, generada antes del primer intento. En un reintento,# reutilícela: es lo que hace que la API devuelva la respuesta guardada en vez# de grabar de nuevo.create_key = str(uuid.uuid4())
# limit=2: con dos resultados, la base tiene un registro repetido.query = urllib.parse.urlencode({"email": crm["email"], "limit": 2})found = call("GET", f"/clients?{query}")if len(found["data"]) > 1: raise SystemExit("Más de un registro con este correo: resuélvalo con la empresa antes de grabar.")
if found["data"]: client = call("PATCH", f"/clients/{found['data'][0]['id']}", fields)else: client = call("POST", "/clients", fields, create_key)
# Guarde el id en el registro de su CRM.print(client["id"])El registro acepta los mismos campos del formulario del panel, incluidos los fiscales (razón social, inscripciones, dirección fiscal). La lista completa, con el formato y el tamaño de cada uno, está en la Referencia.
Cuando la llamada falla
Sección titulada «Cuando la llamada falla»| Respuesta | Qué hacer |
|---|---|
422 validation_failed | Corrija el campo indicado en errors (un teléfono inválido en el filtro, por ejemplo) |
404 resource_not_found | El cliente del PATCH ya no existe, o no es de la empresa |
429 rate_limit_exceeded | Espere el Retry-After y repita. Vea Límites de uso |
| 5xx o falla de red | Repita; en el POST, con la misma Idempotency-Key |
Envíe solo lo que su sistema maneja
Sección titulada «Envíe solo lo que su sistema maneja»El PATCH cambia solo los campos que vienen en la llamada. Un campo que no viene queda exactamente como está, y null limpia un campo opcional.
Úselo a su favor: envíe solo los campos de los que su sistema es dueño. Si el equipo cuida las observaciones en el panel y su ERP cuida el documento y la dirección fiscal, el ERP nunca envía notes, y nadie borra el trabajo del otro.
Evite el bucle de actualizaciones
Sección titulada «Evite el bucle de actualizaciones»Con los dos sentidos activos, es fácil crear un bucle: su sistema actualiza el cliente, Faturei Hoje envía client.updated, su sistema graba de nuevo, y así sucesivamente.
- Antes de grabar, compare. Si el valor que llegó es igual al que ya tiene, no grabe ni llame a la API.
- En un
client.updated,changed_fieldsdice qué cambió. Si son solo campos que su sistema acaba de enviar, con los mismos valores, el evento es el eco de su propia escritura.
Direcciones
Sección titulada «Direcciones»Un cliente creado con dirección ya nace con la dirección “Principal” en la lista de direcciones. Las direcciones adicionales (obra, sucursal, casa de playa) están en GET /public/v1/clients/{client_id}/addresses y POST /public/v1/clients/{client_id}/addresses.
Eliminar
Sección titulada «Eliminar»DELETE /public/v1/clients/{id} elimina el registro para siempre, como en el panel, con direcciones, observaciones, vínculos de grupo, acceso al portal, inventario y obras. Las órdenes de servicio, ventas, presupuestos, tareas, movimientos, contratos y facturas siguen existiendo, sin el cliente. No se puede deshacer.
Si en su sistema “eliminar” significa “ya no es cliente”, tal vez lo que busca sea status: "inactive" en un PATCH. La decisión es suya; la API hace lo que usted pida.
Próximo paso
Sección titulada «Próximo paso»- Sincronización incremental con
updated_after: el otro sentido, de Faturei Hoje a su sistema. - Idempotencia: por qué el
POSTllevaIdempotency-Keyy elPATCHno lo necesita.