Criar OS a partir de outro sistema
Um chamado entra no seu sistema de atendimento, na sua central de monitoramento ou no seu ERP, e precisa virar uma ordem de serviço para a equipe de campo. Este guia abre a OS pela API e mostra como acompanhar o que acontece com ela depois.
A OS criada pela API é igual à criada pelo painel: conta nas cotas do plano, ganha o número da empresa, tem o total calculado e aplica os checklists automáticos que a empresa configurou.
A chave
Seção intitulada “A chave”| Permissão | Para quê |
|---|---|
service_orders:read | Ler os tipos de OS e acompanhar a OS |
service_orders:create | Criar a OS |
service_orders:update | Mudar a situação, iniciar, concluir e cancelar |
team:read | Opcional. Achar o técnico responsável |
appointments:create | Só se a OS for criar o compromisso na agenda (create_appointment) |
finance:create | Só se a conclusão for lançar a receita (link_financial_transaction) |
quotes:read | Só para converter orçamento em OS (from_quote). O membro também precisa de acesso a orçamentos |
Antes da primeira OS: o que a empresa exige
Seção intitulada “Antes da primeira OS: o que a empresa exige”Três leituras resolvem quase todo 422 antes de ele acontecer. Faça uma vez e guarde; elas mudam pouco.
- Tipos de OS.
GET /public/v1/service_order_typeslista os tipos cadastrados (CFTV, alarme, portão). Oidde cada um é o valor detype_id. Quandorequired_on_createvemtrue, a empresa exige tipo, e criar semtype_idresponde 422. Tipo comactive: falsesaiu do seletor do painel. - Técnicos.
GET /public/v1/teamtraz a equipe. Os técnicos vêm comtype: "technician", e oiddeles vai emtechnician_ide emsupport_technician_ids. - Cliente. A OS pode apontar para um cadastro (
client_id), e aí ele precisa ser da empresa. Se você ainda não liga os clientes dos dois sistemas, veja Sincronizar clientes com CRM ou ERP.
A empresa também pode marcar, na configuração de OS do painel, campos obrigatórios na abertura. A API cobra os mesmos campos, e o que faltar volta em errors no 422.
Criar a OS
Seção intitulada “Criar a OS”# 1. Os tipos de OS da empresa (guarde; muda pouco)curl "https://api.fatureihoje.com/public/v1/service_order_types?limit=100" \ -H "Authorization: Bearer $FH_API_KEY"
# 2. Criar a OS. A chave de idempotência vem do número do chamado.curl -X POST "https://api.fatureihoje.com/public/v1/service_orders" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: chamado-4821" \ -d '{ "client_id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", "client_name": "Marcenaria Souza", "client_phone": "+5511988887777", "client_description": "O portão eletrônico não fecha até o fim.", "service_address": "Rua das Flores, 120, São Paulo, SP", "type_id": "7b1e4c2a-9d3f-4a6b-8c5e-1f2a3b4c5d6e", "scheduled_start": "2026-10-05T12:00:00Z", "scheduled_end": "2026-10-05T14:00:00Z", "priority": "high", "internal_notes": "Chamado 4821 do sistema de atendimento", "items": [ { "description": "Visita técnica", "quantity": 1, "unit_price_cents": 15000, "total_cents": 15000 } ] }'const API = 'https://api.fatureihoje.com/public/v1';const auth = { Authorization: `Bearer ${process.env.FH_API_KEY}` };
// O chamado como está no seu sistema.const ticket = { number: 4821, clientId: '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', // id do Faturei que você guardou clientName: 'Marcenaria Souza', phone: '+5511988887777', problem: 'O portão eletrônico não fecha até o fim.', address: 'Rua das Flores, 120, São Paulo, SP', kind: 'Portão',};
async function json(res) { const body = await res.json(); if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`); return body;}
// 1. Os tipos de OS da empresa. Em produção, leia uma vez e guarde.const types = await json(await fetch(`${API}/service_order_types?limit=100`, { headers: auth }));const type = types.data.find((t) => t.active && t.name === ticket.kind);
// 2. Criar a OS.const order = await json( await fetch(`${API}/service_orders`, { method: 'POST', headers: { ...auth, 'Content-Type': 'application/json', // Mesmo chamado, mesma chave: reenviar não abre outra OS. 'Idempotency-Key': `chamado-${ticket.number}`, }, body: JSON.stringify({ client_id: ticket.clientId, client_name: ticket.clientName, client_phone: ticket.phone, client_description: ticket.problem, service_address: ticket.address, type_id: type ? type.id : null, scheduled_start: '2026-10-05T12:00:00Z', scheduled_end: '2026-10-05T14:00:00Z', priority: 'high', internal_notes: `Chamado ${ticket.number} do sistema de atendimento`, items: [{ description: 'Visita técnica', quantity: 1, unit_price_cents: 15000, total_cents: 15000 }], }), }),);
// Guarde order.id no chamado.console.log(order.id, order.status);<?php
const API = 'https://api.fatureihoje.com/public/v1';
function request(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 chamado como está no seu sistema.$ticket = [ 'number' => 4821, 'client_id' => '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', // id do Faturei que você guardou 'client_name' => 'Marcenaria Souza', 'phone' => '+5511988887777', 'problem' => 'O portão eletrônico não fecha até o fim.', 'address' => 'Rua das Flores, 120, São Paulo, SP', 'kind' => 'Portão',];
// 1. Os tipos de OS da empresa. Em produção, leia uma vez e guarde.$typeId = null;foreach (request('GET', '/service_order_types?limit=100')['data'] as $type) { if ($type['active'] && $type['name'] === $ticket['kind']) { $typeId = $type['id']; }}
// 2. Criar a OS. Mesmo chamado, mesma chave: reenviar não abre outra OS.$order = request('POST', '/service_orders', [ 'client_id' => $ticket['client_id'], 'client_name' => $ticket['client_name'], 'client_phone' => $ticket['phone'], 'client_description' => $ticket['problem'], 'service_address' => $ticket['address'], 'type_id' => $typeId, 'scheduled_start' => '2026-10-05T12:00:00Z', 'scheduled_end' => '2026-10-05T14:00:00Z', 'priority' => 'high', 'internal_notes' => 'Chamado ' . $ticket['number'] . ' do sistema de atendimento', 'items' => [ ['description' => 'Visita técnica', 'quantity' => 1, 'unit_price_cents' => 15000, 'total_cents' => 15000], ],], 'chamado-' . $ticket['number']);
// Guarde o id no chamado.echo $order['id'], ' ', $order['status'], PHP_EOL;import jsonimport osimport urllib.errorimport urllib.request
API = "https://api.fatureihoje.com/public/v1"
def request(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 req = urllib.request.Request(API + path, data=data, method=method, headers=headers) try: with urllib.request.urlopen(req) 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 chamado como está no seu sistema.ticket = { "number": 4821, "client_id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", # id do Faturei que você guardou "client_name": "Marcenaria Souza", "phone": "+5511988887777", "problem": "O portão eletrônico não fecha até o fim.", "address": "Rua das Flores, 120, São Paulo, SP", "kind": "Portão",}
# 1. Os tipos de OS da empresa. Em produção, leia uma vez e guarde.types = request("GET", "/service_order_types?limit=100")["data"]type_id = next((t["id"] for t in types if t["active"] and t["name"] == ticket["kind"]), None)
# 2. Criar a OS. Mesmo chamado, mesma chave: reenviar não abre outra OS.order = request( "POST", "/service_orders", { "client_id": ticket["client_id"], "client_name": ticket["client_name"], "client_phone": ticket["phone"], "client_description": ticket["problem"], "service_address": ticket["address"], "type_id": type_id, "scheduled_start": "2026-10-05T12:00:00Z", "scheduled_end": "2026-10-05T14:00:00Z", "priority": "high", "internal_notes": f"Chamado {ticket['number']} do sistema de atendimento", "items": [ {"description": "Visita técnica", "quantity": 1, "unit_price_cents": 15000, "total_cents": 15000} ], }, f"chamado-{ticket['number']}",)
# Guarde o id no chamado.print(order["id"], order["status"])O que vale saber sobre os campos (a lista inteira está na Referência):
client_nameé obrigatório mesmo comclient_id: é o nome como sai na OS, e continua lá se o cadastro for excluído.- Com
scheduled_starta OS nascescheduled; sem ele,pending. - Dinheiro vai em centavos. O painel guarda o
total_centsde cada item como você mandou, sem recalcular pela quantidade, e o subtotal da OS é a soma deles. Veja Datas e dinheiro. - A OS não tem campo para o id do seu sistema. Guarde o
idda OS no seu chamado e, se quiser que a equipe veja o número do chamado, ponha eminternal_notes, que não sai no documento do cliente.
A chave de idempotência vem do chamado
Seção intitulada “A chave de idempotência vem do chamado”No exemplo, a Idempotency-Key é chamado-4821. Assim, se o seu sistema mandar o mesmo chamado duas vezes (um retry de fila, um operador clicando de novo), a segunda chamada devolve a OS da primeira em vez de abrir outra.
Dois cuidados:
- A resposta fica guardada por 24 horas. Depois disso, a mesma chave cria de novo. Por isso guarde o
idda OS no chamado assim que ela voltar, e confira esse campo antes de criar. - A mesma chave com um corpo diferente responde 422
idempotency_key_reused. Se o chamado mudou antes de a OS ser criada, é outra operação.
Veja Idempotência.
Os efeitos que você pode ligar
Seção intitulada “Os efeitos que você pode ligar”São os mesmos do formulário do painel, e desligados por padrão:
send_to_technician: trueavisa o técnico responsável e a equipe de apoio pelo WhatsApp depois de criar. Semtechnician_id, ninguém é avisado.create_appointment: true, junto comscheduled_start, cria o compromisso na agenda e o espelha no Google Agenda de quem conectou. Pedeappointments:createna chave.
Depois de criada
Seção intitulada “Depois de criada”A OS anda pelas mesmas ações dos botões do painel:
| Ação | Rota |
|---|---|
| Mudar a situação (qualquer uma para qualquer outra) | POST /public/v1/service_orders/{id}/status |
| Iniciar | POST /public/v1/service_orders/{id}/start |
| Concluir, com a opção de lançar a receita | POST /public/v1/service_orders/{id}/complete |
| Cancelar | POST /public/v1/service_orders/{id}/cancel |
| Editar | PATCH /public/v1/service_orders/{id} |
OS concluída ou cancelada não aceita edição, início, conclusão nem cancelamento: a resposta é 409 conflict. Para reabrir, mude a situação pela rota de situação. Concluir respeita o checklist obrigatório e a ordem de conclusão do técnico, e responde 409 quando falta algo.
Para saber quando o técnico iniciou ou concluiu, não fique consultando a OS. Inscreva um endpoint em service_order.status_changed e service_order.updated (veja Criar um endpoint), e use a sincronização incremental como rede de segurança.
Orçamento aprovado vira OS
Seção intitulada “Orçamento aprovado vira OS”Se o seu fluxo começa num orçamento do Faturei Hoje, não recrie os itens na mão: POST /public/v1/service_orders/from_quote converte um orçamento aprovado em OS, pela mesma conversão da janela do painel. Ela pede quotes:read na chave e acesso a orçamentos para o membro da chave.
Quando a chamada falha
Seção intitulada “Quando a chamada falha”| Resposta | O que fazer |
|---|---|
422 validation_failed | Campo inválido ou obrigatório da empresa faltando. O campo vem em errors |
404 resource_not_found | Cliente ou técnico que não existe ou não é da empresa |
403 plan_limit_reached | A cota de OS do plano acabou. A empresa resolve no painel |
403 permission_missing | Falta permissão na chave, como appointments:create com create_appointment |
429 rate_limit_exceeded | Espere o Retry-After e repita. Veja Limites de uso |
| 5xx ou falha de rede | Repita com a mesma Idempotency-Key |
Próximo passo
Seção intitulada “Próximo passo”- Lançar vendas e receber pagamentos: o lado financeiro do serviço.
- Catálogo de eventos: o que sai em cada evento de OS.