Sincronizar clientes com CRM ou ERP
O cliente existe no seu CRM ou ERP e no Faturei Hoje. Este guia mostra como levar o cadastro de um lado para o outro sem criar cliente repetido.
São dois sentidos, e cada um tem a sua ferramenta:
| Sentido | Como |
|---|---|
| Do seu sistema para o Faturei | Procurar, depois criar ou editar. É o que esta página mostra |
| Do Faturei para o seu sistema | Os eventos client.created, client.updated e client.deleted avisam na hora, e a sincronização incremental garante que nada ficou para trás |
A chave
Seção intitulada “A chave”| Permissão | Para quê |
|---|---|
clients:read | Procurar o cliente antes de gravar |
clients:create | Cadastrar quem não existe |
clients:update | Atualizar quem já existe |
Guarde o id do Faturei no seu sistema
Seção intitulada “Guarde o id do Faturei no seu sistema”O jeito mais seguro de ligar os dois cadastros é o id. Na primeira vez que você achar ou criar o cliente, guarde o id do Faturei Hoje no registro do seu sistema. Daí em diante, edite direto por ele, com PATCH /public/v1/clients/{id}, sem procurar de novo.
Procurar por e-mail, telefone ou documento serve para a primeira ligação, quando você ainda não tem o id.
Procurar antes de criar
Seção intitulada “Procurar antes de criar”POST /public/v1/clients não confere se o cliente já existe: duas chamadas com o mesmo e-mail criam dois cadastros. Então procure primeiro. GET /public/v1/clients aceita três filtros de cadastro exato:
| Filtro | Como compara |
|---|---|
email | E-mail igual, sem diferenciar maiúscula de minúscula |
phone | O mesmo telefone, com ou sem o nono dígito. Telefone inválido responde 422 |
cpf_cnpj | Os mesmos dígitos, com ou sem pontuação |
Há também search, a mesma busca da tela de clientes, por nome, e-mail, telefone ou documento. Ela é boa para gente procurando, não para decidir sozinha se o cliente é o mesmo.
A base pode ter cadastro repetido de antes da integração. Se a busca trouxer mais de um, não escolha no escuro: registre o caso e resolva com a empresa.
Criar ou atualizar
Seção intitulada “Criar ou atualizar”# 1. Procurar pelo e-mailcurl "https://api.fatureihoje.com/public/v1/clients?email=contato%40marcenariasouza.com.br&limit=2" \ -H "Authorization: Bearer $FH_API_KEY"
# 2a. Não achou: criar# Gere a chave uma vez e guarde: numa nova tentativa, repita com o mesmo 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. Achou: atualizar pelo id que veio na buscaCLIENT_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;}
// O cliente como está no seu 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',};
// Uma chave por operação, gerada antes da primeira tentativa. Numa nova// tentativa, reuse a mesma: é ela que faz a API devolver a resposta guardada// em vez de gravar de novo.const createKey = randomUUID();
// limit=2: com dois resultados, a base tem cadastro repetido.const found = await call('GET', `/clients?email=${encodeURIComponent(crm.email)}&limit=2`);if (found.data.length > 1) throw new Error('Mais de um cadastro com este e-mail: resolva com a empresa antes de gravar.');
const client = found.data.length === 1 ? await call('PATCH', `/clients/${found.data[0].id}`, fields) : await call('POST', '/clients', fields, createKey);
// Guarde client.id no registro do seu 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;}
// O cliente como está no seu 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',];
// Uma chave por operação, gerada antes da primeira tentativa. Numa nova// tentativa, reuse a mesma: é ela que faz a API devolver a resposta guardada// em vez de gravar de novo.$createKey = bin2hex(random_bytes(16));
// limit=2: com dois resultados, a base tem cadastro repetido.$found = call('GET', '/clients?email=' . rawurlencode($crm['email']) . '&limit=2');if (count($found['data']) > 1) { throw new RuntimeException('Mais de um cadastro com este e-mail: resolva com a empresa antes de gravar.');}
$client = count($found['data']) === 1 ? call('PATCH', '/clients/' . $found['data'][0]['id'], $fields) : call('POST', '/clients', $fields, $createKey);
// Guarde o id no registro do seu 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']}")
# O cliente como está no seu 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",}
# Uma chave por operação, gerada antes da primeira tentativa. Numa nova# tentativa, reuse a mesma: é ela que faz a API devolver a resposta guardada em# vez de gravar de novo.create_key = str(uuid.uuid4())
# limit=2: com dois resultados, a base tem cadastro repetido.query = urllib.parse.urlencode({"email": crm["email"], "limit": 2})found = call("GET", f"/clients?{query}")if len(found["data"]) > 1: raise SystemExit("Mais de um cadastro com este e-mail: resolva com a empresa antes de gravar.")
if found["data"]: client = call("PATCH", f"/clients/{found['data'][0]['id']}", fields)else: client = call("POST", "/clients", fields, create_key)
# Guarde o id no registro do seu CRM.print(client["id"])O cadastro aceita os mesmos campos do formulário do painel, inclusive os fiscais (razão social, inscrições, endereço fiscal). A lista completa, com formato e tamanho de cada um, está na Referência.
Quando a chamada falha
Seção intitulada “Quando a chamada falha”| Resposta | O que fazer |
|---|---|
422 validation_failed | Corrija o campo apontado em errors (um telefone inválido no filtro, por exemplo) |
404 resource_not_found | O cliente do PATCH não existe mais, ou não é da empresa |
429 rate_limit_exceeded | Espere o Retry-After e repita. Veja Limites de uso |
| 5xx ou falha de rede | Repita; no POST, com a mesma Idempotency-Key |
Mande só o que o seu sistema manda
Seção intitulada “Mande só o que o seu sistema manda”O PATCH altera só os campos que vierem na chamada. Campo que não vem fica exatamente como está, e null limpa um campo opcional.
Use isso a seu favor: mande só os campos que o seu sistema é dono. Se a equipe cuida das observações pelo painel e o seu ERP cuida do documento e do endereço fiscal, o ERP nunca manda notes, e ninguém apaga o trabalho do outro.
Evite o laço de atualização
Seção intitulada “Evite o laço de atualização”Quando os dois sentidos estão ligados, é fácil criar um laço: o seu sistema atualiza o cliente, o Faturei Hoje manda client.updated, o seu sistema grava de novo, e assim por diante.
- Antes de gravar, compare. Se o valor que chegou é igual ao que você já tem, não grave nem chame a API.
- Num
client.updated, ochanged_fieldsdiz o que mudou. Se são só campos que o seu sistema acabou de mandar, com os mesmos valores, o evento é o eco da sua própria gravação.
Endereços
Seção intitulada “Endereços”Cliente com endereço já nasce com o endereço “Principal” na lista de endereços. Endereços a mais (obra, filial, casa de praia) ficam em GET /public/v1/clients/{client_id}/addresses e POST /public/v1/clients/{client_id}/addresses.
Excluir
Seção intitulada “Excluir”DELETE /public/v1/clients/{id} apaga o cadastro de vez, como no painel, com endereços, observações, vínculos de grupo, acesso ao portal, inventário e obras. Ordens de serviço, vendas, orçamentos, tarefas, lançamentos, contratos e notas fiscais continuam existindo, sem o cliente. Não tem volta.
Se no seu sistema “excluir” quer dizer “não é mais cliente”, talvez o que você quer seja status: "inactive" num PATCH. A decisão é sua; a API faz o que você pedir.
Próximo passo
Seção intitulada “Próximo passo”- Sincronização incremental com
updated_after: o outro sentido, do Faturei Hoje para o seu sistema. - Idempotência: por que o
POSTlevaIdempotency-Keye oPATCHnão precisa.