Crear un endpoint
El endpoint es la dirección de su sistema que recibe los eventos. Registrarlo es un solo paso, en el panel o por la API. Esta guía recorre los dos caminos y termina verificando que las entregas están llegando.
Antes de registrar
Sección titulada «Antes de registrar»- Quién registra. Los webhooks los gestionan el dueño y los administradores, en el panel o con una clave de API de un miembro con ese rol. Los demás roles reciben 403 en toda ruta de webhooks, incluidas las de lectura.
- Una dirección pública con
https://. Las direcciones internas,localhostyhttp://se rechazan, y la verificación se repite en cada entrega. Las reglas completas están en Adónde puede ir la entrega. Para probar desde su máquina, necesita exponer su servidor local en una dirección pública conhttps://, por ejemplo con un servicio de túnel. - Un servidor que verifica la firma. Deje listo el receptor antes, con el código de Verificar la firma. Responde 200 a una entrega firmada y 400 a todo lo demás.
- Los eventos que usa. Elija en el Catálogo de eventos solo lo que su sistema va a tratar. Recibir un evento es recibir sus datos.
Cuántos endpoints puede tener la empresa depende del plan: vea Límites.
En el panel
Sección titulada «En el panel»- Abra la página Configuración: haga clic en su nombre, arriba en la pantalla, y en Configuración. Desde el menú lateral, el camino es Configuración → Preferencias de la Empresa.
- Vaya a la pestaña API. El recuadro Webhooks está debajo de las claves.
- Haga clic en Nuevo endpoint.
- Complete la URL, una Descripción para reconocer después qué sistema recibe, y marque los Eventos. Todos los eventos incluye también los que entren en el catálogo después.
- Confirme con su contraseña y haga clic en Crear endpoint.
La pantalla siguiente muestra el secreto, que empieza con whsec_. Cópielo al servidor que recibe los eventos. En el panel, el dueño y los administradores pueden revelar el secreto de nuevo después, desde el menú del endpoint (Revelar secreto), confirmando la contraseña.
El panel pide la contraseña al crear y cada vez que cambian la URL o los eventos, porque son esos dos campos los que deciden adónde van los datos de la empresa.
Por la API
Sección titulada «Por la API»La clave necesita:
| Permiso | Para qué |
|---|---|
webhooks:create | Registrar el endpoint |
<modulo>:read de cada evento | Suscribirse: sales:read para sale.*, clients:read para client.*, y así sucesivamente. Con ["*"], de todos los módulos |
webhooks:update | Enviar el ping, rotar el secreto, activarlo y desactivarlo. Cambiar la URL o los eventos, reactivarlo, enviar el ping y rotar el secreto piden también el <modulo>:read de cada evento al que el endpoint está suscrito, aunque se haya creado en el panel |
webhooks:read | Leer el endpoint y el registro de entregas |
# 1. Registrar# 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/webhook_endpoints" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CREATE_KEY" \ -d '{ "url": "https://erp.suempresa.com/webhooks/faturei-hoje", "description": "ERP de la tienda", "events": ["sale.created", "sale.paid", "client.created"] }'
# 2. Verificar la conexión, con el id que volvió arribaENDPOINT_ID="7f3a1c5e-9b2d-4f6a-8c0e-2b4d6f8a0c1e"# Genere la clave una vez y guárdela: en un reintento, repita con el mismo valor.PING_KEY=$(uuidgen)curl -X POST "https://api.fatureihoje.com/public/v1/webhook_endpoints/$ENDPOINT_ID/ping" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Idempotency-Key: $PING_KEY"
# 3. Unos segundos después, ver cómo fue la entregacurl "https://api.fatureihoje.com/public/v1/webhook_endpoints/$ENDPOINT_ID/deliveries?limit=5" \ -H "Authorization: Bearer $FH_API_KEY"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 (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey; if (body !== undefined) headers['Content-Type'] = 'application/json'; const res = await fetch(`${API}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), }); const data = res.status === 202 ? null : await res.json(); if (!res.ok) throw new Error(`${res.status} ${data.error.code}: ${data.error.message}`); return data;}
// 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();const pingKey = randomUUID();
// 1. Registrar.const endpoint = await call('POST', '/webhook_endpoints', { url: 'https://erp.suempresa.com/webhooks/faturei-hoje', description: 'ERP de la tienda', events: ['sale.created', 'sale.paid', 'client.created'],}, createKey);// Guarde endpoint.secret en el servidor que recibe los eventos. Por la API, no// vuelve a aparecer. No escriba el secreto en logs.console.log('endpoint', endpoint.id, endpoint.status);
// 2. Verificar la conexión.await call('POST', `/webhook_endpoints/${endpoint.id}/ping`, undefined, pingKey);
// 3. Unos segundos después, ver cómo fue la entrega.await new Promise((resolve) => setTimeout(resolve, 5000));const deliveries = await call('GET', `/webhook_endpoints/${endpoint.id}/deliveries?limit=5`);for (const delivery of deliveries.data) { console.log(delivery.event_type, delivery.status, delivery.response_status_code, delivery.error);}<?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 ($idempotencyKey !== null) { $headers[] = 'Idempotency-Key: ' . $idempotencyKey; } if ($body !== null) { $headers[] = 'Content-Type: application/json'; } $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)); } $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); $data = $status === 202 ? null : json_decode($raw, true); if ($status >= 400) { throw new RuntimeException($status . ' ' . $data['error']['code'] . ': ' . $data['error']['message']); } return $data;}
// 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));$pingKey = bin2hex(random_bytes(16));
// 1. Registrar.$endpoint = call('POST', '/webhook_endpoints', [ 'url' => 'https://erp.suempresa.com/webhooks/faturei-hoje', 'description' => 'ERP de la tienda', 'events' => ['sale.created', 'sale.paid', 'client.created'],], $createKey);// Guarde $endpoint['secret'] en el servidor que recibe los eventos. Por la API,// no vuelve a aparecer. No escriba el secreto en logs.echo 'endpoint ', $endpoint['id'], ' ', $endpoint['status'], PHP_EOL;
// 2. Verificar la conexión.call('POST', '/webhook_endpoints/' . $endpoint['id'] . '/ping', null, $pingKey);
// 3. Unos segundos después, ver cómo fue la entrega.sleep(5);$deliveries = call('GET', '/webhook_endpoints/' . $endpoint['id'] . '/deliveries?limit=5');foreach ($deliveries['data'] as $delivery) { echo $delivery['event_type'], ' ', $delivery['status'], PHP_EOL;}import jsonimport osimport timeimport urllib.errorimport 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']}"} if idempotency_key: headers["Idempotency-Key"] = idempotency_key data = None if body is not None: headers["Content-Type"] = "application/json" data = json.dumps(body).encode() request = urllib.request.Request(API + path, data=data, method=method, headers=headers) try: with urllib.request.urlopen(request) as response: return None if response.status == 202 else json.load(response) except urllib.error.HTTPError as failure: error = json.load(failure)["error"] raise SystemExit(f"{failure.code} {error['code']}: {error['message']}")
# 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())ping_key = str(uuid.uuid4())
# 1. Registrar.endpoint = call( "POST", "/webhook_endpoints", { "url": "https://erp.suempresa.com/webhooks/faturei-hoje", "description": "ERP de la tienda", "events": ["sale.created", "sale.paid", "client.created"], }, create_key,)# Guarde endpoint["secret"] en el servidor que recibe los eventos. Por la API,# no vuelve a aparecer. No escriba el secreto en logs.print("endpoint", endpoint["id"], endpoint["status"])
# 2. Verificar la conexión.call("POST", f"/webhook_endpoints/{endpoint['id']}/ping", None, ping_key)
# 3. Unos segundos después, ver cómo fue la entrega.time.sleep(5)deliveries = call("GET", f"/webhook_endpoints/{endpoint['id']}/deliveries?limit=5")for delivery in deliveries["data"]: print(delivery["event_type"], delivery["status"])La respuesta del registro es 201 y trae el endpoint con el campo secret. Por la API, el secreto solo aparece en esta respuesta y en la rotación. Repetir el registro con la misma Idempotency-Key devuelve el endpoint sin el secreto, porque no queda guardado en claro, ni siquiera para la repetición. Si perdió la respuesta, rote el secreto con POST /public/v1/webhook_endpoints/{id}/rotate_secret, o revélelo en el panel.
El dueño y los administradores reciben un correo por cada endpoint creado.
Verificar la conexión
Sección titulada «Verificar la conexión»El ping (POST /public/v1/webhook_endpoints/{id}/ping, o Enviar ping en el menú del endpoint en el panel) envía un evento ping real solo a ese endpoint, firmado y por el mismo camino que cualquier evento. No lleva datos de la empresa. No es un entorno de prueba: solo prueba que la dirección recibe y verifica la firma.
Después, abra el registro de entregas (GET /public/v1/webhook_endpoints/{id}/deliveries, o Ver entregas en el panel):
| Lo que aparece | Qué significa |
|---|---|
succeeded | Su servidor respondió 2xx. Listo |
pending, con response_status_code | El servidor respondió, pero no 2xx. Después viene otro intento |
pending, con error | No hubo respuesta (tiempo, DNS, TLS, conexión). El motivo está en el campo |
failed | Se acabaron los intentos, o la entrega se cerró sin enviarse. Vea Entregas y reintentos |
Un 400 en el ping casi siempre es una firma que no coincidió de su lado: el secreto equivocado, o el cuerpo interpretado antes de la verificación. Vea Verificar la firma.
Cuando el registro se rechaza
Sección titulada «Cuando el registro se rechaza»| Respuesta | Por qué |
|---|---|
422 con param url | La URL no es https://, no es pública, tiene usuario y contraseña, o no resuelve |
422 con param events | Un evento que no existe en el catálogo, o * junto con otro nombre |
403 permission_missing | Falta webhooks:create, o el <modulo>:read de algún evento elegido |
403 plan_limit_reached | La empresa llegó al límite de endpoints del plan |
403 member_permission_denied | El miembro detrás de la clave no es dueño ni administrador |
Próximo paso
Sección titulada «Próximo paso»- Verificar la firma: el código del receptor.
- Entregas y reintentos: qué pasa cuando su servidor falla.
- Buenas prácticas: responder rápido, ignorar duplicados y ordenar.