Crear OS desde otro sistema
Un ticket entra en su sistema de atención, en su central de monitoreo o en su ERP, y tiene que convertirse en una orden de servicio para el equipo de campo. Esta guía abre la OS por la API y muestra cómo seguir lo que pasa con ella después.
La OS creada por la API es igual a la creada en el panel: cuenta en las cuotas del plan, recibe el número de la empresa, tiene el total calculado y aplica los checklists automáticos que la empresa configuró.
La clave
Sección titulada «La clave»| Permiso | Para qué |
|---|---|
service_orders:read | Leer los tipos de OS y seguir la OS |
service_orders:create | Crear la OS |
service_orders:update | Cambiar el estado, iniciar, concluir y cancelar |
team:read | Opcional. Encontrar al técnico responsable |
appointments:create | Solo si la OS va a crear la cita en la agenda (create_appointment) |
finance:create | Solo si la conclusión va a registrar el ingreso (link_financial_transaction) |
quotes:read | Solo para convertir un presupuesto en OS (from_quote). El miembro también necesita acceso a presupuestos |
Antes de la primera OS: lo que exige la empresa
Sección titulada «Antes de la primera OS: lo que exige la empresa»Tres lecturas evitan casi todo 422 antes de que ocurra. Hágalas una vez y guárdelas; cambian poco.
- Tipos de OS.
GET /public/v1/service_order_typeslista los tipos registrados (CCTV, alarma, portón). Elidde cada uno es el valor detype_id. Cuandorequired_on_createvienetrue, la empresa exige tipo, y crear sintype_idresponde 422. Un tipo conactive: falsesalió del selector del panel. - Técnicos.
GET /public/v1/teamtrae el equipo. Los técnicos vienen contype: "technician", y suidva entechnician_idy ensupport_technician_ids. - Cliente. La OS puede apuntar a un registro (
client_id), y en ese caso tiene que ser de la empresa. Si todavía no vincula los clientes de los dos sistemas, vea Sincronizar clientes con un CRM o ERP.
La empresa también puede marcar, en la configuración de OS del panel, campos obligatorios al abrir. La API exige los mismos campos, y lo que falte vuelve en errors en el 422.
Crear la OS
Sección titulada «Crear la OS»# 1. Los tipos de OS de la empresa (guárdelos; cambian poco)curl "https://api.fatureihoje.com/public/v1/service_order_types?limit=100" \ -H "Authorization: Bearer $FH_API_KEY"
# 2. Crear la OS. La clave de idempotencia sale del número del ticket.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: ticket-4821" \ -d '{ "client_id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", "client_name": "Marcenaria Souza", "client_phone": "+5511988887777", "client_description": "El portón eléctrico no cierra del todo.", "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": "Ticket 4821 del sistema de atención", "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}` };
// El ticket como está en su sistema.const ticket = { number: 4821, clientId: '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', // id de Faturei que usted guardó clientName: 'Marcenaria Souza', phone: '+5511988887777', problem: 'El portón eléctrico no cierra del todo.', address: 'Rua das Flores, 120, São Paulo, SP', kind: 'Portón',};
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. Los tipos de OS de la empresa. En producción, léalos una vez y guárdelos.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. Crear la OS.const order = await json( await fetch(`${API}/service_orders`, { method: 'POST', headers: { ...auth, 'Content-Type': 'application/json', // Mismo ticket, misma clave: reenviar no abre otra OS. 'Idempotency-Key': `ticket-${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: `Ticket ${ticket.number} del sistema de atención`, items: [{ description: 'Visita técnica', quantity: 1, unit_price_cents: 15000, total_cents: 15000 }], }), }),);
// Guarde order.id en el ticket.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;}
// El ticket como está en su sistema.$ticket = [ 'number' => 4821, 'client_id' => '0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d', // id de Faturei que usted guardó 'client_name' => 'Marcenaria Souza', 'phone' => '+5511988887777', 'problem' => 'El portón eléctrico no cierra del todo.', 'address' => 'Rua das Flores, 120, São Paulo, SP', 'kind' => 'Portón',];
// 1. Los tipos de OS de la empresa. En producción, léalos una vez y guárdelos.$typeId = null;foreach (request('GET', '/service_order_types?limit=100')['data'] as $type) { if ($type['active'] && $type['name'] === $ticket['kind']) { $typeId = $type['id']; }}
// 2. Crear la OS. Mismo ticket, misma clave: reenviar no abre otra 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' => 'Ticket ' . $ticket['number'] . ' del sistema de atención', 'items' => [ ['description' => 'Visita técnica', 'quantity' => 1, 'unit_price_cents' => 15000, 'total_cents' => 15000], ],], 'ticket-' . $ticket['number']);
// Guarde el id en el ticket.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']}")
# El ticket como está en su sistema.ticket = { "number": 4821, "client_id": "0d9a7c5e-3f1b-4d2a-8c6e-4a2b0c8e6f1d", # id de Faturei que usted guardó "client_name": "Marcenaria Souza", "phone": "+5511988887777", "problem": "El portón eléctrico no cierra del todo.", "address": "Rua das Flores, 120, São Paulo, SP", "kind": "Portón",}
# 1. Los tipos de OS de la empresa. En producción, léalos una vez y guárdelos.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. Crear la OS. Mismo ticket, misma clave: reenviar no abre otra 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"Ticket {ticket['number']} del sistema de atención", "items": [ {"description": "Visita técnica", "quantity": 1, "unit_price_cents": 15000, "total_cents": 15000} ], }, f"ticket-{ticket['number']}",)
# Guarde el id en el ticket.print(order["id"], order["status"])Lo que conviene saber de los campos (la lista completa está en la Referencia):
client_namees obligatorio aun conclient_id: es el nombre como sale en la OS, y sigue ahí si el registro se elimina.- Con
scheduled_startla OS nacescheduled; sin él,pending. - El dinero va en centavos. El panel guarda el
total_centsde cada ítem tal como usted lo envió, sin recalcular por la cantidad, y el subtotal de la OS es la suma de ellos. Vea Fechas y dinero. - La OS no tiene un campo para el id de su sistema. Guarde el
idde la OS en su ticket y, si quiere que el equipo vea el número del ticket, póngalo eninternal_notes, que no sale en el documento del cliente.
La clave de idempotencia sale del ticket
Sección titulada «La clave de idempotencia sale del ticket»En el ejemplo, la Idempotency-Key es ticket-4821. Así, si su sistema envía el mismo ticket dos veces (un reintento de cola, un operador que vuelve a hacer clic), la segunda llamada devuelve la OS de la primera en vez de abrir otra.
Dos cuidados:
- La respuesta queda guardada 24 horas. Después de eso, la misma clave vuelve a crear. Por eso, guarde el
idde la OS en el ticket apenas vuelva, y revise ese campo antes de crear. - La misma clave con un cuerpo diferente responde 422
idempotency_key_reused. Si el ticket cambió antes de que se creara la OS, es otra operación.
Vea Idempotencia.
Los efectos que puede activar
Sección titulada «Los efectos que puede activar»Son los mismos del formulario del panel, y vienen desactivados:
send_to_technician: trueavisa al técnico responsable y al equipo de apoyo por WhatsApp después de crear. Sintechnician_id, no se avisa a nadie.create_appointment: true, junto conscheduled_start, crea la cita en la agenda y la refleja en el Google Calendar de quien lo haya conectado. Pideappointments:createen la clave.
Después de creada
Sección titulada «Después de creada»La OS avanza con las mismas acciones de los botones del panel:
| Acción | Ruta |
|---|---|
| Cambiar el estado (de cualquiera a cualquiera) | POST /public/v1/service_orders/{id}/status |
| Iniciar | POST /public/v1/service_orders/{id}/start |
| Concluir, con la opción de registrar el ingreso | POST /public/v1/service_orders/{id}/complete |
| Cancelar | POST /public/v1/service_orders/{id}/cancel |
| Editar | PATCH /public/v1/service_orders/{id} |
Una OS concluida o cancelada no acepta edición, inicio, conclusión ni cancelación: la respuesta es 409 conflict. Para reabrirla, cambie el estado por la ruta de estado. Concluir respeta el checklist obligatorio y el orden de conclusión del técnico, y responde 409 cuando falta algo.
Para saber cuándo el técnico inició o concluyó, no consulte la OS una y otra vez. Suscriba un endpoint a service_order.status_changed y service_order.updated (vea Crear un endpoint), y use la sincronización incremental como red de seguridad.
Un presupuesto aprobado se convierte en OS
Sección titulada «Un presupuesto aprobado se convierte en OS»Si su flujo empieza en un presupuesto de Faturei Hoje, no vuelva a cargar los ítems a mano: POST /public/v1/service_orders/from_quote convierte un presupuesto aprobado en OS, con la misma conversión de la ventana del panel. Pide quotes:read en la clave y acceso a presupuestos para el miembro de la clave.
Cuando la llamada falla
Sección titulada «Cuando la llamada falla»| Respuesta | Qué hacer |
|---|---|
422 validation_failed | Un campo inválido, o falta un campo que la empresa exige. El campo viene en errors |
404 resource_not_found | Un cliente o técnico que no existe o no es de la empresa |
403 plan_limit_reached | Se agotó la cuota de OS del plan. La empresa lo resuelve en el panel |
403 permission_missing | Le falta un permiso a la clave, como appointments:create con create_appointment |
429 rate_limit_exceeded | Espere el Retry-After y repita. Vea Límites de uso |
| 5xx o falla de red | Repita con la misma Idempotency-Key |
Próximo paso
Sección titulada «Próximo paso»- Registrar ventas y recibir pagos: el lado financiero del servicio.
- Catálogo de eventos: qué trae cada evento de OS.