Criar um endpoint
O endpoint é o endereço do seu sistema que recebe os eventos. Cadastrar é um passo só, pelo painel ou pela API. Este guia faz os dois caminhos e termina com a conferência de que as entregas estão chegando.
Antes de cadastrar
Seção intitulada “Antes de cadastrar”- Quem cadastra. Webhook é gerido por dono e administradores, pelo painel ou por uma chave de API de um membro com esse papel. Os outros papéis recebem 403 em toda rota de webhooks, inclusive as de leitura.
- Um endereço público com
https://. Endereço interno,localhostehttp://são recusados, e a conferência se repete em toda entrega. As regras completas estão em Para onde a entrega pode ir. Para testar a partir da sua máquina, você precisa expor o seu servidor local num endereço público comhttps://, por exemplo com um serviço de túnel. - Um servidor que confere a assinatura. Deixe o recebimento pronto antes, com o código de Verificar a assinatura. Ele responde 200 a uma entrega assinada e 400 ao resto.
- Os eventos que você usa. Escolha no Catálogo de eventos só o que o seu sistema vai tratar. Receber um evento é receber o dado dele.
Quantos endpoints a empresa pode ter depende do plano: veja Limites.
Pelo painel
Seção intitulada “Pelo painel”- Abra a página Configurações: clique no seu nome, no alto da tela, e em Configurações. Pelo menu lateral, o caminho é Configurações → Preferências da Empresa.
- Vá na aba API. O quadro Webhooks fica abaixo das chaves.
- Clique em Novo endpoint.
- Preencha a URL, uma Descrição para reconhecer depois qual sistema recebe, e marque os Eventos. Todos os eventos inclui também os que entrarem no catálogo depois.
- Confirme com a sua senha e clique em Criar endpoint.
A tela seguinte mostra o segredo, que começa com whsec_. Copie para o servidor que recebe os eventos. No painel, dono e administradores podem revelar o segredo de novo depois, pelo menu do endpoint (Revelar segredo), confirmando a senha.
O painel pede a senha ao criar e sempre que a URL ou os eventos mudam, porque são esses dois campos que decidem para onde os dados da empresa vão.
Pela API
Seção intitulada “Pela API”A chave precisa de:
| Permissão | Para quê |
|---|---|
webhooks:create | Cadastrar o endpoint |
<modulo>:read de cada evento | Se inscrever: sales:read para sale.*, clients:read para client.*, e assim por diante. Com ["*"], de todos os módulos |
webhooks:update | Mandar o ping, rotacionar o segredo, ligar e desligar. Trocar a URL ou os eventos, religar, mandar o ping e rotacionar o segredo pedem também o <modulo>:read de cada evento que o endpoint assina, mesmo que ele tenha sido criado no painel |
webhooks:read | Ler o endpoint e o log de entregas |
# 1. Cadastrar# 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/webhook_endpoints" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $CREATE_KEY" \ -d '{ "url": "https://erp.suaempresa.com.br/webhooks/faturei-hoje", "description": "ERP da loja", "events": ["sale.created", "sale.paid", "client.created"] }'
# 2. Conferir a conexão, com o id que voltou acimaENDPOINT_ID="7f3a1c5e-9b2d-4f6a-8c0e-2b4d6f8a0c1e"# Gere a chave uma vez e guarde: numa nova tentativa, repita com o mesmo 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. Alguns segundos depois, ver como a entrega foicurl "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;}
// 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();const pingKey = randomUUID();
// 1. Cadastrar.const endpoint = await call('POST', '/webhook_endpoints', { url: 'https://erp.suaempresa.com.br/webhooks/faturei-hoje', description: 'ERP da loja', events: ['sale.created', 'sale.paid', 'client.created'],}, createKey);// Guarde endpoint.secret no servidor que recebe os eventos. Pela API, ele// não aparece de novo. Não escreva o segredo em log.console.log('endpoint', endpoint.id, endpoint.status);
// 2. Conferir a conexão.await call('POST', `/webhook_endpoints/${endpoint.id}/ping`, undefined, pingKey);
// 3. Alguns segundos depois, ver como a entrega foi.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;}
// 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));$pingKey = bin2hex(random_bytes(16));
// 1. Cadastrar.$endpoint = call('POST', '/webhook_endpoints', [ 'url' => 'https://erp.suaempresa.com.br/webhooks/faturei-hoje', 'description' => 'ERP da loja', 'events' => ['sale.created', 'sale.paid', 'client.created'],], $createKey);// Guarde $endpoint['secret'] no servidor que recebe os eventos. Pela API, ele// não aparece de novo. Não escreva o segredo em log.echo 'endpoint ', $endpoint['id'], ' ', $endpoint['status'], PHP_EOL;
// 2. Conferir a conexão.call('POST', '/webhook_endpoints/' . $endpoint['id'] . '/ping', null, $pingKey);
// 3. Alguns segundos depois, ver como a entrega foi.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']}")
# 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())ping_key = str(uuid.uuid4())
# 1. Cadastrar.endpoint = call( "POST", "/webhook_endpoints", { "url": "https://erp.suaempresa.com.br/webhooks/faturei-hoje", "description": "ERP da loja", "events": ["sale.created", "sale.paid", "client.created"], }, create_key,)# Guarde endpoint["secret"] no servidor que recebe os eventos. Pela API, ele# não aparece de novo. Não escreva o segredo em log.print("endpoint", endpoint["id"], endpoint["status"])
# 2. Conferir a conexão.call("POST", f"/webhook_endpoints/{endpoint['id']}/ping", None, ping_key)
# 3. Alguns segundos depois, ver como a entrega foi.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"])A resposta do cadastro é 201 e traz o endpoint com o campo secret. Pela API, o segredo só aparece nesta resposta e na rotação. Repetir o cadastro com a mesma Idempotency-Key devolve o endpoint sem o segredo, porque ele não fica guardado em claro nem para a repetição. Se você perdeu a resposta, rotacione o segredo com POST /public/v1/webhook_endpoints/{id}/rotate_secret, ou revele-o pelo painel.
Dono e administradores recebem um e-mail a cada endpoint criado.
Conferir a conexão
Seção intitulada “Conferir a conexão”O ping (POST /public/v1/webhook_endpoints/{id}/ping, ou Enviar ping no menu do endpoint no painel) manda um evento ping de verdade só para aquele endpoint, assinado e pelo mesmo caminho de qualquer evento. Ele não leva dado da empresa. Não é ambiente de teste: é só a prova de que o endereço recebe e confere a assinatura.
Depois, abra o log de entregas (GET /public/v1/webhook_endpoints/{id}/deliveries, ou Ver entregas no painel):
| O que aparece | O que quer dizer |
|---|---|
succeeded | O seu servidor respondeu 2xx. Pronto |
pending, com response_status_code | O servidor respondeu, mas não 2xx. Uma nova tentativa vem depois |
pending, com error | Não houve resposta (tempo, DNS, TLS, conexão). O motivo está no campo |
failed | Acabaram as tentativas, ou a entrega foi fechada sem envio. Veja Entregas e novas tentativas |
Resposta 400 no ping quase sempre é assinatura que não conferiu no seu lado: o segredo errado, ou o corpo interpretado antes da conferência. Veja Os três erros que mais aparecem.
Quando o cadastro é recusado
Seção intitulada “Quando o cadastro é recusado”| Resposta | Por quê |
|---|---|
422 com param url | A URL não é https://, não é pública, tem usuário e senha, ou não resolve |
422 com param events | Evento que não existe no catálogo, ou * junto de outro nome |
403 permission_missing | Falta webhooks:create, ou o <modulo>:read de algum evento escolhido |
403 plan_limit_reached | A empresa chegou ao limite de endpoints do plano |
403 member_permission_denied | O membro por trás da chave não é dono nem administrador |
Próximo passo
Seção intitulada “Próximo passo”- Verificar a assinatura: o código do recebimento.
- Entregas e novas tentativas: o que acontece quando o seu servidor falha.
- Boas práticas: responder rápido, ignorar duplicata e ordenar.