Como Integrar Assinatura Digital em Go (Golang)

Se você está construindo um backend em Go e precisa coletar assinaturas com validade jurídica no Brasil, este guia é um quickstart prático de ponta a ponta. Vamos do go get do SDK oficial até o download do PDF assinado, passando por autenticação OAuth2, criação de sessão de assinatura, adição de signatários e verificação de webhooks com HMAC — sempre com código Go idiomático, tratamento de erros explícito e context.Context.

O Go é uma escolha excelente para integrações de assinatura: tipagem estática, concorrência barata e uma biblioteca padrão robusta para HTTP, JSON e criptografia. A API de assinatura digital da SignDocs Brasil foi desenhada para ser previsível e amigável a esse perfil de aplicação, com um SDK Go que empacota a parte chata (auth, retry, tipos) e deixa você focar na lógica de negócio.

Você não precisa ser um especialista em criptografia ICP-Brasil para começar. Em poucos minutos terá uma sessão de assinatura criada e um link pronto para enviar ao signatário. Se preferir ver o panorama maior primeiro, vale a leitura de o que é uma API de assinatura digital, que cobre os conceitos independentes de linguagem.

Por que Go para integrar assinatura digital

Antes do código, vale entender por que o Go cai tão bem nesse tipo de integração. Boa parte do trabalho de uma integração de assinatura é I/O de rede com APIs externas, somada a um receptor de webhooks que precisa ser rápido e seguro. Esse é exatamente o ponto forte da linguagem.

  • Concorrência nativa: goroutines e channels tornam trivial enviar dezenas de documentos em paralelo sem travar threads.
  • Biblioteca padrão completa: net/http, encoding/json, crypto/hmac e context cobrem quase tudo sem dependências externas.
  • Tipagem estática: os payloads da API viram structs, e o compilador pega erros de campo antes de chegar à produção.
  • Binário único: seu receptor de webhooks compila para um binário sem dependências, ideal para containers enxutos e funções serverless.
  • Cancelamento limpo: context.Context propaga deadlines e cancelamentos, evitando goroutines vazadas em chamadas lentas.

A SignDocs Brasil oferece SDKs oficiais para Go, TypeScript/Node, Python, Java, PHP e C#/.NET. Se você trabalha com mais de uma stack, vale comparar com os guias de integração em Node.js e integração em Python — a API é a mesma, muda apenas o idioma do cliente.

Passo 1: Instalar o SDK Go

Comece criando um módulo Go para a sua integração e instalando o SDK oficial. O SDK usa apenas a biblioteca padrão — zero dependências externas em runtime.

# Inicializa um novo módulo Go go mod init github.com/sua-empresa/assinatura-service # Instala o SDK oficial da SignDocs Brasil go get github.com/signdocsbrasil/signdocsbrasil-go

Em seguida, configure suas credenciais. Nunca coloque client_id e client_secret direto no código — use variáveis de ambiente ou um gerenciador de segredos:

export SIGNDOCS_CLIENT_ID="seu_client_id" export SIGNDOCS_CLIENT_SECRET="seu_client_secret" # Homologação (sandbox). Em produção, remova esta linha. export SIGNDOCS_BASE_URL="https://api-hml.signdocs.com.br"
Onde obter as credenciais: O client_id e o client_secret são gerados no painel da SignDocs Brasil. Crie um par de chaves dedicado para cada ambiente (homologação e produção) e nunca reutilize as mesmas credenciais entre eles. O passo a passo de geração está em como obter suas credenciais da API; o protocolo em si, no guia de autenticação OAuth2.

Passo 2: Autenticar com OAuth2 client-credentials

A API usa o fluxo OAuth2 client-credentials: você troca suas credenciais por um token bearer de curta duração — um JWT assinado com ECDSA ES256, com as chaves protegidas em KMS, que expira em 15 minutos — e o envia no header Authorization de cada chamada. O SDK Go faz essa troca e renova o token automaticamente quando ele expira.

Usando o SDK oficial

Inicializar o client é direto. Forneça as credenciais e, opcionalmente, o BaseURL do ambiente:

package main import ( "context" "encoding/base64" "fmt" "log" "os" "time" signdocs "github.com/signdocsbrasil/signdocsbrasil-go" ) func newClient() (*signdocs.Client, error) { client, err := signdocs.NewClient( os.Getenv("SIGNDOCS_CLIENT_ID"), signdocs.WithClientSecret(os.Getenv("SIGNDOCS_CLIENT_SECRET")), // Aponta para o sandbox em homologação; omita em produção. signdocs.WithBaseURL(os.Getenv("SIGNDOCS_BASE_URL")), signdocs.WithTimeout(30*time.Second), ) if err != nil { return nil, fmt.Errorf("falha ao criar client: %w", err) } return client, nil }

Sem o SDK: net/http puro

Como a API REST é agnóstica de linguagem, você pode usar a biblioteca padrão. O pacote golang.org/x/oauth2/clientcredentials cuida da obtenção e renovação do token, devolvendo um *http.Client já autenticado:

import ( "context" "net/http" "golang.org/x/oauth2/clientcredentials" ) func oauthClient(ctx context.Context) *http.Client { cfg := &clientcredentials.Config{ ClientID: os.Getenv("SIGNDOCS_CLIENT_ID"), ClientSecret: os.Getenv("SIGNDOCS_CLIENT_SECRET"), TokenURL: "https://api.signdocs.com.br/oauth2/token", } // Retorna um *http.Client que injeta e renova o bearer token. return cfg.Client(ctx) }

Repare no uso de fmt.Errorf com o verbo %w para encapsular o erro original: esse é o padrão idiomático em Go para preservar a cadeia de erros e permitir inspeção posterior com errors.Is e errors.As. Trataremos os erros da API de forma estruturada mais adiante.

Passo 3: Criar uma sessão de assinatura

A forma mais rápida de coletar uma assinatura é a Assinatura Expressa (signing sessions): uma única chamada a POST /v1/signing-sessions cria a sessão e devolve a URL de checkout hospedado e o clientSecret para o embed em popup (via SDK front-end @signdocs-brasil/js). É o caminho ideal para fluxos de um signatário. Para múltiplos signatários, existe a API de envelopes (Passo 4); o ciclo de vida completo está no guia do fluxo transacional.

Vamos criar uma sessão usando o SDK. Note o uso de context.WithTimeout para limitar o tempo da chamada de rede:

func createSession(ctx context.Context, client *signdocs.Client, pdf []byte) (*signdocs.SigningSession, error) { ctx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() session, err := client.SigningSessions.Create(ctx, &signdocs.CreateSigningSessionRequest{ Purpose: signdocs.TransactionPurposeDocumentSignature, // Política de assinatura. DIGITAL_CERTIFICATE exige cert ICP-Brasil (A1). Policy: signdocs.Policy{ Profile: "DIGITAL_CERTIFICATE", }, Signer: signdocs.Signer{ Name: "Maria Silva", Email: "maria@empresa.com.br", UserExternalID: "usr-maria-001", CPF: "12345678901", // 11 dígitos, sem pontuação }, Document: &signdocs.DocumentRequest{ Content: base64.StdEncoding.EncodeToString(pdf), Filename: "Contrato_Prestacao_Servicos.pdf", }, ReturnURL: "https://sua-empresa.com/assinatura-concluida", }) if err != nil { return nil, fmt.Errorf("erro ao criar sessão de assinatura: %w", err) } // session.URL + "?cs=" + session.ClientSecret é o link de assinatura. return session, nil }
Atenção ao profile: O valor DIGITAL_CERTIFICATE é o que ativa a exigência de certificado ICP-Brasil — na API, o A1 (para tokens A3, o aplicativo SignDocs oferece o assinador desktop). Um erro comum é tentar usar DIGITAL_SIGN_A1 como profile — isso é rejeitado com HTTP 400. DIGITAL_SIGN_A1 existe apenas como tipo de etapa (step type) dentro da resposta. Os demais perfis (CLICK_ONLY, CLICK_PLUS_OTP, BIOMETRIC, BIOMETRIC_PLUS_OTP) cobrem os níveis eletrônicos; veja a lista completa em o que é uma API de assinatura digital.

Passo 4: Múltiplos signatários com envelopes

Na Assinatura Expressa, o signatário já vai na própria criação da sessão — não há passo separado de "adicionar e enviar". Quando o documento precisa de mais de uma assinatura, o modelo é o envelope: você cria o envelope (com modo sequencial ou paralelo) e adiciona uma sessão por signatário, com a posição na fila dada pelo SignerIndex:

// 1) Criar o envelope sequencial env, err := client.Envelopes.Create(ctx, &signdocs.CreateEnvelopeRequest{ SigningMode: "SEQUENTIAL", TotalSigners: 2, Document: signdocs.EnvelopeDocument{ Content: base64.StdEncoding.EncodeToString(pdf), Filename: "contrato.pdf", }, }) if err != nil { return fmt.Errorf("erro ao criar envelope: %w", err) } // 2) Uma sessão por signatário, na ordem da fila (SignerIndex 1..N) s1, err := client.Envelopes.AddSession(ctx, env.EnvelopeID, &signdocs.AddEnvelopeSessionRequest{ Signer: signdocs.EnvelopeSessionSigner{ Name: "Maria Silva", Email: "maria@cliente.com.br", UserExternalID: "usr-maria-001", }, Policy: signdocs.EnvelopeSessionPolicy{Profile: "DIGITAL_CERTIFICATE"}, Purpose: "DOCUMENT_SIGNATURE", SignerIndex: 1, }) s2, err := client.Envelopes.AddSession(ctx, env.EnvelopeID, &signdocs.AddEnvelopeSessionRequest{ Signer: signdocs.EnvelopeSessionSigner{ Name: "João Gestor", Email: "joao@empresa.com.br", UserExternalID: "usr-joao-002", }, Policy: signdocs.EnvelopeSessionPolicy{Profile: "CLICK_PLUS_OTP"}, Purpose: "DOCUMENT_SIGNATURE", SignerIndex: 2, }) fmt.Println(s1.URL, s2.URL) // cada sessão tem sua própria URL de assinatura

No modo SEQUENTIAL, a plataforma impõe a fila: o signatário 2 só consegue concluir depois que o 1 termina. Um envelope aceita até 100 signatários, cada um com sua própria política de autenticação. O detalhamento dos modos está em ordem de assinatura com múltiplos signatários.

Tratamento de erros estruturado

Erros da API chegam tipados pelo SDK, o que permite reagir de forma específica usando errors.As. Trate rate limiting e erros de validação separadamente de falhas genéricas:

// Erros tipados: BadRequestError, UnauthorizedError, RateLimitError... var rateErr *signdocs.RateLimitError if errors.As(err, &rateErr) { // Respeite o Retry-After antes de tentar de novo. log.Printf("rate limit atingido, aguardar %ds", rateErr.RetryAfterSeconds) return } var apiErr *signdocs.ApiError if errors.As(err, &apiErr) { // Erros seguem RFC 7807 (application/problem+json) log.Printf("erro da API (%d): %s", apiErr.StatusCode, apiErr.ProblemDetail.Detail) }

Passo 5: Receber e verificar webhooks

Em vez de ficar consultando o status em loop (polling), o correto é deixar a API avisar você via webhook sempre que um evento acontece: SIGNING_SESSION.COMPLETED, TRANSACTION.COMPLETED, STEP.FAILED, e assim por diante. Os fundamentos da arquitetura de eventos estão no guia de webhooks e eventos da API; aqui focamos na implementação do receptor em Go.

Cada webhook chega como um POST HTTPS com o corpo em JSON e um header contendo a assinatura HMAC-SHA256 do corpo. Sua obrigação é recalcular esse HMAC com o segredo compartilhado e comparar de forma timing-safe. Veja um handler net/http completo e idiomático:

package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "log" "net/http" "os" "strconv" "time" ) var webhookSecret = os.Getenv("SIGNDOCS_WEBHOOK_SECRET") const timestampTolerance = 300 // 5 minutos, em segundos // verifySignature recalcula o HMAC-SHA256 e compara de forma timing-safe. func verifySignature(body []byte, signature, timestamp string) bool { // 1. Validar o timestamp para mitigar replay attacks. ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return false } if diff := time.Now().Unix() - ts; diff > timestampTolerance || diff < -timestampTolerance { return false } // 2. Recalcular o HMAC sobre "timestamp.body". mac := hmac.New(sha256.New, []byte(webhookSecret)) mac.Write([]byte(timestamp)) mac.Write([]byte(".")) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) // 3. Comparação timing-safe (NUNCA use ==). return hmac.Equal([]byte(expected), []byte(signature)) } type WebhookEvent struct { ID string `json:"id"` EventType string `json:"eventType"` TenantID string `json:"tenantId"` TransactionID string `json:"transactionId,omitempty"` Timestamp string `json:"timestamp"` Data json.RawMessage `json:"data"` } func webhookHandler(w http.ResponseWriter, r *http.Request) { // Lê o corpo bruto: é ele que assina o HMAC. body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) if err != nil { http.Error(w, "corpo inválido", http.StatusBadRequest) return } sig := r.Header.Get("X-SignDocs-Signature") ts := r.Header.Get("X-SignDocs-Timestamp") if !verifySignature(body, sig, ts) { http.Error(w, "assinatura inválida", http.StatusUnauthorized) return } var evt WebhookEvent if err := json.Unmarshal(body, &evt); err != nil { http.Error(w, "json inválido", http.StatusBadRequest) return } // Idempotência + processamento assíncrono (ver abaixo). if !markProcessed(evt.ID) { w.WriteHeader(http.StatusOK) // duplicata: confirma e ignora return } go handleEvent(evt) // processa fora do caminho da resposta // Responda 200 imediatamente, dentro do timeout. w.WriteHeader(http.StatusOK) } func main() { http.HandleFunc("/webhooks/signdocs", webhookHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
Segurança crítica: Use sempre hmac.Equal para comparar assinaturas — ou delegue ao helper do SDK, signdocs.VerifyWebhookSignature(body, sig, ts, secret), que faz exatamente essa validação. Comparar com == ou bytes.Equal ingênuo abre brecha para timing attacks, onde um atacante infere a assinatura correta medindo o tempo de resposta. Leia o corpo bruto com io.ReadAll antes de qualquer parsing: re-serializar o JSON altera bytes e quebra o HMAC.

Idempotência: o mesmo evento pode chegar duas vezes

Webhooks operam com semântica de entrega ao menos uma vez. Um timeout de rede após o processamento, ou um retry da API, pode reentregar o mesmo evento. Por isso o handler acima usa markProcessed, que registra o id do evento de forma atômica e retorna false se já foi visto. Com Redis, fica assim:

// markProcessed retorna true se o evento é novo; false se já foi processado. func markProcessed(eventID string) bool { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() // SetNX é atômico: grava só se a chave ainda não existe. ok, err := rdb.SetNX(ctx, "webhook:"+eventID, "1", 7*24*time.Hour).Result() if err != nil { log.Printf("erro de dedup, processando mesmo assim: %v", err) return true // fail-open: prefira reprocessar a perder o evento } return ok }

Passo 6: Baixar o PDF assinado

Quando o evento TRANSACTION.COMPLETED chega, o documento final assinado está disponível para download. O SDK devolve URLs temporárias; a integração fecha o ciclo buscando o PDF e armazenando-o no seu repositório:

func downloadSigned(ctx context.Context, client *signdocs.Client, transactionID string) error { ctx, cancel := context.WithTimeout(ctx, 60*time.Second) defer cancel() // 1. Obter as URLs temporárias de download da transação. dl, err := client.Documents.Download(ctx, transactionID) if err != nil { return fmt.Errorf("erro ao obter URLs de download: %w", err) } // 2. Baixar o PDF carimbado (SignedURL) por streaming. req, _ := http.NewRequestWithContext(ctx, http.MethodGet, dl.SignedURL, nil) resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("erro ao baixar documento assinado: %w", err) } defer resp.Body.Close() out, err := os.Create("contrato_assinado.pdf") if err != nil { return fmt.Errorf("erro ao criar arquivo: %w", err) } defer out.Close() if _, err := io.Copy(out, resp.Body); err != nil { return fmt.Errorf("erro ao gravar PDF: %w", err) } // 3. Evidence pack (.p7m): metadados + downloadUrl temporária. ev, err := client.Evidence.Get(ctx, transactionID) if err == nil { fmt.Println("evidenceId:", ev.EvidenceID) } return nil }

O PDF baixado é um documento PAdES com a assinatura embutida, o carimbo de hora do servidor (timestamp ISO-8601 registrado pelo SignDocs) e a cadeia de certificados ICP-Brasil. Além do PDF, você pode obter o evidence pack em formato .p7m, que reúne toda a trilha de auditoria como prova jurídica — assunto do guia sobre evidence pack e prova jurídica. Qualquer pessoa pode validar o documento depois no verificador público.

Assinatura Expressa vs. Transaction API em Go

A SignDocs expõe duas superfícies de API, e a escolha entre elas define a estrutura do seu código Go. A tabela abaixo resume quando usar cada uma:

Critério Assinatura Expressa (Signing Sessions) Transaction API (Envelopes)
Chamadas para iniciar Uma (POST /v1/signing-sessions) Duas por signatário (criar envelope + uma sessão por signatário)
Signatários Foco em um signatário Até 100, com ordem via SignerIndex
Entrega Checkout hospedado ou embed em popup via SDK @signdocs-brasil/js Convites por e-mail/link, fluxo orquestrado
Tempo até o primeiro envio Minutos Algumas horas de integração
Ideal para Onboarding, aceite de termos, contratos B2C Contratos B2B, fluxos multipartes, RH, jurídico

A boa notícia é que ambas usam o mesmo client autenticado e os mesmos webhooks. Você pode começar pela Expressa para validar o produto e migrar trechos do fluxo para a Transaction API conforme a complexidade cresce, sem reescrever a camada de autenticação ou o receptor de webhooks.

Checklist para ir à produção

Antes de apontar sua integração Go para produção, valide tudo no ambiente de homologação. O host base do sandbox é api-hml.signdocs.com.br (com hífen, não api.hml) e as entidades criadas lá têm TTL de 7 dias, expirando automaticamente.

  1. Credenciais separadas por ambiente: use pares distintos de client_id/client_secret para HML e produção, sem reaproveitar.
  2. Segredos fora do código: mantenha client_secret e webhook_secret em variáveis de ambiente ou em um cofre (Vault, KMS, Secrets Manager).
  3. Timeouts e contextos: todas as chamadas de rede com context.WithTimeout; nunca use context.Background() diretamente em chamadas longas.
  4. Webhook idempotente e timing-safe: hmac.Equal para assinatura, dedup pelo id do evento e resposta 200 imediata.
  5. Retry com backoff: respeite o header Retry-After em respostas 429 e aplique backoff exponencial em falhas transitórias.
  6. Observabilidade: registre o id do evento e o transactionId em logs estruturados para reconciliação.
  7. Endpoint HTTPS público: o receptor de webhooks precisa de TLS 1.2+ e ser acessível pela internet (use API Gateway ou um proxy reverso em produção).
mTLS para clientes regulados: Se sua aplicação opera em contexto BACEN ou Open Finance, a SignDocs oferece mTLS (mutual TLS) além do OAuth2. Em Go, isso significa configurar um tls.Config com o certificado do cliente no http.Transport. Os detalhes estão no guia de segurança mTLS para enterprise.

Perguntas Frequentes

Existe um SDK oficial de assinatura digital para Go?

Sim. A SignDocs Brasil mantém SDKs oficiais para Go, TypeScript/Node, Python, Java, PHP e C#/.NET. O SDK Go é instalado com go get e empacota a autenticação OAuth2, a renovação automática do token bearer, os tipos das requisições e respostas e os helpers de verificação de webhook. Como a API REST é agnóstica de linguagem, você também pode chamar os endpoints diretamente com net/http caso prefira não depender de um pacote externo.

Como autenticar na API de assinatura a partir de uma aplicação Go?

A autenticação usa o fluxo OAuth2 client-credentials. Você troca o client_id e o client_secret por um token bearer de curta duração (15 minutos), assinado com ECDSA (ES256), e o envia no header Authorization de cada requisição. Em Go, o pacote golang.org/x/oauth2/clientcredentials cuida da obtenção e da renovação automática do token, retornando um *http.Client já autenticado. O SDK oficial faz isso internamente, então você só precisa fornecer as credenciais.

Como verificar a assinatura HMAC de um webhook em Go?

Cada webhook chega com um header contendo a assinatura HMAC-SHA256 do corpo, calculada com um segredo compartilhado. No seu handler net/http, leia o corpo bruto com io.ReadAll, recalcule o HMAC usando crypto/hmac e crypto/sha256 com o mesmo segredo, e compare os valores com hmac.Equal, que é uma comparação timing-safe. Valide também o timestamp para mitigar ataques de replay. Nunca use == para comparar as assinaturas, pois isso é vulnerável a timing attacks.

Devo usar context.Context nas chamadas à API em Go?

Sim, é a prática idiomática em Go. Propagar um context.Context permite definir timeouts e deadlines por requisição e cancelar chamadas em andamento quando o request HTTP que as originou é abortado. Construa as requisições com http.NewRequestWithContext e passe um contexto derivado de context.WithTimeout. O SDK oficial aceita um context.Context em todos os métodos que fazem chamadas de rede, garantindo cancelamento limpo e ausência de goroutines vazadas.

Qual a diferença entre a Transaction API e a Assinatura Expressa para um app Go?

A Assinatura Expressa (signing sessions) é a forma mais rápida de começar: uma única chamada a POST /v1/signing-sessions retorna a URL de checkout hospedado e o clientSecret para o embed em popup, ideal para fluxos de um signatário. A API de envelopes suporta múltiplos signatários no mesmo documento (até 100), ordem de assinatura sequencial ou paralela e o ciclo de vida transacional completo. Em Go, ambas usam o mesmo client autenticado; escolha a Expressa para integrar em minutos e a Transaction API quando precisar de orquestração avançada.

A API Go suporta certificados ICP-Brasil (A1 e A3)?

Sim. Ao definir o profile da política de assinatura como DIGITAL_CERTIFICATE, a sessão exige um certificado ICP-Brasil A1 — o formato usado em integrações via API (para tokens A3, o aplicativo SignDocs oferece o assinador desktop). O passo correspondente na resposta é o DIGITAL_SIGN_A1, com a classe do certificado registrada no resultado. Perfis com OTP e biometria facial cobrem os níveis eletrônicos, com plena validade jurídica sob a MP 2.200-2/2001.

Como testar a integração Go antes de ir para produção?

Use o ambiente de homologação (sandbox), cujo host base é api-hml.signdocs.com.br. Aponte o BaseURL do client Go para esse host e use credenciais de HML. As transações criadas em homologação têm TTL de 7 dias e são automaticamente expiradas, então não há risco de poluir dados de produção. Quando os testes passarem, basta trocar o BaseURL e as credenciais para o ambiente de produção, sem alterar o restante do código.

Comece a integrar assinatura digital em Go hoje

SDK Go oficial (zero dependências), autenticação OAuth2, webhooks com HMAC-SHA256 e certificado ICP-Brasil com validade jurídica sob a MP 2.200-2/2001. O acesso à API é um plano sob medida, com sandbox de homologação gratuito para ter sua primeira sessão de assinatura rodando antes de qualquer contrato.

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