APIPreçosDocumentaçãoBlogParceirosContato
Voltar ao blog
Engineering

Projetando uma API de Ponto Biométrico Que Desenvolvedores Realmente Querem Usar

A maioria das APIs biométricas são remendos sobre SDKs de hardware. Veja como projetamos a API REST do PunchConnect a partir de princípios fundamentais — com webhooks, idempotência e entrega em tempo real que desenvolvedores esperam de infraestrutura moderna.

PunchConnect Team·Mar 28, 2026·10 min read

Introduction

Você construiu um sistema de folha de pagamento. A folha depende dos dados de frequência. Agora você precisa capturar os eventos de ponto de 200 dispositivos biométricos no seu sistema — de forma confiável, em tempo real, sem perder um único registro. Esse é o problema de design de API de que ninguém fala até estar depurando batidas perdidas às 2 da manhã.

A maioria das "APIs" biométricas são SDKs de fabricante disfarçados de REST. Expõem registros brutos do dispositivo, exigem loops de polling e quebram quando um relógio de ponto se desconecta por 30 segundos. Não foram projetadas para aplicações cloud-native.

Quando construímos a API do PunchConnect, começamos do outro lado: o que um desenvolvedor construindo uma integração ERP esperaria? A resposta se parecia muito com Stripe, Twilio e GitHub — não com um manual de hardware.

Três Princípios de Design Emprestados dos Melhores

Estudamos as APIs que desenvolvedores realmente gostam de usar. Três padrões apareciam repetidamente:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 320" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="30" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">Princípios de Design de API</text>
<!-- Previsível -->
<rect x="30" y="60" width="220" height="120" rx="12" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="140" y="90" text-anchor="middle" fill="#22d3ee" font-size="17" font-weight="bold" font-family="system-ui">🎯 Previsível</text>
<text x="140" y="115" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Nomenclatura consistente</text>
<text x="140" y="135" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Códigos de erro uniformes</text>
<text x="140" y="155" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Estrutura resposta padrão</text>
<!-- Tempo real -->
<rect x="290" y="60" width="220" height="120" rx="12" stroke="#a78bfa" stroke-width="2" fill="none"/>
<text x="400" y="90" text-anchor="middle" fill="#a78bfa" font-size="17" font-weight="bold" font-family="system-ui">⚡ Tempo Real</text>
<text x="400" y="115" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Webhooks, não polling</text>
<text x="400" y="135" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Eventos enviados na hora</text>
<text x="400" y="155" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Entrega sub-segundo</text>
<!-- Resiliente -->
<rect x="550" y="60" width="220" height="120" rx="12" stroke="#34d399" stroke-width="2" fill="none"/>
<text x="660" y="90" text-anchor="middle" fill="#34d399" font-size="17" font-weight="bold" font-family="system-ui">🛡️ Resiliente</text>
<text x="660" y="115" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Chaves de idempotência</text>
<text x="660" y="135" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Entrega ao-menos-uma-vez</text>
<text x="660" y="155" text-anchor="middle" fill="#94a3b8" font-size="14" font-family="system-ui">Janela de retry 72h</text>
<!-- Resumo -->
<rect x="130" y="220" width="540" height="70" rx="12" stroke="#64748b" stroke-width="1.5" fill="none" stroke-dasharray="5,4"/>
<text x="400" y="250" text-anchor="middle" fill="#e2e8f0" font-size="15" font-weight="bold" font-family="system-ui">Resultado: Uma API que funciona como Stripe — para hardware biométrico</text>
<text x="400" y="272" text-anchor="middle" fill="#64748b" font-size="14" font-family="system-ui">24.000+ funcionários na AgriWise comprovam na escala</text>
<line x1="140" y1="180" x2="250" y2="220" stroke="#64748b" stroke-width="1.5"/>
<line x1="400" y1="180" x2="400" y2="220" stroke="#64748b" stroke-width="1.5"/>
<line x1="660" y1="180" x2="550" y2="220" stroke="#64748b" stroke-width="1.5"/>
</svg>

1. Ser previsível. Cada endpoint segue a mesma convenção de nomenclatura. Cada resposta tem a mesma estrutura. Cada erro retorna um objeto estruturado com code legível por máquina e message legível por humanos. Sem surpresas.

2. Ser tempo real. Webhooks para eventos, não polling. Quando um funcionário bate o ponto na porta, sua aplicação sabe em segundos — não quando seu cron job executa.

3. Ser resiliente. Cada operação mutante aceita uma chave de idempotência. Cada entrega webhook é retentada com backoff exponencial. Cada dado pode ser reproduzido sem duplicatas.

O Pipeline de Entrega de Webhooks

A parte mais difícil de uma API de ponto biométrico não são os endpoints REST — é garantir a entrega dos eventos quando o mundo real é instável. Dispositivos se desconectam. Redes caem. Servidores reiniciam durante deploys.

PunchConnect garante entrega ao-menos-uma-vez para cada evento webhook:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 380" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="25" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">Pipeline de Entrega Webhook</text>
<rect x="20" y="60" width="150" height="65" rx="10" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="95" y="85" text-anchor="middle" fill="#22d3ee" font-size="15" font-family="system-ui">🖐 Batida</text>
<text x="95" y="105" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Evento ocorre</text>
<line x1="170" y1="92" x2="220" y2="92" stroke="#64748b" stroke-width="2" marker-end="url(#ptArrow)"/>
<rect x="220" y="60" width="150" height="65" rx="10" stroke="#a78bfa" stroke-width="2" fill="none"/>
<text x="295" y="85" text-anchor="middle" fill="#a78bfa" font-size="15" font-family="system-ui">📥 Fila durável</text>
<text x="295" y="105" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Persistido em disco</text>
<line x1="370" y1="92" x2="420" y2="92" stroke="#64748b" stroke-width="2" marker-end="url(#ptArrow)"/>
<rect x="420" y="60" width="150" height="65" rx="10" stroke="#34d399" stroke-width="2" fill="none"/>
<text x="495" y="85" text-anchor="middle" fill="#34d399" font-size="15" font-family="system-ui">📤 HTTP POST</text>
<text x="495" y="105" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Para seu callback</text>
<line x1="570" y1="92" x2="620" y2="92" stroke="#64748b" stroke-width="2" marker-end="url(#ptArrow)"/>
<rect x="620" y="60" width="150" height="65" rx="10" stroke="#fbbf24" stroke-width="2" fill="none"/>
<text x="695" y="85" text-anchor="middle" fill="#fbbf24" font-size="15" font-family="system-ui">✅ 2xx = OK</text>
<text x="695" y="105" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Marcado entregue</text>
<text x="400" y="165" text-anchor="middle" fill="#fb923c" font-size="15" font-weight="bold" font-family="system-ui">Se seu servidor retorna 5xx ou timeout:</text>
<rect x="40" y="190" width="100" height="50" rx="8" stroke="#fb923c" stroke-width="1.5" fill="none"/>
<text x="90" y="212" text-anchor="middle" fill="#fb923c" font-size="14" font-family="system-ui">1 min</text>
<text x="90" y="230" text-anchor="middle" fill="#64748b" font-size="12" font-family="system-ui">Retry 1</text>
<line x1="140" y1="215" x2="170" y2="215" stroke="#fb923c" stroke-width="1.5" marker-end="url(#ptRetry)"/>
<rect x="170" y="190" width="100" height="50" rx="8" stroke="#fb923c" stroke-width="1.5" fill="none"/>
<text x="220" y="212" text-anchor="middle" fill="#fb923c" font-size="14" font-family="system-ui">5 min</text>
<text x="220" y="230" text-anchor="middle" fill="#64748b" font-size="12" font-family="system-ui">Retry 2</text>
<line x1="270" y1="215" x2="300" y2="215" stroke="#fb923c" stroke-width="1.5" marker-end="url(#ptRetry)"/>
<rect x="300" y="190" width="100" height="50" rx="8" stroke="#fb923c" stroke-width="1.5" fill="none"/>
<text x="350" y="212" text-anchor="middle" fill="#fb923c" font-size="14" font-family="system-ui">30 min</text>
<text x="350" y="230" text-anchor="middle" fill="#64748b" font-size="12" font-family="system-ui">Retry 3</text>
<line x1="400" y1="215" x2="430" y2="215" stroke="#fb923c" stroke-width="1.5" marker-end="url(#ptRetry)"/>
<rect x="430" y="190" width="100" height="50" rx="8" stroke="#fb923c" stroke-width="1.5" fill="none"/>
<text x="480" y="212" text-anchor="middle" fill="#fb923c" font-size="14" font-family="system-ui">2 horas</text>
<text x="480" y="230" text-anchor="middle" fill="#64748b" font-size="12" font-family="system-ui">Retry 4</text>
<line x1="530" y1="215" x2="560" y2="215" stroke="#fb923c" stroke-width="1.5" marker-end="url(#ptRetry)"/>
<rect x="560" y="190" width="100" height="50" rx="8" stroke="#fb923c" stroke-width="1.5" fill="none"/>
<text x="610" y="212" text-anchor="middle" fill="#fb923c" font-size="14" font-family="system-ui">12 horas</text>
<text x="610" y="230" text-anchor="middle" fill="#64748b" font-size="12" font-family="system-ui">Retry 5</text>
<line x1="660" y1="215" x2="690" y2="215" stroke="#fb923c" stroke-width="1.5" marker-end="url(#ptRetry)"/>
<rect x="690" y="190" width="80" height="50" rx="8" stroke="#fb923c" stroke-width="1.5" fill="none"/>
<text x="730" y="212" text-anchor="middle" fill="#fb923c" font-size="14" font-family="system-ui">72h</text>
<text x="730" y="230" text-anchor="middle" fill="#64748b" font-size="12" font-family="system-ui">Final</text>
<line x1="730" y1="240" x2="730" y2="280" stroke="#ef4444" stroke-width="1.5" marker-end="url(#ptFail)"/>
<rect x="620" y="280" width="180" height="50" rx="8" stroke="#ef4444" stroke-width="1.5" fill="none"/>
<text x="710" y="302" text-anchor="middle" fill="#ef4444" font-size="14" font-family="system-ui">❌ Marcado falhou</text>
<text x="710" y="320" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Notificação email enviada</text>
<defs>
<marker id="ptArrow" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#64748b"/></marker>
<marker id="ptRetry" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#fb923c"/></marker>
<marker id="ptFail" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#ef4444"/></marker>
</defs>
</svg>

1. O funcionário bate ponto no dispositivo biométrico (impressão digital, rosto, cartão)
2. O evento é persistido em uma fila durável — seguro mesmo se o sistema reiniciar
3. O worker de entrega envia um HTTP POST para sua URL de callback registrada
4. Seu servidor responde com 2xx → evento marcado como entregue
5. Se a entrega falha (5xx, timeout, erro de rede) → retries com backoff exponencial por 72 horas
6. Após esgotar retries → evento marcado como falhou, notificação email enviada

Payload do Webhook

Cada webhook entrega um objeto JSON limpo e normalizado:

json
{
"event_id": "evt_a1b2c3d4e5f6",
"event_type": "attendance.punch",
"timestamp": "2026-03-28T08:01:23Z",
"device": {
"id": "dev_9x8y7z",
"serial_number": "BKRK233600045",
"name": "Entrada Principal",
"site": "Matriz"
},
"employee": {
"id": "emp_4k5l6m",
"badge_number": "1042",
"name": "Sarah Chen"
},
"punch": {
"direction": "in",
"method": "fingerprint",
"confidence": 0.97,
"local_time": "2026-03-28T09:01:23-03:00"
},
"metadata": {
"deduplicated": false,
"delivery_attempt": 1
}
}

Receber Webhooks na Sua Aplicação

Python (Flask)

python
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_seu_segredo_de_assinatura"
def verify_signature(payload, signature):
expected = hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
@app.route("/webhooks/attendance", methods=["POST"])
def handle_attendance():
signature = request.headers.get("X-PunchConnect-Signature")
if not verify_signature(request.data, signature):
return jsonify({"error": "Assinatura inválida"}), 401
event = request.json
if event["event_type"] == "attendance.punch":
employee = event["employee"]["badge_number"]
direction = event["punch"]["direction"]
timestamp = event["timestamp"]
print(f"Funcionário {employee} bateu ponto {direction} às {timestamp}")
return jsonify({"received": True}), 200

Node.js (Express)

javascript
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = 'whsec_seu_segredo_de_assinatura';
app.post('/webhooks/attendance', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-punchconnect-signature'];
const expected = `sha256=${crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex')}`;
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Assinatura inválida' });
}
const event = JSON.parse(req.body);
if (event.event_type === 'attendance.punch') {
const { badge_number } = event.employee;
const { direction } = event.punch;
console.log(`Funcionário ${badge_number} bateu ponto ${direction} às ${event.timestamp}`);
}
res.json({ received: true });
});
app.listen(3000, () => console.log('Listener webhook ativo na :3000'));

cURL — Registrar Seu Webhook

bash
curl -X POST https://api.punchconnect.com/v1/webhooks \
-H "Authorization: Bearer SUA_CHAVE_API" \
-H "Content-Type: application/json" \
-d '{
"url": "https://seu-app.com/webhooks/attendance",
"events": ["attendance.punch", "device.status"],
"secret": "whsec_seu_segredo_de_assinatura"
}'

Idempotência: Cada Comando É Seguro para Retentar

Falhas de rede acontecem. Sua requisição expira. O dispositivo recebeu o comando ou não? Sem idempotência, você está adivinhando.

PunchConnect aceita um header Idempotency-Key em cada endpoint mutante:

Isso é crítico para comandos de dispositivo — sincronização de listas de funcionários, atualização de regras de acesso. Sem idempotência, um simples retry poderia cadastrar funcionários duas vezes.

bash
curl -X POST https://api.punchconnect.com/v1/devices/dev_9x8y7z/commands \
-H "Authorization: Bearer SUA_CHAVE_API" \
-H "Idempotency-Key: sync-funcionarios-2026-03-28-001" \
-H "Content-Type: application/json" \
-d '{
"command": "sync_employees",
"payload": {
"employees": [
{"badge_number": "1042", "name": "Sarah Chen"},
{"badge_number": "1043", "name": "James Okoro"}
]
}
}'

Rate Limiting Transparente

Cada resposta inclui headers de rate limit:

Quando você atinge o limite, recebe um 429 Too Many Requests com header Retry-After. Limites por padrão: 1.000 chamadas REST por dispositivo por dia. Webhooks entrantes (dispositivo → PunchConnect) são ilimitados.

Conformidade LGPD: Todos os dados transitam em HTTPS. Os webhooks suportam verificação HMAC-SHA256. Os dados de ponto podem ser excluídos via API conforme o direito à eliminação da LGPD (Lei 13.709/2018). PunchConnect é compatível com a Portaria 671 do MTP para registro eletrônico de ponto (REP-P) e com os requisitos de registro de jornada da CLT. Funciona com qualquer provedor de nuvem brasileiro ou internacional.

text
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1711584000

Normalização dos Dados

Dados de dispositivos biométricos são bagunçados: batidas duplicadas, timestamps desalinhados, registros corrompidos. PunchConnect normaliza tudo:

- Detecção de duplicatas — Mesmo funcionário, mesmo dispositivo em janela configurável (padrão: 60 seg) → filtrado
- Normalização de timestamps — Reconciliação relógio dispositivo/servidor, entrega em UTC
- Inferência de direção — Regras configuráveis (primeira batida = entrada, última = saída, ou alternância)
- Filtro de dados inválidos — Registros malformados, timestamps zero, crachás não cadastrados → capturados e logados

O Que 24.000+ Funcionários Nos Ensinaram

Rodar PunchConnect em 50+ sites para os 24.000+ funcionários da AgriWise nos ensinou coisas que nenhuma teoria de design de API poderia:

Drift de relógio é real. Dispositivos em fusos horários e redes instáveis desalinham. Normalização UTC não é opcional.

Buffering offline é obrigatório. Sites agrícolas rurais perdem internet por horas. Os relógios de ponto armazenam batidas localmente. Quando a conectividade volta, tudo é processado em ordem.

Operações batch importam. Sincronizar 5.000 funcionários em 50 dispositivos exige endpoints batch com acompanhamento de progresso e tratamento de falhas parciais.

Perguntas Frequentes

Qual a velocidade de entrega dos webhooks?
Geralmente menos de 2 segundos entre a batida de ponto e a recepção no seu webhook. O gargalo costuma ser o intervalo de push do dispositivo, não o processamento do PunchConnect.

O que acontece se meu endpoint ficar fora do ar por horas?
Os eventos se acumulam e são retentados com backoff exponencial por 72 horas. Quando seu servidor volta, você recebe todos os eventos perdidos em ordem.

Posso reproduzir eventos webhook históricos?
Sim. A API de gerenciamento de webhooks permite reproduzir eventos para um intervalo de tempo.

Preciso tratar entregas webhook duplicadas?
Com entrega ao-menos-uma-vez, é possível receber o mesmo evento duas vezes. Use o campo event_id para deduplicação — é único por evento e estável entre retries.

Como testar webhooks durante o desenvolvimento?
Registre uma URL de webhook apontando para ngrok ou webhook.site. PunchConnect entregará eventos reais dos seus dispositivos de teste para sua máquina local.

---

Construindo uma integração biométrica? PunchConnect dá a você a API que você projetaria — se tivesse seis meses e 24.000 funcionários para testar. Inicie um teste gratuito de 7 dias e receba seu primeiro webhook em menos de 5 minutos. Sem cartão de crédito. Sem IP fixo. Apenas dados limpos, entregues de forma confiável.

Artigos relacionados

Projetando uma API de Ponto Biométrico Que Desenvolvedores Realmente Querem Usar | PunchConnect