Webhooks
Receba eventos da operação em tempo real, com assinatura HMAC e retry automático.
Em vez de bater na API perguntando "já assinaram?", você registra uma URL e a ForSign te avisa assim que algo relevante acontece — alguém assinou, todos terminaram, alguém preencheu um formulário etc.
Content-Type: application/json. Responda 2xx em até 30s.
Header X-ForSign-Signature: sha256=<hex> quando há Secret.
Backoff fixo: 30s, 2min, 10min. Total ~12 minutos.
Deduplique por Data.Id (OperationCompanyId) + Action.
Configurar no painel
Webhooks são configurados no painel, não pela API:
- Acesse Configurações > Desenvolvedor > Webhooks.
- Clique em Novo webhook e informe:
- Nome — qualquer string descritiva.
- URI — sua URL pública (
https://meusite.com/forsign-hook). - Ação — qual tipo de evento (um webhook = um tipo). Crie múltiplos se quiser receber mais de um.
- Headers customizados (opcional) — pares chave/valor que a ForSign vai
anexar a cada requisição (ex.:
X-Tenant-Id: abc).
- Salve. A ForSign gera automaticamente um
Secretde 32 bytes (base64) — guarde, é o que assina cada payload.
Você pode regenerar o Secret a qualquer momento no painel. Webhooks já em
fila de retry continuam sendo assinados com o segredo que estava ativo na hora
do enfileiramento.
Verificação de assinatura (HMAC)
Quando o webhook tem Secret, toda requisição inclui dois headers extras:
| Header | Conteúdo |
|---|---|
X-ForSign-Signature | sha256=<hex_lowercase> — HMAC-SHA256 do corpo cru. |
X-ForSign-Timestamp | Unix timestamp (segundos) do envio. |
Compute HMAC_SHA256(secret, raw_body) e compare em tempo constante. Use o
corpo bruto — não re-serialize JSON, ou a assinatura quebra.
import crypto from 'node:crypto';
function verify(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const received = signatureHeader.replace(/^sha256=/, '');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(received, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}using System.Security.Cryptography;
using System.Text;
static bool Verify(string rawBody, string signatureHeader, string secret)
{
var key = Encoding.UTF8.GetBytes(secret);
var payload = Encoding.UTF8.GetBytes(rawBody);
using var hmac = new HMACSHA256(key);
var expected = Convert.ToHexString(hmac.ComputeHash(payload)).ToLowerInvariant();
var received = signatureHeader.Replace("sha256=", "");
return CryptographicOperations.FixedTimeEquals(
Encoding.ASCII.GetBytes(expected),
Encoding.ASCII.GetBytes(received));
}import hmac, hashlib
def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
received = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)Estrutura padrão do payload
Todo webhook segue esta forma. Os campos são PascalCase (Action, Data.Id,
Data.Members):
{
"Action": 0,
"ActionDescription": "UpdateOperation",
"Success": true,
"Message": "",
"CreatedAt": "2024-05-16T16:21:35.670887Z",
"CompanyId": 67,
"OperationId": 5538,
"OperationMemberId": null,
"Data": { /* específico do evento */ }
}| Campo | Tipo | Descrição |
|---|---|---|
Action | int | Código numérico do evento (ver tabela abaixo). |
ActionDescription | string | Nome legível do evento — use para rotear. |
Success | bool | true se a ação foi bem-sucedida. |
Message | string | Detalhes extras (opcional, geralmente vazio). |
CreatedAt | ISO-8601 | Quando o evento foi gerado. |
CompanyId | long? | ID da empresa dona da operação. |
OperationId | long? | ID da operação. |
OperationMemberId | long? | Preenchido em eventos por-membro (DocumentSigned, FormFilled). |
Data | object | Payload específico (sempre inclui Id = OperationCompanyId). |
Tipos disponíveis
Action | ActionDescription | Quando dispara |
|---|---|---|
0 | UpdateOperation | Mudou algo na operação (status, assinante, anexo etc.). |
1 | DocumentSigned | Um assinante assinou. Envia Data.Member. |
2 | CompletedOperation | Todos assinaram (ou finalização manual). |
3 | Notification | Obsoleto. Não dispara mais — só fica para registros antigos. |
4 | FailedOperation | Obsoleto. Não dispara mais — só fica para registros antigos. |
5 | DocumentReady | PDFs finais selados e prontos para download. |
6 | AttachmentFilled | Assinante anexou um arquivo solicitado. |
7 | FormFilled | Assinante preencheu os formFields. Inclui FormFields. |
8 | SelfieCaptured | Selfie/antifraude capturada com sucesso. |
Notification (3) e FailedOperation (4) estão marcados como [Obsolete] no
código — não dispare nada novo neles, mas continue tolerando-os se você já
tinha integração antiga.
Eventos em detalhe
UpdateOperation (Action: 0)
Disparado quando algo na operação muda. Use para manter um espelho local.
{
"Action": 0,
"ActionDescription": "UpdateOperation",
"Success": true,
"Data": {
"Id": 5538,
"OperationCompanyId": 5538,
"CreatedAt": "2024-05-16T16:21:35.670887Z",
"Name": "Contrato",
"ExternalId": "user-123",
"CompanyId": 67,
"Source": "Api",
"Version": 3,
"Status": 2,
"Stage": 0,
"HasManualFinish": false,
"Members": [
{ "Id": 35845, "Name": "Fulano", "Email": "[email protected]",
"Phone": "5544...", "Order": 1, "Role": "", "Observer": false,
"Attachments": [], "FormFields": [] }
],
"Files": [
{ "ExternalId": "d7928c35-...", "OriginalName": "Doc-1.pdf",
"Name": "1d9d5fd8-....pdf", "Description": "contrato",
"Size": "114.97 KB", "TotalPages": 3, "Updated": false, "Id": 16087 }
],
"Metadata": [{ "Key": "@module/redirect-url", "Value": "https://..." }]
}
}DocumentSigned (Action: 1)
Disparado quando um assinante assina. Útil para notificar partes internas ou
atualizar UI. Inclui o assinante em Data.Member.
{
"Action": 1,
"ActionDescription": "DocumentSigned",
"Success": true,
"Data": {
"Id": 5538,
"Name": "Contrato",
"Member": {
"Id": 35845, "Name": "Fulano", "Email": "[email protected]",
"Phone": "5544...", "Order": 1, "Role": "", "Observer": false,
"AntifraudResults": [
{ "ValidationType": "Liveness", "Approved": true, "Score": 1.0,
"NumberOfAttempts": 1, "Timestamp": "2024-05-16T16:30:00Z" }
]
}
}
}CompletedOperation (Action: 2)
Disparado quando a operação é finalizada. Não baixe os documentos ainda —
o PDF selado pode ainda estar sendo gerado. Espere DocumentReady.
{
"Action": 2,
"ActionDescription": "CompletedOperation",
"Success": true,
"Data": {
"Id": 5538,
"Status": 5,
"Stage": 4,
"Members": [/* todos */],
"Files": [/* todos */]
}
}DocumentReady (Action: 5)
Disparado quando os PDFs finalizados (com selo digital) estão prontos para download. Esse é o evento certo pra disparar o download via ZIP.
{
"Action": 5,
"ActionDescription": "DocumentReady",
"Success": true,
"Data": {
"Id": 5538,
"CompanyId": 67,
"ExternalId": "user-123"
}
}AttachmentFilled (Action: 6)
Disparado quando um assinante anexa um arquivo solicitado. Data.Member.Attachments
contém o estado atual.
{
"Action": 6,
"ActionDescription": "AttachmentFilled",
"Success": true,
"Data": {
"Id": 5538,
"Member": {
"Id": 35845, "Name": "Fulano",
"Attachments": [
{ "Id": 712, "Name": "RG frente", "FileType": "RG",
"Status": "Pending", "Required": true, "MemberId": 35845 }
]
}
}
}FormFilled (Action: 7)
Disparado quando um membro preenche os campos de formulário (formFields).
Útil para alimentar CRM antes mesmo da assinatura. Os FormFields só
aparecem neste evento — em outros eventos esse array vem vazio.
{
"Action": 7,
"ActionDescription": "FormFilled",
"Success": true,
"Data": {
"Id": 754,
"OperationCompanyId": 754,
"Member": {
"Id": 48022, "Name": "Fulano",
"FormFields": [
{ "Id": 1, "Name": "Nome da mãe", "FieldType": "TextLetter",
"Type": "TextLetter", "Value": "Maria", "Required": false },
{ "Id": 2, "Name": "Plano escolhido", "FieldType": "Select",
"Type": "Select", "Value": "Premium", "Required": true }
]
}
}
}SelfieCaptured (Action: 8)
Disparado quando o assinante submete a selfie no fluxo de antifraude,
independentemente de a foto ter sido aprovada ou reprovada. Os detalhes de cada validação ficam em
Data.Member.AntifraudResults[] — um item por validação realizada
(Liveness, FacialCpfSerpro, CpfSerpro).
{
"Action": 8,
"ActionDescription": "SelfieCaptured",
"Success": true,
"CreatedAt": "2024-05-16T16:25:01Z",
"CompanyId": 67,
"OperationId": 5538,
"OperationMemberId": 35845,
"Data": {
"Id": 5538,
"OperationCompanyId": 5538,
"Member": {
"Id": 35845,
"Name": "Fulano",
"Email": "[email protected]",
"AntifraudResults": [
{ "ValidationType": "Liveness", "Approved": true, "Score": 1.0,
"NumberOfAttempts": 1, "Timestamp": "2024-05-16T16:25:00Z" },
{ "ValidationType": "FacialCpfSerpro", "Approved": true, "Score": 0.9123,
"NumberOfAttempts": 1, "Timestamp": "2024-05-16T16:25:00Z" },
{ "ValidationType": "CpfSerpro", "Approved": true, "Score": null,
"NumberOfAttempts": 1, "Timestamp": "2024-05-16T16:25:00Z" }
]
}
}
}Campo de AntifraudResults[] | Tipo | Conteúdo |
|---|---|---|
ValidationType | string | "Liveness", "FacialCpfSerpro" ou "CpfSerpro". |
Approved | bool | true se a etapa específica passou. |
Score | decimal? | Similaridade Serpro para FacialCpfSerpro. 1.0 para Liveness aprovado. null para CpfSerpro. |
NumberOfAttempts | int | Tentativas acumuladas até o resultado atual. |
Timestamp | datetime? | Momento do desfecho da etapa. |
SelfieCaptured é o único webhook biométrico disparado pela API. Eventos de
bloqueio (AntifraudBlocked) chegam por email ao criador da operação, não
por webhook. Para detalhes sobre os fluxos, status (Approved, Blocked,
Bypassed, ...) e endpoints administrativos de bypass/unblock, veja
Biometria e antifraude.
Os mesmos AntifraudResults aparecem também em DocumentSigned (Action 1) e
CompletedOperation (Action 2), o que dispensa consultar a API separadamente
para conhecer o resultado biométrico final.
Tipagem do payload
export enum WebhookAction {
UpdateOperation = 0,
DocumentSigned = 1,
CompletedOperation = 2,
// 3 e 4 são obsoletos
DocumentReady = 5,
AttachmentFilled = 6,
FormFilled = 7,
SelfieCaptured = 8,
}
export interface WebhookPayload<T = OperationData> {
Action: WebhookAction;
ActionDescription: keyof typeof WebhookAction;
Success: boolean;
Message: string;
CreatedAt: string;
CompanyId: number | null;
OperationId: number | null;
OperationMemberId: number | null;
Data: T;
}
export interface OperationData {
Id: number;
OperationCompanyId: number;
CompanyId: number;
Name: string;
ExternalId: string | null;
CreatedAt: string;
Status: number;
Stage: number;
Source: string;
Version: number;
HasManualFinish: boolean;
Members?: MemberData[];
Member?: MemberData;
Files: FileData[];
Metadata: { Key: string; Value: string }[];
}
export interface MemberData {
Id: number;
Name: string;
Email: string;
Phone: string;
Order: number;
Role: string;
Observer: boolean;
Attachments: AttachmentData[];
FormFields: FormFieldData[];
AntifraudResults?: AntifraudResult[];
}
export interface FileData {
Id: number;
ExternalId: string;
Name: string;
OriginalName: string;
Description: string;
Size: string;
TotalPages: number;
Updated: boolean;
}
export interface AttachmentData {
Id: number;
MemberId: number;
Name: string;
FileType: string;
Status: string;
Required: boolean;
}
export interface FormFieldData {
Id: number;
Name: string;
Type: string;
FieldType: string;
Value: string;
Required: boolean | null;
}
export interface AntifraudResult {
Approved: boolean;
Score: number | null;
NumberOfAttempts: number;
Timestamp: string | null;
ValidationType: 'Liveness' | 'FacialCpfSerpro' | 'CpfSerpro';
}public enum WebhookAction
{
UpdateOperation = 0,
DocumentSigned = 1,
CompletedOperation = 2,
DocumentReady = 5,
AttachmentFilled = 6,
FormFilled = 7,
SelfieCaptured = 8,
}
public record WebhookPayload(
WebhookAction Action,
string ActionDescription,
bool Success,
string Message,
DateTime CreatedAt,
long? CompanyId,
long? OperationId,
long? OperationMemberId,
OperationData Data);
public record OperationData(
long Id,
long OperationCompanyId,
long CompanyId,
string Name,
string? ExternalId,
int Status,
int Stage,
List<MemberData>? Members,
MemberData? Member,
List<FileData> Files);
public record MemberData(long Id, string Name, string Email,
List<FormFieldData> FormFields, List<AntifraudResult>? AntifraudResults);Idempotência
Em raros casos um evento pode ser entregue mais de uma vez (timeout no seu lado
seguido de retry). Use o par Data.Id + Action como chave de deduplicação:
const key = `${payload.Data.Id}:${payload.Action}`;
if (await seen.has(key)) return res.sendStatus(200); // já processei
await seen.add(key, { ttl: '7d' });
// ...processaRetentativas
Quando sua URL responde fora da faixa 2xx ou estoura timeout (30s), a ForSign agenda retries via fila com backoff fixo:
| Tentativa | Delay desde a anterior | Tempo total |
|---|---|---|
| 1 (inicial) | — | 0 |
| 2 | 30s | 30s |
| 3 | 2min | 2min 30s |
| 4 | 10min | 12min 30s |
Total: 3 retries (4 entregas no máximo). Depois disso a ForSign desiste e
loga Max retries reached.
Responda 2xx rápido (em até 30s). Faça o processamento pesado em background — enfileire o payload e responda 200 imediatamente. Se você demorar, o cliente ForSign timeouta e dispara retry, gerando entregas duplicadas.
Reprocessar manualmente
Os logs de webhook ficam guardados. Se você perdeu eventos por um bug no seu lado, acesse Configurações > Desenvolvedor > Webhooks no painel e reenvie cada entrega individualmente a partir do histórico.
Boas práticas
- Tolere campos novos. A ForSign pode adicionar campos no
Datasem aviso — não use parsers estritos que quebram em chaves desconhecidas. - Verifique
Success: trueantes de agir em produção. - Use
DocumentReady(5) — nãoCompletedOperation(2) — para disparar o download dos documentos finalizados. - Headers customizados que você cadastra no painel são reenviados em todas as entregas, inclusive retries. Útil para identificar tenant ou ambiente no seu lado.
- Roteie por
ActionDescriptionem vez deAction: mais legível em logs e imune a mudanças hipotéticas noint.