Docs
Conceitos

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.

HTTP POST

Content-Type: application/json. Responda 2xx em até 30s.

HMAC SHA-256

Header X-ForSign-Signature: sha256=<hex> quando há Secret.

Retry 3x

Backoff fixo: 30s, 2min, 10min. Total ~12 minutos.

Idempotente

Deduplique por Data.Id (OperationCompanyId) + Action.

Configurar no painel

Webhooks são configurados no painel, não pela API:

  1. Acesse Configurações > Desenvolvedor > Webhooks.
  2. 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).
  3. Salve. A ForSign gera automaticamente um Secret de 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:

HeaderConteúdo
X-ForSign-Signaturesha256=<hex_lowercase> — HMAC-SHA256 do corpo cru.
X-ForSign-TimestampUnix 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 */ }
}
CampoTipoDescrição
ActionintCódigo numérico do evento (ver tabela abaixo).
ActionDescriptionstringNome legível do evento — use para rotear.
Successbooltrue se a ação foi bem-sucedida.
MessagestringDetalhes extras (opcional, geralmente vazio).
CreatedAtISO-8601Quando o evento foi gerado.
CompanyIdlong?ID da empresa dona da operação.
OperationIdlong?ID da operação.
OperationMemberIdlong?Preenchido em eventos por-membro (DocumentSigned, FormFilled).
DataobjectPayload específico (sempre inclui Id = OperationCompanyId).

Tipos disponíveis

ActionActionDescriptionQuando dispara
0UpdateOperationMudou algo na operação (status, assinante, anexo etc.).
1DocumentSignedUm assinante assinou. Envia Data.Member.
2CompletedOperationTodos assinaram (ou finalização manual).
3NotificationObsoleto. Não dispara mais — só fica para registros antigos.
4FailedOperationObsoleto. Não dispara mais — só fica para registros antigos.
5DocumentReadyPDFs finais selados e prontos para download.
6AttachmentFilledAssinante anexou um arquivo solicitado.
7FormFilledAssinante preencheu os formFields. Inclui FormFields.
8SelfieCapturedSelfie/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[]TipoConteúdo
ValidationTypestring"Liveness", "FacialCpfSerpro" ou "CpfSerpro".
Approvedbooltrue se a etapa específica passou.
Scoredecimal?Similaridade Serpro para FacialCpfSerpro. 1.0 para Liveness aprovado. null para CpfSerpro.
NumberOfAttemptsintTentativas acumuladas até o resultado atual.
Timestampdatetime?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' });
// ...processa

Retentativas

Quando sua URL responde fora da faixa 2xx ou estoura timeout (30s), a ForSign agenda retries via fila com backoff fixo:

TentativaDelay desde a anteriorTempo total
1 (inicial)0
230s30s
32min2min 30s
410min12min 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 Data sem aviso — não use parsers estritos que quebram em chaves desconhecidas.
  • Verifique Success: true antes de agir em produção.
  • Use DocumentReady (5) — não CompletedOperation (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 ActionDescription em vez de Action: mais legível em logs e imune a mudanças hipotéticas no int.

On this page