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). Ausente nos webhooks de crédito.
CreditobjectSó em CreditConsumed e CreditRefunded — ver Webhooks de crédito.

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.
9AllAttachmentsFilledAssinante enviou todos os anexos exigidos.
10CreditConsumedUm crédito do plano foi consumido. Payload em Credit.
11CreditRefundedCréditos voltaram ao plano (operação cancelada ou expirada). Payload em Credit.

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);

Webhooks de crédito

CreditConsumed (10) e CreditRefunded (11) são financeiros: avisam cada movimento no saldo de créditos do plano e dizem de onde ele veio. O envelope é o mesmo dos demais (Action, ActionDescription, assinatura HMAC, retentativas), mas o conteúdo vem em Credit, e Data não aparece.

  • Um evento por consumo. Criar uma operação com 2 documentos gera um CreditConsumed de Document com Quantity: 2 e outro de Operation com Quantity: 1. Cada SMS, WhatsApp ou validação antifraude gera o seu.
  • Só o que foi efetivado. O evento sai depois que o débito é gravado. Se a criação da operação falha, ou se o provedor recusa o SMS, nada é cobrado e nada é enviado.
  • Estorno. Quando uma operação sem nenhuma assinatura é cancelada ou expira, os créditos de Document, Certificate e AuthenticatedStamp voltam ao plano. Créditos já vencidos não voltam, e por isso o estorno pode trazer uma quantidade menor que a do consumo, ou nem acontecer.

CreditConsumed (Action: 10)

{
  "Action": 10,
  "ActionDescription": "CreditConsumed",
  "Success": true,
  "CreatedAt": "2026-10-01T14:03:22Z",
  "CompanyId": 67,
  "OperationId": 5538,
  "OperationMemberId": 35845,
  "Credit": {
    "EventId": "8f1c2b7e-4c1d-4a8e-9a51-0d6f3b2c9e11",
    "ProductType": "Sms",
    "Quantity": 1,
    "OccurredAt": "2026-10-01T14:03:21Z",
    "RefundReason": null,
    "Operation": { "Id": 48213, "ExternalId": "PED-991", "Name": "Contrato 991", "Status": 2 },
    "Member": { "Id": 35845, "ExternalId": "CLI-5", "Name": "Maria", "Email": "[email protected]", "Phone": "5511999990000" }
  }
}

CreditRefunded (Action: 11)

Mesmo formato. RefundReason diz o motivo, e Member vem null, porque o estorno é da operação inteira.

{
  "Action": 11,
  "ActionDescription": "CreditRefunded",
  "Credit": {
    "EventId": "2a9d0c41-77e3-4f0b-8b2e-5f1c6a3d4b20",
    "ProductType": "Document",
    "Quantity": 2,
    "OccurredAt": "2026-10-03T09:00:04Z",
    "RefundReason": "Cancelled",
    "Operation": { "Id": 48213, "ExternalId": "PED-991", "Name": "Contrato 991", "Status": 6 },
    "Member": null
  }
}
Campo de CreditTipoConteúdo
EventIduuidIdentificador do movimento. Igual em toda reentrega — use para deduplicar.
ProductTypestringDocument, Operation, Certificate, AuthenticatedStamp, Sms, Whatsapp, Liveness, FacialCpfSerpro ou CpfValidation.
QuantityintCréditos consumidos ou devolvidos neste movimento.
OccurredAtISO-8601Quando o débito ou o estorno foi gravado.
RefundReasonstring?Cancelled ou Expired. Só em CreditRefunded.
Operationobject?Operação que originou o movimento. Id é o OperationCompanyId, o mesmo id de Data.Id nos outros webhooks.
Memberobject?Participante que gerou o consumo (SMS, WhatsApp, biometria, CPF). null quando o consumo é da operação (documento, envelope, certificado, carimbo).

O webhook informa créditos, não valores em reais: o preço de cada produto está no seu contrato. Para conciliar, some Quantity por ProductType e subtraia os CreditRefunded.

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 (nos webhooks de crédito, use Credit.EventId):

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