EfratAPI
URL base: https://api.efrat.com.br

Integrando com o Efrat

A API do Efrat permite que outros sistemas (ERP, CRM, app, automações) consultem e gravem dados da sua empresa no Efrat, e recebam avisos automáticos quando algo muda.

Como funciona, em resumo

  1. Crie um Cliente de integração dentro do Efrat, marcando o que o sistema externo pode fazer.
  2. Guarde o Client ID e o Client Secret gerados — eles são a "senha" do sistema externo.
  3. Faça o login com essas chaves e receba um token de acesso (válido por 60 minutos).
  4. Chame as rotas enviando o token no cabeçalho Authorization.
  5. Opcional: cadastre Webhooks para receber os dados no seu sistema sempre que um registro for incluído, alterado ou excluído.

Informações gerais

URL basehttps://api.efrat.com.br
ProtocoloHTTPS obrigatório
FormatoJSON (UTF-8) no envio e na resposta — Content-Type: application/json
AutenticaçãoToken Bearer (JWT) obtido com Client ID + Client Secret
Nomes dos camposcamelCase (ex.: dueDate, totalRecords)
IdentificadoresUUID (ex.: 3fa85f64-5717-4562-b3fc-2c963f66afa6)
Rotas disponíveis— rotas, nos módulos Sistema, Único, Igreja, Financeiro, Agenda, Fiscal e Projetos

Seus dados ficam isolados. Cada cliente de integração pertence a uma empresa e só enxerga e grava dados dessa empresa. Não é possível acessar dados de outra empresa, mesmo conhecendo o identificador de um registro.

1. Criar um cliente de integração

O cliente de integração é a credencial de um sistema externo. Crie um para cada sistema que vai se conectar — por exemplo, um para o seu ERP e outro para o seu CRM. Assim cada um tem suas próprias permissões e pode ser desativado sem afetar os outros.

  1. Entre no Efrat com um usuário que tenha permissão em Clientes de Integração (normalmente o administrador da empresa).
  2. Abra o módulo Sistema → menu Configurações → Clientes de Integração.
  3. Clique em Novo Client.
  4. Em Nome, identifique o sistema (ex.: ERP Financeiro, CRM Vendas).
  5. Em Escopos, marque, para cada tela, o que o sistema pode fazer: Consultar, Incluir, Alterar e/ou Excluir. Só aparecem as telas liberadas no plano contratado.
  6. Deixe Ativo marcado e clique em Salvar.

Libere somente o necessário. Se o sistema externo só precisa ler o cadastro de pessoas, marque apenas Único › Pessoa — Consultar. Escopos podem ser alterados depois, a qualquer momento, na mesma tela.

Gerenciando clientes existentes

  • Editar: muda o nome, os escopos ou desativa o cliente. Mudanças de escopo valem a partir do próximo login (token novo).
  • Desativar: bloqueia novos logins na hora. Tokens já emitidos continuam válidos até expirarem (no máximo 60 minutos).
  • Gerar novo secret: cria um secret novo e invalida o anterior para novos logins — use se o secret vazar.
  • Último uso: a lista mostra quando cada cliente fez login pela última vez.

2. Client ID e Client Secret

Ao salvar um cliente novo, o Efrat mostra uma janela com as duas chaves:

ChaveFormatoÉ segredo?Onde aparece depois
Client IDic_ seguido de 24 caracteres (ex.: ic_4f9a1c0e7b2d8a6e3c5b1f07)Não — identifica o clienteSempre visível na lista de Clientes de Integração
Client SecretTexto longo e aleatórioSim — funciona como senhaNunca mais. Só é exibido na criação e ao gerar um novo secret

Copie o Client Secret na hora e guarde em local seguro (cofre de senhas, variável de ambiente do servidor). O Efrat não guarda o secret em texto — se ele se perder, use Gerar novo secret.

Nunca coloque o secret em código de navegador, aplicativo mobile ou repositório público: ele deve ficar só no servidor do sistema que se integra.

3. Login: obter o token de acesso

Troque o Client ID e o Client Secret por um token. O envio é sempre no corpo da requisição, em JSON (não use cabeçalho nem parâmetro de URL).

POST /api/integration/token
Content-Type: application/json

{
  "clientId": "ic_4f9a1c0e7b2d8a6e3c5b1f07",
  "clientSecret": "SEU_CLIENT_SECRET"
}

Resposta de sucesso — 200 OK

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "tokenType": "Bearer",
  "expiresIn": 3600
}
CampoDescrição
accessTokenToken a ser enviado em todas as requisições.
tokenTypeSempre Bearer.
expiresInValidade em segundos (3600 = 60 minutos). Depois disso, faça o login de novo.

Erros do login

CódigoMotivo
401Client ID ou Client Secret inválidos, ou cliente desativado. Resposta: {"message": "Client id ou client secret inválidos."}
400Corpo ausente ou JSON inválido.

Exemplos

curl -X POST "https://api.efrat.com.br/api/integration/token" \
  -H "Content-Type: application/json" \
  -d '{"clientId":"ic_4f9a1c0e7b2d8a6e3c5b1f07","clientSecret":"SEU_CLIENT_SECRET"}'
// Node.js 18+ (fetch nativo)
const res = await fetch('https://api.efrat.com.br/api/integration/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    clientId: process.env.EFRAT_CLIENT_ID,
    clientSecret: process.env.EFRAT_CLIENT_SECRET,
  }),
});
if (!res.ok) throw new Error(`Login falhou: ${res.status}`);
const { accessToken, expiresIn } = await res.json();
import os, requests

res = requests.post(
    "https://api.efrat.com.br/api/integration/token",
    json={"clientId": os.environ["EFRAT_CLIENT_ID"], "clientSecret": os.environ["EFRAT_CLIENT_SECRET"]},
    timeout=30,
)
res.raise_for_status()
token = res.json()["accessToken"]
using System.Net.Http.Json;

var http = new HttpClient { BaseAddress = new Uri("https://api.efrat.com.br") };
var res = await http.PostAsJsonAsync("/api/integration/token", new
{
    clientId = Environment.GetEnvironmentVariable("EFRAT_CLIENT_ID"),
    clientSecret = Environment.GetEnvironmentVariable("EFRAT_CLIENT_SECRET"),
});
res.EnsureSuccessStatusCode();
var login = await res.Content.ReadFromJsonAsync<TokenResponse>();

record TokenResponse(string AccessToken, string TokenType, int ExpiresIn);
<?php
$ch = curl_init('https://api.efrat.com.br/api/integration/token');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'clientId' => getenv('EFRAT_CLIENT_ID'),
        'clientSecret' => getenv('EFRAT_CLIENT_SECRET'),
    ]),
]);
$body = json_decode(curl_exec($ch), true);
$token = $body['accessToken'];

Reaproveite o token. Faça o login uma vez e use o mesmo token por até 60 minutos. Renove alguns minutos antes de vencer, ou quando receber 401. Não existe token de renovação (refresh token): basta fazer o login de novo.

4. Fazendo requisições

Em toda requisição, envie o token no cabeçalho Authorization:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Nas inclusões e alterações (POST/PUT), envie também Content-Type: application/json e o registro em JSON no corpo.

Exemplo: consultar o cadastro de pessoas (Único)

Escopo necessário: PESSOA:view (Único › Pessoa — Consultar).

GET /api/uniques?page=1&pageSize=20&name=Silva
Authorization: Bearer SEU_TOKEN
{
  "data": [
    {
      "id": "8229f2c4-da94-4c25-8c51-ceef19fef98a",
      "name": "Maria da Silva",
      "cpfCnpj": "12345678900",
      "typePerson": "Fisica",
      "fantasy": null,
      "primaryCellphone": "47999999999",
      "status": true
    }
  ],
  "totalRecords": 134,
  "page": 1,
  "pageSize": 20
}

Exemplo: incluir um centro de custo

Escopo necessário: CENTRO_CUSTO:create (Financeiro › Centro de Custo — Incluir).

POST /api/cost-centers
Authorization: Bearer SEU_TOKEN
Content-Type: application/json

{ "code": "0010", "description": "Marketing" }

Resposta 201 Created com o registro criado:

{ "id": "922fabe3-772b-49aa-a3d4-8f149cd5e2b5", "code": "0010", "description": "Marketing", "status": true }
# Consultar
curl "https://api.efrat.com.br/api/uniques?page=1&pageSize=20" \
  -H "Authorization: Bearer $TOKEN"

# Incluir
curl -X POST "https://api.efrat.com.br/api/cost-centers" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"code":"0010","description":"Marketing"}'
const api = (path, options = {}) =>
  fetch(`https://api.efrat.com.br${path}`, {
    ...options,
    headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', ...options.headers },
  });

// Consultar
const pessoas = await (await api('/api/uniques?page=1&pageSize=20')).json();

// Incluir
const res = await api('/api/cost-centers', {
  method: 'POST',
  body: JSON.stringify({ code: '0010', description: 'Marketing' }),
});
const criado = await res.json();
s = requests.Session()
s.headers.update({"Authorization": f"Bearer {token}"})
base = "https://api.efrat.com.br"

# Consultar
pessoas = s.get(f"{base}/api/uniques", params={"page": 1, "pageSize": 20}, timeout=30).json()

# Incluir
criado = s.post(f"{base}/api/cost-centers", json={"code": "0010", "description": "Marketing"}, timeout=30).json()
http.DefaultRequestHeaders.Authorization =
    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", login!.AccessToken);

// Consultar
var pessoas = await http.GetFromJsonAsync<JsonElement>("/api/uniques?page=1&pageSize=20");

// Incluir
var res = await http.PostAsJsonAsync("/api/cost-centers", new { code = "0010", description = "Marketing" });
res.EnsureSuccessStatusCode();
<?php
function efrat($method, $path, $token, $body = null) {
    $ch = curl_init('https://api.efrat.com.br' . $path);
    $headers = ['Authorization: Bearer ' . $token, 'Content-Type: application/json'];
    curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers]);
    if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    $response = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    return [$status, $response];
}

[$status, $pessoas] = efrat('GET', '/api/uniques?page=1&pageSize=20', $token);
[$status, $criado] = efrat('POST', '/api/cost-centers', $token, ['code' => '0010', 'description' => 'Marketing']);

Verbos HTTP

VerboUsoEscopo (ação)Resposta de sucesso
GETConsultar (listar ou buscar um registro)Consultar (view)200 OK com os dados
POSTIncluir um registro ou executar uma ação (ex.: cancelar, emitir)Incluir (create) ou Alterar (update), conforme a rota201 Created ou 200 OK
PUTAlterar um registro (envie o registro completo)Alterar (update)200 OK com o registro atualizado
DELETEExcluirExcluir (delete)204 No Content

O escopo exato de cada rota está indicado em Todas as rotas.

Escopos e permissões

Cada rota pertence a uma tela do Efrat e exige uma ação. O escopo é escrito como TELA:ação — por exemplo, LANCAMENTO:view permite consultar lançamentos financeiros e LANCAMENTO:create permite incluí-los.

Ação no cadastroCódigoPermite
ConsultarviewListar, buscar e baixar dados da tela
IncluircreateCriar registros
AlterarupdateEditar registros e executar ações sobre eles
ExcluirdeleteExcluir registros

Uma rota só responde se as três condições forem verdadeiras:

  1. O escopo da rota está marcado no cliente de integração;
  2. A tela faz parte do plano contratado pela empresa;
  3. A assinatura da empresa está ativa.

Caso contrário, a resposta é 403 Forbidden.

Rotas que não estão disponíveis para integração: usuários e permissões, login de usuários, parâmetros do sistema, cadastro de webhooks e de clientes de integração, assinatura, configurações de gateways de pagamento, WhatsApp, Google, Jira, Trello e configuração fiscal, campanhas de WhatsApp e dados de crianças. Essas operações só podem ser feitas por usuários dentro do Efrat.

Telas disponíveis

Paginação, filtros e datas

Listas paginadas

As listas aceitam page (começa em 1) e pageSize na query string e respondem neste formato:

{
  "data": [ ... ],
  "totalRecords": 134,
  "page": 1,
  "pageSize": 20
}

Para ler tudo, aumente page até ter lido totalRecords registros. O tamanho máximo de página varia por rota (normalmente 100) — valores maiores são ajustados automaticamente.

// Lendo todas as páginas
let page = 1, all = [];
while (true) {
  const r = await (await api(`/api/uniques?page=${page}&pageSize=100`)).json();
  all.push(...r.data);
  if (all.length >= r.totalRecords || r.data.length === 0) break;
  page++;
}

Filtros

Os filtros de cada lista são parâmetros de query string (ex.: ?status=true&search=maria) e estão descritos em cada rota na referência. Filtros não informados são ignorados.

Datas e valores

  • Datas no formato ISO 8601: 2026-10-31 ou 2026-10-31T14:30:00Z.
  • Na Agenda, data/hora dos compromissos são no horário local da empresa, sem fuso (ex.: 2026-10-31T14:30:00).
  • Valores monetários são números decimais com ponto: 1250.90.
  • Campos de tipo (ex.: typePerson, type) usam texto: "Fisica", "Receber" — os valores aceitos aparecem em cada rota.

Erros

CódigoSignificadoO que fazer
400Dados inválidos (campo obrigatório faltando, valor fora da regra)Leia a mensagem e corrija o envio
401Token ausente, inválido ou vencidoFaça o login de novo e repita a requisição
403Sem escopo para a rota, tela fora do plano, assinatura bloqueada ou rota indisponível para integraçãoRevise os escopos do cliente de integração
404Registro não encontrado nesta empresaConfira o identificador
500Erro internoTente novamente depois; se persistir, contate o suporte

Formato das mensagens

Erros de regra de negócio:

{ "message": "Já existe um centro de custo com este código." }

Erros de formato do JSON enviado (campos com tipo errado ou obrigatórios ausentes):

{
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": { "Description": ["The Description field is required."] }
}

Exemplos completos

Cliente pronto para uso: faz o login, guarda o token, renova quando vence e repete a requisição uma vez em caso de 401.

// efrat.js — Node.js 18+
const BASE = 'https://api.efrat.com.br';
let token = null, expiresAt = 0;

async function login() {
  const res = await fetch(`${BASE}/api/integration/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ clientId: process.env.EFRAT_CLIENT_ID, clientSecret: process.env.EFRAT_CLIENT_SECRET }),
  });
  if (!res.ok) throw new Error(`Login Efrat falhou (${res.status})`);
  const data = await res.json();
  token = data.accessToken;
  expiresAt = Date.now() + (data.expiresIn - 120) * 1000; // renova 2 min antes
}

export async function efrat(method, path, body, retried = false) {
  if (!token || Date.now() > expiresAt) await login();
  const res = await fetch(`${BASE}${path}`, {
    method,
    headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (res.status === 401 && !retried) { token = null; return efrat(method, path, body, true); }
  if (!res.ok) throw new Error(`${method} ${path} → ${res.status}: ${await res.text()}`);
  return res.status === 204 ? null : res.json();
}

// Uso
const lancamentos = await efrat('GET', '/api/financial-entries?page=1&pageSize=50&type=Receber');
# efrat.py
import os, time, requests

BASE = "https://api.efrat.com.br"

class Efrat:
    def __init__(self):
        self.token, self.expires_at = None, 0
        self.http = requests.Session()

    def _login(self):
        r = self.http.post(f"{BASE}/api/integration/token", json={
            "clientId": os.environ["EFRAT_CLIENT_ID"],
            "clientSecret": os.environ["EFRAT_CLIENT_SECRET"],
        }, timeout=30)
        r.raise_for_status()
        data = r.json()
        self.token = data["accessToken"]
        self.expires_at = time.time() + data["expiresIn"] - 120

    def request(self, method, path, body=None, retried=False):
        if not self.token or time.time() > self.expires_at:
            self._login()
        r = self.http.request(method, f"{BASE}{path}", json=body, timeout=60,
                              headers={"Authorization": f"Bearer {self.token}"})
        if r.status_code == 401 and not retried:
            self.token = None
            return self.request(method, path, body, True)
        r.raise_for_status()
        return None if r.status_code == 204 else r.json()

efrat = Efrat()
lancamentos = efrat.request("GET", "/api/financial-entries?page=1&pageSize=50&type=Receber")
// EfratClient.cs — .NET 8+
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;

public sealed class EfratClient(HttpClient http, string clientId, string clientSecret)
{
    private string? _token;
    private DateTime _expiresAt;

    private async Task LoginAsync()
    {
        var res = await http.PostAsJsonAsync("https://api.efrat.com.br/api/integration/token", new { clientId, clientSecret });
        res.EnsureSuccessStatusCode();
        var data = (await res.Content.ReadFromJsonAsync<TokenResponse>())!;
        _token = data.AccessToken;
        _expiresAt = DateTime.UtcNow.AddSeconds(data.ExpiresIn - 120);
    }

    public async Task<HttpResponseMessage> SendAsync(HttpMethod method, string path, object? body = null, bool retried = false)
    {
        if (_token is null || DateTime.UtcNow > _expiresAt) await LoginAsync();
        var req = new HttpRequestMessage(method, "https://api.efrat.com.br" + path);
        req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
        if (body is not null) req.Content = JsonContent.Create(body);
        var res = await http.SendAsync(req);
        if (res.StatusCode == HttpStatusCode.Unauthorized && !retried) { _token = null; return await SendAsync(method, path, body, true); }
        return res;
    }

    private sealed record TokenResponse(string AccessToken, string TokenType, int ExpiresIn);
}
<?php
// efrat.php
class Efrat {
    private $base = 'https://api.efrat.com.br';
    private $token = null;
    private $expiresAt = 0;

    private function call($method, $url, $headers, $body = null) {
        $ch = curl_init($url);
        curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 60]);
        if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
        $resp = curl_exec($ch);
        return [curl_getinfo($ch, CURLINFO_HTTP_CODE), json_decode($resp, true)];
    }

    private function login() {
        [$status, $data] = $this->call('POST', $this->base . '/api/integration/token', ['Content-Type: application/json'],
            ['clientId' => getenv('EFRAT_CLIENT_ID'), 'clientSecret' => getenv('EFRAT_CLIENT_SECRET')]);
        if ($status !== 200) throw new Exception('Login Efrat falhou: ' . $status);
        $this->token = $data['accessToken'];
        $this->expiresAt = time() + $data['expiresIn'] - 120;
    }

    public function request($method, $path, $body = null, $retried = false) {
        if (!$this->token || time() > $this->expiresAt) $this->login();
        [$status, $data] = $this->call($method, $this->base . $path,
            ['Authorization: Bearer ' . $this->token, 'Content-Type: application/json'], $body);
        if ($status === 401 && !$retried) { $this->token = null; return $this->request($method, $path, $body, true); }
        if ($status >= 400) throw new Exception("$method $path → $status");
        return $data;
    }
}

$efrat = new Efrat();
$lancamentos = $efrat->request('GET', '/api/financial-entries?page=1&pageSize=50&type=Receber');
TOKEN=$(curl -s -X POST "https://api.efrat.com.br/api/integration/token" \
  -H "Content-Type: application/json" \
  -d "{\"clientId\":\"$EFRAT_CLIENT_ID\",\"clientSecret\":\"$EFRAT_CLIENT_SECRET\"}" | jq -r .accessToken)

curl "https://api.efrat.com.br/api/financial-entries?page=1&pageSize=50&type=Receber" \
  -H "Authorization: Bearer $TOKEN"

Webhooks: receba os dados quando algo mudar

Em vez de consultar a API de tempos em tempos, você pode cadastrar um webhook: sempre que um registro de uma tela for incluído, alterado ou excluído no Efrat (por um usuário ou por uma integração), o Efrat envia um POST com os dados para o endereço que você informar.

Cadastrando um webhook

  1. No Efrat, abra o módulo Sistema → menu Configurações → WebHook e clique em Novo.
  2. Dê um Nome (ex.: Novos lançamentos para o ERP).
  3. Escolha a Rota (a tela, ex.: Financeiro › Lançamentos) e o Gatilho: Created (inclusão), Updated (alteração) ou Deleted (exclusão). Cada webhook escuta um gatilho; para receber os três, cadastre três webhooks.
  4. Informe a URL de destino — um endereço https:// do seu sistema que aceite POST.
  5. Salve e copie o Secret exibido. Ele serve para conferir a assinatura de cada envio e só é mostrado agora (pode ser regenerado depois).

O que o Efrat envia

POST https://seu-sistema.com.br/webhooks/efrat
Content-Type: application/json
X-Webhook-Id: 6a1f0c2e-9a7b-4d0e-8f53-1c2b3d4e5f60
X-Webhook-Event: CENTRO_CUSTO.Created
X-Webhook-Attempt: 1
X-Webhook-Signature: sha256=5d41402abc4b2a76b9719d911017c592...

{
  "event": "CENTRO_CUSTO.Created",
  "entity": "CENTRO_CUSTO",
  "trigger": "Created",
  "method": "POST",
  "path": "/api/cost-centers",
  "occurredAtUtc": "2026-10-20T14:32:05.123Z",
  "data": {
    "id": "922fabe3-772b-49aa-a3d4-8f149cd5e2b5",
    "code": "0010",
    "description": "Marketing",
    "status": true
  }
}
Campo / cabeçalhoDescrição
eventTELA.Gatilho — ex.: LANCAMENTO.Updated.
entityCódigo da tela (o mesmo dos escopos).
triggerCreated, Updated ou Deleted.
method / pathA rota do Efrat que gerou o evento.
occurredAtUtcQuando aconteceu (UTC).
dataO mesmo JSON que a rota devolveu — normalmente o registro completo. Em exclusões, traz o identificador do registro excluído (ex.: {"id": "..."}).
X-Webhook-IdIdentificador do webhook cadastrado.
X-Webhook-AttemptNúmero da tentativa (1, 2 ou 3).
X-Webhook-Signaturesha256= + assinatura HMAC-SHA256 do corpo com o seu Secret (veja abaixo).

Respondendo ao Efrat

  • Responda com qualquer código 2xx (ex.: 200) para confirmar o recebimento.
  • Responda rápido: processe os dados depois (fila), sem segurar a resposta.
  • Se a entrega falhar (erro de rede ou resposta diferente de 2xx), o Efrat tenta de novo após 5 segundos e após 30 segundos — no máximo 3 tentativas. Por isso, trate o mesmo evento chegando mais de uma vez (use event + data.id + occurredAtUtc para descartar repetidos).
  • O histórico de entregas (sucesso, código de resposta e trecho do retorno) aparece no cadastro do webhook, no Efrat.

Validando a assinatura do webhook

Para ter certeza de que a chamada veio do Efrat, calcule o HMAC-SHA256 do corpo bruto recebido (exatamente os bytes recebidos, antes de converter o JSON) usando o Secret do webhook, em hexadecimal minúsculo, e compare com o valor depois de sha256= no cabeçalho X-Webhook-Signature. Se não bater, recuse com 401.

// Express
import express from 'express';
import crypto from 'node:crypto';

const app = express();
app.post('/webhooks/efrat', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.EFRAT_WEBHOOK_SECRET).update(req.body).digest('hex');
  const received = req.get('X-Webhook-Signature') || '';
  const ok = received.length === expected.length && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(200);              // responde já
  processarDepois(event);           // e processa em seguida
});
# Flask
import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/webhooks/efrat")
def efrat_webhook():
    raw = request.get_data()
    expected = "sha256=" + hmac.new(os.environ["EFRAT_WEBHOOK_SECRET"].encode(), raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Webhook-Signature", "")):
        abort(401)
    event = request.get_json()
    # enfileire event para processar depois
    return "", 200
// ASP.NET Core (minimal API)
app.MapPost("/webhooks/efrat", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    var raw = await reader.ReadToEndAsync();

    var secret = Environment.GetEnvironmentVariable("EFRAT_WEBHOOK_SECRET")!;
    using var hmac = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(secret));
    var expected = "sha256=" + Convert.ToHexStringLower(hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(raw)));
    var received = request.Headers["X-Webhook-Signature"].ToString();

    if (!System.Security.Cryptography.CryptographicOperations.FixedTimeEquals(
            System.Text.Encoding.UTF8.GetBytes(expected), System.Text.Encoding.UTF8.GetBytes(received)))
        return Results.Unauthorized();

    var evt = System.Text.Json.JsonDocument.Parse(raw);
    // enfileire evt para processar depois
    return Results.Ok();
});
<?php
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('EFRAT_WEBHOOK_SECRET'));
$received = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

if (!hash_equals($expected, $received)) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);
// enfileire $event para processar depois
http_response_code(200);
# Conferindo uma assinatura na linha de comando
echo -n '{"event":"CENTRO_CUSTO.Created", ...corpo exato...}' \
  | openssl dgst -sha256 -hmac "SEU_WEBHOOK_SECRET" | sed 's/^.* /sha256=/'

Testando: use um serviço como webhook.site como URL de destino para ver exatamente o que o Efrat envia, antes de apontar para o seu sistema.

Todas as rotas

Rotas disponíveis para clientes de integração, agrupadas por módulo e tela. Clique numa rota para ver os parâmetros, os campos do corpo, um exemplo de resposta e o comando pronto. Todas exigem o cabeçalho Authorization: Bearer SEU_TOKEN.

Boas práticas

  • Um cliente de integração por sistema, com o mínimo de escopos necessário.
  • Guarde o Client Secret e o Secret do webhook só no servidor (variáveis de ambiente ou cofre de segredos).
  • Reaproveite o token pelos 60 minutos de validade, em vez de fazer login a cada requisição.
  • Prefira webhooks a consultas repetidas para saber o que mudou; use a API para buscar detalhes e para gravar.
  • Pagine listas grandes e use os filtros de data/status para buscar só o necessário.
  • Trate repetições de webhook (até 3 tentativas) e responda 2xx rapidamente.
  • Registre os erros com o corpo da resposta: a mensagem em message explica o motivo.
  • Troque o secret (Gerar novo secret) se suspeitar de vazamento, e desative clientes que não usa mais.

Perguntas frequentes

Recebo 403 numa rota que deveria funcionar.

Confira se o escopo indicado na rota está marcado no cliente de integração, se a tela faz parte do plano contratado e se a assinatura está em dia. Depois de mudar os escopos, faça o login de novo para receber um token atualizado.

Recebo 401 de repente.

O token venceu (60 minutos) ou o cliente foi desativado/teve o secret trocado. Faça o login de novo.

Os registros gravados pela integração aparecem como feitos por quem?

Como Integração <nome do cliente> — assim fica claro no Efrat o que veio de cada sistema.

Posso usar o mesmo cliente de integração em dois sistemas?

Pode, mas não é recomendado: crie um para cada sistema, para controlar e revogar o acesso separadamente.

O webhook também dispara para alterações feitas pela própria integração?

Sim. Qualquer inclusão, alteração ou exclusão na tela dispara o webhook, venha de um usuário ou de uma integração.

Existe ambiente de testes?

Use um cliente de integração com escopo só de consulta para começar, e um endereço como webhook.site para inspecionar os webhooks.