net/http e crypto/hmac dão conta das duas pontas.
| Stack | Go 1.23 · biblioteca padrão |
| Você terá | uma rota que emite a credencial e um endpoint de webhook com assinatura conferida |
Confira o exemplo
Os arquivos desta POC, com o destino de cada um.
Emitir a credencial
verification.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
const endpoint = "https://api.legitimuz.com/public/verifications"
type entry struct {
Kind string `json:"kind"`
URL string `json:"url,omitempty"`
}
type legitimuzResponse struct {
Verification struct {
PublicID string `json:"public_id"`
} `json:"verification"`
Entry entry `json:"entry"`
}
var client = &http.Client{Timeout: 10 * time.Second}
func createVerification(w http.ResponseWriter, r *http.Request) {
user, ok := authenticate(r)
if !ok {
http.Error(w, "not_authenticated", http.StatusUnauthorized)
return
}
registration := db.RegistrationByUser(user.ID)
body, _ := json.Marshal(map[string]any{
"schema_version": "1.0",
"ref_id": registration.ID,
"document": map[string]string{"type": "cpf", "number": registration.CPF},
"flow_public_id": os.Getenv("LEGITIMUZ_FLOW_ID"),
})
req, _ := http.NewRequestWithContext(r.Context(), http.MethodPost, endpoint, bytes.NewReader(body))
req.Header.Set("X-API-Key", os.Getenv("LEGITIMUZ_API_KEY"))
req.Header.Set("Content-Type", "application/json")
response, err := client.Do(req)
if err != nil {
http.Error(w, "legitimuz_unavailable", http.StatusBadGateway)
return
}
defer response.Body.Close()
if response.StatusCode >= 400 {
http.Error(w, fmt.Sprintf("legitimuz_%d", response.StatusCode), http.StatusBadGateway)
return
}
var data legitimuzResponse
if err := json.NewDecoder(response.Body).Decode(&data); err != nil {
http.Error(w, "invalid_response", http.StatusBadGateway)
return
}
db.Link(registration.ID, data.Verification.PublicID)
// Só a `entry` volta ao cliente.
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"entry": data.Entry})
}
Receber o desfecho
webhook.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const toleranceSeconds = 300
func isSignatureValid(rawBody []byte, header, secret string) bool {
parts := map[string]string{}
for _, part := range strings.Split(header, ",") {
if key, value, ok := strings.Cut(part, "="); ok {
parts[key] = value
}
}
ts, err := strconv.ParseInt(parts["t"], 10, 64)
if err != nil || parts["v1"] == "" {
return false
}
if d := time.Now().Unix() - ts; d > toleranceSeconds || d < -toleranceSeconds {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.", ts)
mac.Write(rawBody)
return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(parts["v1"]))
}
func receberWebhook(w http.ResponseWriter, r *http.Request) {
// Lê os bytes originais. Um `json.NewDecoder(r.Body)` aqui consumiria o corpo antes da conferência.
rawBody, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if !isSignatureValid(rawBody, r.Header.Get("X-Legitimuz-Signature"), os.Getenv("LEGITIMUZ_WEBHOOK_SECRET")) {
w.WriteHeader(http.StatusUnauthorized)
return
}
if db.RecordIfNew(r.Header.Get("X-Legitimuz-Delivery")) {
queue.Publish(rawBody)
}
w.WriteHeader(http.StatusOK)
}
O
io.LimitReader não é decoração: sem teto, um corpo grande no seu endpoint público vira
consumo de memória sem limite.Antes de rodar
.env
LEGITIMUZ_API_KEY=<SUA_CHAVE>
LEGITIMUZ_WEBHOOK_SECRET=<SEGREDO_DO_ENDPOINT>
LEGITIMUZ_FLOW_ID=<FLOW_PUBLIC_ID>
| Variável | Onde achar |
|---|---|
LEGITIMUZ_API_KEY | Integrações → Segurança → Chaves de API. Aparece uma vez |
LEGITIMUZ_WEBHOOK_SECRET | Integrações → Segurança → Webhooks, na criação do endpoint |
LEGITIMUZ_FLOW_ID | Solução KYC → Fluxos, no menu da linha, em Copiar ID do fluxo |
O que esta POC não faz
POC é código para entender o fluxo, não para copiar em produção. Em todas elas,
authenticate() é
um stub, db é um objeto de mentira e não há migration, observabilidade nem retentativa própria.dbequeuesão variáveis de pacote fictícias.- Sem
contextde cancelamento no worker. - Sem retry na criação. O catálogo de quando repetir está em erros.
Próximo passo
Fila e worker
Processar o desfecho fora do request.
Boas práticas
O que separa uma integração que funciona de uma que gera chamado.