Como Integrar Assinatura Digital em C# / .NET

Se você desenvolve em C# / .NET e precisa coletar assinaturas eletrônicas ou digitais com validade jurídica, este guia mostra o caminho completo: instalar o SDK oficial via NuGet, autenticar com OAuth2, criar uma sessão de assinatura, adicionar signatários, enviar o documento, validar webhooks com HMAC no ASP.NET Core e baixar o PDF assinado — tudo com código idiomático, async/await e HttpClient.

A API do SignDocs Brasil é REST e, portanto, agnóstica de linguagem. Mas usar o SDK .NET acelera bastante o trabalho: ele cuida da renovação do token, da serialização JSON com System.Text.Json e da tipagem dos modelos de requisição e resposta. Ainda assim, mostraremos também como cada passo é feito diretamente com HttpClient, para que você entenda o que acontece "por baixo" e possa integrar mesmo em runtimes mais antigos.

Ao final deste artigo você terá uma integração ponta a ponta funcionando em homologação. Se ainda estiver avaliando a plataforma como um todo, vale começar pela visão geral da API de assinatura digital e pelo quickstart de 5 minutos.

Pré-requisitos para integrar em .NET

Antes de escrever a primeira linha, garanta que seu ambiente atende ao mínimo necessário. A integração é leve e não exige nenhuma dependência nativa específica do Brasil — apenas um runtime .NET moderno e suas credenciais de API.

  • .NET 6, 7 ou 8 (recomendado LTS). O SDK também funciona em .NET Framework 4.7.2+ via REST direto.
  • Uma conta SignDocs com acesso à API. Veja como obter sua API key para gerar client_id e client_secret.
  • Acesso ao ambiente de homologação (api-hml.signdocs.com.br) para testar sem custos. Detalhes em ambiente de homologação / sandbox.
  • Um endpoint HTTPS público (ou um túnel como ngrok) caso queira receber webhooks durante o desenvolvimento.
Dois caminhos de integração. A API expõe duas superfícies. A Transaction API modela envelopes com múltiplos signatários e o ciclo de vida completo. A Assinatura Expressa / Signing Sessions resolve o caso simples — uma única chamada a POST /v1/signing-sessions devolve um checkout hospedado ou, via SDK front-end @signdocs-brasil/js, um checkout embutido em popup. Neste guia usamos predominantemente as signing sessions por serem o atalho mais rápido para colocar algo em produção.

Passo 1 — Instalar o SDK via NuGet

O pacote .NET oficial é distribuído pelo NuGet. Adicione-o ao seu projeto pela CLI do .NET ou pelo Package Manager Console do Visual Studio:

# Via .NET CLI dotnet add package SignDocsBrasil.Api # Ou via Package Manager Console (Visual Studio) Install-Package SignDocsBrasil.Api # F# idiomático (Result-based) — opcional dotnet add package SignDocsBrasil.FSharp

Caso prefira não usar o SDK, você precisa apenas das bibliotecas que já vêm na BCL: System.Net.Http para as requisições e System.Text.Json para a serialização. Nenhum pacote de terceiros é obrigatório para falar com a API REST.

Estrutura recomendada do projeto

Para uma aplicação ASP.NET Core, registre o cliente como serviço via injeção de dependência e mantenha as credenciais fora do código, em appsettings.json ou variáveis de ambiente:

// appsettings.json (use User Secrets / variáveis de ambiente em produção) { "SignDocs": { "BaseUrl": "https://api-hml.signdocs.com.br", "TokenUrl": "https://api-hml.signdocs.com.br/oauth2/token", "ClientId": "seu_client_id", "ClientSecret": "seu_client_secret", "WebhookSecret": "a1b2c3d4e5f6...seu_segredo_de_webhook" } }

Passo 2 — Autenticar com OAuth2 (client-credentials)

A autenticação segue o fluxo OAuth2 client-credentials: você troca client_id e client_secret por um bearer token JWT (assinado com ECDSA ES256), que acompanha cada requisição no header Authorization. O token expira em 15 minutos (expires_in: 900) e deve ser renovado quando vence. Para o detalhamento completo do fluxo, consulte o guia de autenticação OAuth2 da API.

O SDK encapsula isso, mas é instrutivo ver a obtenção do token diretamente com HttpClient:

using System.Net.Http.Json; using System.Text.Json.Serialization; public record TokenResponse( [property: JsonPropertyName("access_token")] string AccessToken, [property: JsonPropertyName("token_type")] string TokenType, [property: JsonPropertyName("expires_in")] int ExpiresIn); public class SignDocsAuth { private readonly HttpClient _http; private readonly IConfiguration _cfg; private TokenResponse? _token; private DateTimeOffset _expiresAt; public SignDocsAuth(HttpClient http, IConfiguration cfg) => (_http, _cfg) = (http, cfg); public async Task<string> GetAccessTokenAsync(CancellationToken ct = default) { // Reutiliza o token enquanto válido (margem de 60s) if (_token is not null && DateTimeOffset.UtcNow < _expiresAt.AddSeconds(-60)) return _token.AccessToken; var body = new Dictionary<string, string> { ["grant_type"] = "client_credentials", ["client_id"] = _cfg["SignDocs:ClientId"]!, ["client_secret"] = _cfg["SignDocs:ClientSecret"]! }; using var resp = await _http.PostAsync( _cfg["SignDocs:TokenUrl"], new FormUrlEncodedContent(body), ct); resp.EnsureSuccessStatusCode(); _token = await resp.Content.ReadFromJsonAsync<TokenResponse>(cancellationToken: ct) ?? throw new InvalidOperationException("Token inválido"); _expiresAt = DateTimeOffset.UtcNow.AddSeconds(_token.ExpiresIn); return _token.AccessToken; } }

Injetando o token automaticamente com um DelegatingHandler

O padrão idiomático em .NET é registrar um HttpClient nomeado via IHttpClientFactory e anexar um DelegatingHandler que adiciona o header Authorization a cada chamada, renovando o token de forma transparente:

public class AuthHandler : DelegatingHandler { private readonly SignDocsAuth _auth; public AuthHandler(SignDocsAuth auth) => _auth = auth; protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken ct) { var token = await _auth.GetAccessTokenAsync(ct); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); return await base.SendAsync(request, ct); } } // Program.cs — registro via injeção de dependência builder.Services.AddSingleton<SignDocsAuth>(); builder.Services.AddTransient<AuthHandler>(); builder.Services.AddHttpClient("signdocs", client => { client.BaseAddress = new Uri( builder.Configuration["SignDocs:BaseUrl"]!); }).AddHttpMessageHandler<AuthHandler>();

Passo 3 — Criar uma sessão de assinatura

Com a autenticação resolvida, criar uma sessão é uma única chamada a POST /v1/signing-sessions. Você descreve o documento, os signatários e a política de assinatura (o profile). Para coletar uma assinatura com certificado ICP-Brasil, use o profile DIGITAL_CERTIFICATE — na API, o certificado é o A1 (em arquivo; para titulares de token A3, o aplicativo SignDocs oferece o assinador desktop). Para assinatura eletrônica, use perfis como CLICK_ONLY e CLICK_PLUS_OTP.

Veja a criação de uma sessão usando o SDK oficial:

using SignDocsBrasil.Api; using SignDocsBrasil.Api.Models; using var client = SignDocsBrasilClient.CreateBuilder() .ClientId(config["SignDocs:ClientId"]!) .ClientSecret(config["SignDocs:ClientSecret"]!) .BaseUrl("https://api-hml.signdocs.com.br") .Build(); var session = await client.SigningSessions.CreateAsync(new CreateSigningSessionRequest { Purpose = "DOCUMENT_SIGNATURE", Policy = new Policy { Profile = "DIGITAL_CERTIFICATE" }, Signer = new Signer { Name = "Maria Silva", Email = "maria@empresa.com.br", UserExternalId = "usr-maria-001", Cpf = "12345678901" // 11 dígitos, sem pontuação }, Document = new CreateSigningSessionRequest.SessionDocument { Content = Convert.ToBase64String( await File.ReadAllBytesAsync("contrato.pdf")), Filename = "Contrato_Prestacao_Servicos.pdf" }, Owner = new Owner { Email = "contratos@minhaempresa.com.br" }, ReturnUrl = "https://minhaempresa.com.br/assinatura/concluida" }); Console.WriteLine($"Sessão criada: {session!.SessionId}"); Console.WriteLine($"Link de assinatura: {session.Url}?cs={session.ClientSecret}");
Atenção ao link de assinatura. O campo Url sozinho não é o link compartilhável. Ele precisa ser combinado com o ClientSecret retornado, no formato {url}?cs={clientSecret}. Além disso, o convite por e-mail só é disparado automaticamente quando o Owner.Email for diferente do e-mail do signatário — caso contrário, entregue você mesmo o link. Veja o fluxo completo no quickstart de 5 minutos.

A mesma chamada com HttpClient puro

Se você não usar o SDK, monte o corpo da requisição com System.Text.Json e poste no client nomeado que já injeta o bearer token:

var http = httpClientFactory.CreateClient("signdocs"); var payload = new { purpose = "DOCUMENT_SIGNATURE", policy = new { profile = "DIGITAL_CERTIFICATE" }, signer = new { name = "Maria Silva", email = "maria@empresa.com.br", userExternalId = "usr-maria-001", cpf = "12345678901" }, document = new { content = Convert.ToBase64String( await File.ReadAllBytesAsync("contrato.pdf")), filename = "Contrato_Prestacao_Servicos.pdf" }, owner = new { email = "contratos@minhaempresa.com.br" } }; using var resp = await http.PostAsJsonAsync("/v1/signing-sessions", payload, ct); resp.EnsureSuccessStatusCode(); using var doc = await JsonDocument.ParseAsync( await resp.Content.ReadAsStreamAsync(ct), cancellationToken: ct); var root = doc.RootElement; var sessionId = root.GetProperty("sessionId").GetString(); var url = root.GetProperty("url").GetString(); var clientSecret = root.GetProperty("clientSecret").GetString(); Console.WriteLine($"Assine em: {url}?cs={clientSecret}");
Cuidado com o profile. O endpoint aceita DIGITAL_CERTIFICATE como policy.profile. Um erro comum é enviar DIGITAL_SIGN_A1 nesse campo — isso retorna HTTP 400. DIGITAL_SIGN_A1 é apenas o tipo de uma etapa (step.type) na resposta, nunca um valor de profile. Para a lista completa de perfis, veja a seção de métodos de autenticação em o que é uma API de assinatura digital.

Passo 4 — Múltiplos signatários e ordem de assinatura

Quando o documento precisa de mais de uma assinatura, use a API de envelopes: um envelope agrupa até 100 signatários sobre o mesmo documento, e o SignerIndex de cada sessão define a posição na fila quando o modo é sequencial (por exemplo, o cliente antes do gestor):

// 1) Criar o envelope sequencial var envelope = await client.Envelopes.CreateAsync(new CreateEnvelopeRequest { SigningMode = "SEQUENTIAL", TotalSigners = 2, DocumentContent = Convert.ToBase64String(pdfBytes), DocumentFilename = "Acordo_Confidencialidade.pdf", }); // 2) Uma sessão por signatário, na ordem da fila var s1 = await client.Envelopes.AddSessionAsync(envelope!.EnvelopeId!, new AddEnvelopeSessionRequest { SignerName = "Maria Silva", SignerEmail = "maria@cliente.com.br", PolicyProfile = "DIGITAL_CERTIFICATE", SignerIndex = 1, }); var s2 = await client.Envelopes.AddSessionAsync(envelope.EnvelopeId!, new AddEnvelopeSessionRequest { SignerName = "João Gestor", SignerEmail = "joao@minhaempresa.com.br", PolicyProfile = "DIGITAL_CERTIFICATE", SignerIndex = 2, }); Console.WriteLine($"Envelope {envelope.EnvelopeId}: {s1!.Url} / {s2!.Url}");

Para um aprofundamento na sequência de coleta e nos modos sequencial vs. paralelo, consulte ordem de assinatura com múltiplos signatários; o ciclo de vida completo está em fluxos transacionais da API.

Passo 5 — Receber e validar webhooks no ASP.NET Core

Em vez de ficar consultando o status repetidamente (polling), o correto é registrar um webhook e deixar a API notificar sua aplicação a cada evento (SIGNING_SESSION.COMPLETED, TRANSACTION.COMPLETED, etc.). Cada entrega traz uma assinatura HMAC-SHA256 que você precisa validar para garantir a autenticidade. O detalhamento dos eventos e da estratégia de retry está em webhooks e eventos da API.

A regra de ouro no ASP.NET Core é validar a assinatura sobre o corpo bruto (raw body), antes de qualquer desserialização. Veja uma implementação com Minimal API:

using System.Security.Cryptography; using System.Text; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapPost("/webhooks/signdocs", async (HttpRequest req, IConfiguration cfg) => { // 1. Ler o corpo bruto SEM desserializar ainda req.EnableBuffering(); using var reader = new StreamReader(req.Body, Encoding.UTF8, leaveOpen: true); var rawBody = await reader.ReadToEndAsync(); req.Body.Position = 0; var signature = req.Headers["X-SignDocs-Signature"].ToString(); var timestamp = req.Headers["X-SignDocs-Timestamp"].ToString(); // 2. Validar timestamp (tolerância de 5 min) contra replay attacks if (!long.TryParse(timestamp, out var ts) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300) return Results.Unauthorized(); // 3. Recalcular o HMAC-SHA256 sobre "{timestamp}.{rawBody}" var secret = cfg["SignDocs:WebhookSecret"]!; var signedPayload = $"{timestamp}.{rawBody}"; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload)); var expected = Convert.ToHexString(hash).ToLowerInvariant(); // 4. Comparação timing-safe contra timing attacks var valid = CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(signature)); if (!valid) return Results.Unauthorized(); // 5. Só agora desserializar e enfileirar para processamento assíncrono var evt = System.Text.Json.JsonSerializer .Deserialize<WebhookEvent>(rawBody)!; // Enfileire (Channel, fila, SQS...) e responda 200 rapidamente _ = Task.Run(() => ProcessEventAsync(evt)); return Results.Ok(new { status = "received" }); }); app.Run(); record WebhookEvent( [property: JsonPropertyName("id")] string Id, [property: JsonPropertyName("eventType")] string EventType, [property: JsonPropertyName("tenantId")] string TenantId, [property: JsonPropertyName("transactionId")] string? TransactionId, [property: JsonPropertyName("timestamp")] string Timestamp, [property: JsonPropertyName("data")] JsonElement Data);
Segurança crítica: sempre use CryptographicOperations.FixedTimeEquals para comparar a assinatura. Comparações comuns com == sobre strings são vulneráveis a timing attacks, em que um atacante infere a assinatura correta medindo diferenças de tempo de resposta. Responda HTTP 200 em poucos segundos e deixe qualquer trabalho pesado para uma fila — webhooks operam com semântica at-least-once, então seu handler deve ser idempotente, deduplicando pelo Id do evento. Se preferir não implementar a validação na mão, o SDK expõe o helper WebhookVerifier, que aplica exatamente essa checagem (HMAC sobre {timestamp}.{corpo}, tolerância de replay e comparação em tempo constante).

Passo 6 — Baixar o PDF assinado

Quando você recebe o evento TRANSACTION.COMPLETED, o documento final assinado — no padrão PAdES (nível baseline: assinatura + certificado + carimbo de hora do servidor + trilha de auditoria), com validade jurídica — fica disponível para download. O SDK devolve URLs temporárias de download; baixe o arquivo por streaming, sem carregar tudo na memória:

public async Task DownloadSignedPdfAsync( string transactionId, string destPath, CancellationToken ct) { // 1. Obter as URLs temporárias de download da transação var download = await client.Documents.DownloadAsync(transactionId, ct: ct); // 2. Baixar o PDF carimbado (SignedUrl) por streaming var http = _httpClientFactory.CreateClient(); using var resp = await http.GetAsync( download!.SignedUrl, HttpCompletionOption.ResponseHeadersRead, ct); resp.EnsureSuccessStatusCode(); await using var source = await resp.Content.ReadAsStreamAsync(ct); await using var file = File.Create(destPath); await source.CopyToAsync(file, ct); // 3. Evidence pack (.p7m): metadados + downloadUrl temporária var evidence = await client.Evidence.GetAsync(transactionId, ct: ct); Console.WriteLine($"PDF salvo em {destPath}; evidenceId: {evidence!.EvidenceId}"); }

Além do PDF, a plataforma gera um pacote de evidências (.p7m) com a trilha de auditoria completa — IPs, geolocalização, métodos de autenticação e carimbos de tempo. Esse pacote é a prova jurídica da assinatura; saiba mais em evidence pack e prova jurídica. Qualquer pessoa pode conferir a autenticidade do documento no verificador público.

SDK oficial vs. REST puro: qual escolher?

Ambos os caminhos são válidos e atingem o mesmo resultado. A tabela abaixo ajuda a decidir conforme o contexto do seu projeto .NET:

Critério SDK oficial (NuGet) REST puro (HttpClient)
Tipagem dos modelos Classes/records prontos para request e response Você define os DTOs ou usa JsonElement
Renovação do token OAuth2 Automática, transparente Você implementa (ex.: DelegatingHandler)
Dependências externas Um pacote NuGet Apenas a BCL (System.Net.Http, System.Text.Json)
Compatibilidade legada Otimizado para .NET moderno Funciona em .NET Framework 4.7.2+
Velocidade de implementação Mais rápida para o caminho feliz Mais controle, porém mais código
Recomendado para Maioria dos projetos novos Casos com requisitos específicos de HTTP ou runtimes antigos
Dica de produção. Independentemente do caminho, prefira IHttpClientFactory em vez de instanciar new HttpClient() manualmente — isso evita o esgotamento de sockets sob carga e habilita resiliência (retry, circuit breaker) via Polly. Para padrões REST além do básico — paginação, idempotência e tratamento de erros — vale conferir o guia de integração via REST puro com cURL, que vale para qualquer linguagem.

Boas práticas para a integração .NET em produção

  • Nunca versione o client_secret. Use User Secrets em desenvolvimento e variáveis de ambiente, AWS Secrets Manager ou Azure Key Vault em produção.
  • Tudo assíncrono. Use async/await ponta a ponta, propague CancellationToken e evite .Result ou .Wait(), que causam deadlocks.
  • Idempotência nos webhooks. Deduplique pelo id do evento (em cache ou banco) para tratar entregas repetidas com segurança.
  • Responda rápido aos webhooks. Valide, enfileire e retorne 200. Processe o evento em um BackgroundService ou fila externa.
  • Comece em homologação. Aponte a BaseUrl para api-hml.signdocs.com.br e lembre que as entidades de teste expiram em 7 dias.
  • Trate erros HTTP explicitamente. Use EnsureSuccessStatusCode com tratamento de HttpRequestException e logue o corpo de erro retornado pela API.

Se a sua integração for em outras stacks além do .NET, temos guias equivalentes — por exemplo, integrar assinatura digital em Java segue a mesma estrutura de OAuth2, sessões e webhooks, adaptada ao ecossistema JVM.

SignDocs: SDK .NET pronto para ICP-Brasil. O SDK oficial via NuGet, a Assinatura Expressa de uma chamada e os webhooks com HMAC-SHA256 deixam sua aplicação C# / .NET coletando assinaturas com validade jurídica em minutos — e em conformidade com a LGPD. Fale com nosso time para receber as credenciais do sandbox gratuito e a proposta sob medida — incluindo mTLS para clientes regulados.

Perguntas Frequentes

Existe um SDK oficial de assinatura digital para C# / .NET?

Sim. O SignDocs Brasil mantém SDKs oficiais para TypeScript/Node, Python, Go, Java, PHP e C# / .NET. O pacote .NET é distribuído via NuGet e é compatível com .NET 6, 7 e 8, além de funcionar em projetos ASP.NET Core, worker services e aplicações console. O SDK encapsula a autenticação OAuth2, a serialização JSON e a renovação automática do token de acesso, mas você sempre pode recorrer à API REST diretamente com HttpClient caso prefira.

Qual versão do .NET preciso para usar a API de assinatura digital?

Recomendamos .NET 6 ou superior (LTS), pois oferece suporte nativo a async/await, HttpClient com IHttpClientFactory, System.Text.Json e minimal APIs no ASP.NET Core. A integração também funciona em .NET Framework 4.7.2+ usando chamadas REST diretas, mas o SDK oficial é otimizado para .NET moderno. Como a API é REST e baseada em HTTPS, qualquer runtime capaz de fazer requisições TLS 1.2+ consegue integrar.

Como autenticar minha aplicação .NET na API de assinatura?

A autenticação usa o fluxo OAuth2 client-credentials. Sua aplicação envia o client_id e client_secret ao endpoint de token e recebe um bearer token JWT, assinado com ECDSA (ES256) e com validade de 15 minutos, que deve ser incluído no header Authorization de cada requisição. Em .NET, o ideal é registrar um HttpClient nomeado via IHttpClientFactory com um DelegatingHandler que injeta e renova o token automaticamente, mantendo o client_secret em IConfiguration / secrets de ambiente, nunca no código-fonte.

Como validar a assinatura HMAC de um webhook no ASP.NET Core?

Leia o corpo bruto (raw body) da requisição antes de qualquer desserialização, calcule o HMAC-SHA256 do payload usando o webhook secret com a classe HMACSHA256 do namespace System.Security.Cryptography e compare com o header de assinatura recebido usando CryptographicOperations.FixedTimeEquals para evitar timing attacks. Valide também o timestamp para mitigar replay attacks. Só então desserialize o evento e responda com HTTP 200 rapidamente, deixando o processamento pesado para uma fila assíncrona.

Posso testar a integração .NET em um ambiente de homologação?

Sim. O SignDocs oferece um ambiente de homologação (sandbox) com host base api-hml.signdocs.com.br, onde você usa credenciais de teste e simula todo o ciclo de assinatura sem custos nem efeitos legais. Basta apontar a BaseUrl do SDK ou do HttpClient para o host de homologação. Vale lembrar que as entidades criadas em homologação têm TTL de 7 dias e são removidas automaticamente, então não dependa delas para testes de longa duração.

A API .NET suporta assinatura com certificado ICP-Brasil (A1 e A3)?

Sim. Ao criar a sessão ou transação você define o profile da política de assinatura como DIGITAL_CERTIFICATE para exigir certificado ICP-Brasil A1 — o formato usado em integrações via API, com um protocolo em duas fases em que a chave privada nunca sai do titular (para tokens A3, o aplicativo SignDocs oferece o assinador desktop). A assinatura digital acontece no fluxo conduzido pelo signatário; sua aplicação .NET orquestra a criação da sessão, o envio e o acompanhamento dos eventos, recebendo ao final um PDF no padrão PAdES com validade jurídica conforme a MP 2.200-2/2001.

Integre assinatura digital em C# / .NET hoje mesmo

SDK oficial via NuGet, OAuth2, Assinatura Expressa de uma chamada, webhooks com HMAC-SHA256 e certificado ICP-Brasil — tudo com validade jurídica e conformidade LGPD. O acesso à API é um plano sob medida, com sandbox de homologação gratuito para colocar sua aplicação .NET assinando antes de qualquer contrato.

Fale com o time comercial Conheça a plataforma grátis