Como Integrar Assinatura Digital em PHP / Laravel

Se você desenvolve em PHP ou Laravel e precisa coletar assinaturas com validade jurídica direto da sua aplicação, este guia mostra o caminho completo: instalar o SDK oficial via Composer, autenticar via OAuth2, criar uma sessão de assinatura, adicionar signatários, enviar o documento, verificar webhooks com HMAC e, por fim, baixar o PDF assinado. Tudo com snippets idiomáticos de PHP e Laravel, prontos para colar no seu projeto.

A SignDocs Brasil expõe uma API REST agnóstica de linguagem e mantém SDKs oficiais para TypeScript/Node, Python, Go, Java, PHP e C#/.NET. Como o SDK PHP é um pacote Composer puro (PSR-4, sem dependência de framework), ele se encaixa igualmente bem em Laravel, Symfony, Slim, WordPress ou PHP procedural.

Antes de começar, vale entender o panorama geral em o que é a API de assinatura digital da SignDocs e, se você quiser comparar com outras stacks, veja os guias paralelos de integração em Node.js e integração em Java. Para um panorama agnóstico de linguagem, comece por o que é uma API de assinatura digital.

Pré-requisitos

Para acompanhar este tutorial você precisa de:

  • PHP 8.1+ (o SDK aproveita tipos nomeados, enums e propriedades readonly).
  • Composer instalado para gerenciar dependências.
  • Uma conta SignDocs e um par de credenciais de API (client_id e client_secret). Veja como obter sua API key.
  • Acesso ao ambiente de homologação (sandbox) para testar sem custo.
  • Opcionalmente, um framework como Laravel 10/11 — usaremos Laravel nos exemplos de rota e controller.
Dica de ambiente: durante todo o desenvolvimento, aponte o SDK para a base de homologação api-hml.signdocs.com.br (forma com hífen, não api.hml). As entidades de homologação têm TTL de 7 dias — transações e evidências de teste somem depois disso, o que é ótimo para manter o sandbox limpo.

Passo 1 — Instalar o SDK PHP via Composer

A instalação é uma única linha de Composer (o pacote exige PHP 8.1+ e usa Guzzle por baixo):

# Instala o SDK oficial de assinatura digital composer require signdocs-brasil/signdocs-brasil-php # O SDK é um pacote PSR-4 sem acoplamento a framework: # nada mais a fazer além de configurar as credenciais.

Em um projeto Laravel, mantenha as credenciais no arquivo .env e exponha-as via um arquivo de configuração. Isso evita hardcode de segredos e facilita alternar entre homologação e produção:

# .env SIGNDOCS_CLIENT_ID=seu_client_id SIGNDOCS_CLIENT_SECRET=seu_client_secret SIGNDOCS_BASE_URL=https://api-hml.signdocs.com.br SIGNDOCS_WEBHOOK_SECRET=a1b2c3d4e5f6...seu_segredo_de_webhook
// config/signdocs.php <?php return [ 'client_id' => env('SIGNDOCS_CLIENT_ID'), 'client_secret' => env('SIGNDOCS_CLIENT_SECRET'), 'base_url' => env('SIGNDOCS_BASE_URL', 'https://api.signdocs.com.br'), 'webhook_secret' => env('SIGNDOCS_WEBHOOK_SECRET'), ];

Passo 2 — Autenticar via OAuth2 (client-credentials)

A SignDocs usa OAuth2 no fluxo client-credentials: você troca client_id + client_secret por um bearer token JWT de curta duração (15 minutos), assinado com ECDSA (ES256). O SDK PHP cuida dessa troca e renova o token automaticamente antes de expirar, então normalmente você só instancia o cliente uma vez. Para entender o mecanismo a fundo, veja o guia de autenticação OAuth2 da API.

Em Laravel, o padrão idiomático é registrar o cliente como singleton em um Service Provider, para que todo o app reutilize a mesma instância (e o mesmo token em cache):

// app/Providers/SignDocsServiceProvider.php <?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use SignDocsBrasil\Api\SignDocsBrasilClient; use SignDocsBrasil\Api\Config; class SignDocsServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton(SignDocsBrasilClient::class, function () { return new SignDocsBrasilClient(new Config( clientId: config('signdocs.client_id'), clientSecret: config('signdocs.client_secret'), baseUrl: config('signdocs.base_url'), )); }); } }

Em PHP puro (sem framework), a inicialização é igualmente direta. O SDK obtém o token na primeira chamada e o reutiliza enquanto válido:

<?php require 'vendor/autoload.php'; use SignDocsBrasil\Api\SignDocsBrasilClient; use SignDocsBrasil\Api\Config; $signdocs = new SignDocsBrasilClient(new Config( clientId: getenv('SIGNDOCS_CLIENT_ID'), clientSecret: getenv('SIGNDOCS_CLIENT_SECRET'), baseUrl: 'https://api-hml.signdocs.com.br', )); // O token OAuth2 é obtido e renovado automaticamente pelo SDK.
Segurança: nunca exponha o client_secret no frontend, em repositórios públicos ou em logs. Mantenha-o em variáveis de ambiente no servidor. Para clientes enterprise regulados (BACEN/Open Finance), há ainda suporte a mTLS (mutual TLS), que adiciona autenticação mútua por certificado de cliente sobre o OAuth2.

Passo 3 — Criar a sessão de assinatura

A SignDocs oferece duas superfícies de API que você pode usar a partir do PHP:

Superfície Quando usar Característica
Assinatura Expressa (Signing Sessions) Fluxo de signatário único e rápido — um único POST /v1/signing-sessions retorna um checkout hospedado ou widget embutido Menos chamadas; ideal para "assinar agora"
Transaction API (envelopes) Múltiplos signatários no mesmo documento, ordem de assinatura, ciclo de vida completo Controle granular sobre o fluxo transacional

Para o quickstart, usaremos a Assinatura Expressa, que entrega valor com uma única chamada. O exemplo abaixo cria uma sessão para um contrato e define o perfil de política DIGITAL_CERTIFICATE (certificado ICP-Brasil A1 — para titulares de token A3, o aplicativo SignDocs oferece o assinador desktop):

<?php use SignDocsBrasil\Api\SignDocsBrasilClient; use SignDocsBrasil\Api\Models\CreateSigningSessionRequest; use SignDocsBrasil\Api\Models\Policy; use SignDocsBrasil\Api\Models\Signer; use SignDocsBrasil\Api\Models\Owner; // Em Laravel, resolva o singleton: $signdocs = app(SignDocsBrasilClient::class); $session = $signdocs->signingSessions->create(new CreateSigningSessionRequest( purpose: 'DOCUMENT_SIGNATURE', policy: new Policy( // DIGITAL_CERTIFICATE = certificado ICP-Brasil A1 via API profile: 'DIGITAL_CERTIFICATE', ), signer: new Signer( name: 'Maria Silva', userExternalId: 'cliente-8821', email: 'maria@empresa.com.br', cpf: '12345678901', ), document: [ 'content' => base64_encode(file_get_contents('contrato.pdf')), 'filename' => 'Contrato_Prestacao_Servicos.pdf', ], owner: new Owner( // com owner, a SignDocs envia o convite por e-mail ao signatário email: 'contratos@suaempresa.com.br', name: 'Sua Empresa', ), returnUrl: 'https://suaempresa.com.br/assinatura/concluida', )); // O link de assinatura é a URL + o clientSecret como query param ?cs= $signingUrl = $session->url . '?cs=' . $session->clientSecret; echo "Sessão criada: " . $session->sessionId . PHP_EOL; echo "Link de assinatura: " . $signingUrl . PHP_EOL;
Atenção ao perfil de política: o valor correto para o campo policy.profile é DIGITAL_CERTIFICATE. Não use DIGITAL_SIGN_A1 ali — esse valor é um tipo de etapa (step.type) que aparece na resposta, e a API rejeita com 400 se for enviado como profile. Para escolher entre clickwrap, OTP, biometria e certificado, consulte a seção de métodos em o que é uma API de assinatura digital.

Resposta típica da criação

{ "sessionId": "01JC9J6L8N0Q2S4U6W8Y0A2C4E", "transactionId": "01JC9J6L8N0Q2S4U6W8Y0A2C4F", "status": "ACTIVE", "url": "https://sign-hml.signdocs.com.br/s/01JC9J6L8N0Q2S4U6W8Y0A2C4E", "clientSecret": "ss_secret_x9y8z7w6v5u4", "expiresAt": "2026-06-27T18:00:00Z", "inviteSent": true }

Passo 4 — Múltiplos signatários e envelopes (Transaction API)

Quando o mesmo documento precisa de mais de um signatário, ou de uma ordem de assinatura definida, use envelopes. O SDK PHP expõe métodos para criar o envelope e adicionar uma sessão por signatário — cada uma com sua própria URL de assinatura. Saiba mais sobre o desenho do fluxo em fluxo transacional da API de assinatura.

<?php use SignDocsBrasil\Api\Models\CreateEnvelopeRequest; use SignDocsBrasil\Api\Models\AddEnvelopeSessionRequest; // 1. Cria o envelope sequencial com o documento $envelope = $signdocs->envelopes->create(new CreateEnvelopeRequest( signingMode: 'SEQUENTIAL', totalSigners: 2, documentContent: base64_encode(file_get_contents('locacao.pdf')), documentFilename: 'locacao.pdf', )); // 2. Uma sessão por signatário — signerIndex define a ordem da fila $sessao1 = $signdocs->envelopes->addSession($envelope->envelopeId, new AddEnvelopeSessionRequest( signerName: 'Locador Ltda', signerEmail: 'locador@empresa.com.br', policyProfile: 'DIGITAL_CERTIFICATE', signerIndex: 1, // assina primeiro )); $sessao2 = $signdocs->envelopes->addSession($envelope->envelopeId, new AddEnvelopeSessionRequest( signerName: 'João Inquilino', signerEmail: 'joao@gmail.com', policyProfile: 'CLICK_PLUS_OTP', signerIndex: 2, // só conclui depois do primeiro )); echo "Links de assinatura: " . $sessao1->url . " | " . $sessao2->url . PHP_EOL;

Para detalhes sobre a sequência de assinatura quando há vários signatários, veja ordem de assinatura com múltiplos signatários.

SignDocs: SDK PHP nativo + ICP-Brasil. A API SignDocs é LGPD-first, com base legal na MP 2.200-2/2001, suporte a certificados ICP-Brasil (A1 e A3), produto e suporte em pt-BR. O SDK PHP oficial reduz a integração a poucas linhas de Composer. O acesso à API é um plano sob medida, com sandbox de homologação gratuito — fale com nossa equipe para receber as credenciais.

Passo 5 — Verificar webhooks com HMAC em Laravel

Em vez de ficar consultando o status da transação em loop (polling), o ideal é receber webhooks: a API faz um HTTP POST no seu endpoint sempre que um evento ocorre (SIGNING_SESSION.COMPLETED, TRANSACTION.COMPLETED, STEP.FAILED, etc.). Cada webhook traz uma assinatura HMAC-SHA256 no header, e você deve verificá-la antes de processar. O guia completo está em webhooks e eventos da API de assinatura.

Registrar a rota

Em Laravel, exclua a rota de webhook da verificação CSRF (ela não vem de um formulário do seu app) e aponte para um controller dedicado:

// routes/api.php use App\Http\Controllers\SignDocsWebhookController; Route::post('/webhooks/signdocs', [SignDocsWebhookController::class, 'handle']);

O controller que verifica a assinatura

O ponto crítico é ler o corpo bruto da requisição com $request->getContent() (não o array já parseado), recalcular o HMAC e comparar com hash_equals(), que é uma comparação timing-safe — imune a timing attacks:

// app/Http/Controllers/SignDocsWebhookController.php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; class SignDocsWebhookController extends Controller { private const TIMESTAMP_TOLERANCE = 300; // 5 minutos public function handle(Request $request) { $payload = $request->getContent(); // corpo BRUTO, não parseado $signature = $request->header('X-SignDocs-Signature', ''); $timestamp = $request->header('X-SignDocs-Timestamp', ''); $secret = config('signdocs.webhook_secret'); // 1. Protege contra replay attacks if (abs(time() - (int) $timestamp) > self::TIMESTAMP_TOLERANCE) { return response()->json(['error' => 'Timestamp expirado'], 401); } // 2. Recalcula o HMAC sobre "timestamp.payload" $signedPayload = $timestamp . '.' . $payload; $expected = hash_hmac('sha256', $signedPayload, $secret); // 3. Comparação timing-safe if (! hash_equals($expected, $signature)) { return response()->json(['error' => 'Assinatura inválida'], 401); } $event = json_decode($payload, true); // 4. Idempotência: ignora reentregas do mesmo id de evento if (\App\Models\WebhookEvent::where('id_evento', $event['id'])->exists()) { return response()->json(['status' => 'already_processed'], 200); } // 5. Despacha o processamento pesado para uma fila (assíncrono) \App\Jobs\ProcessSignDocsEvent::dispatch($event); // 6. Responde 200 rapidamente para evitar retry desnecessário return response()->json(['status' => 'received'], 200); } }

Prefere não implementar o HMAC na mão? O SDK traz o helper pronto: SignDocsBrasil\Api\WebhookVerifier::verify($payload, $signature, $timestamp, $secret) faz a mesma validação (incluindo a tolerância de replay) em uma chamada.

Regra de ouro dos webhooks: verifique o HMAC, persista o id do evento para idempotência, despache o trabalho pesado para uma fila (queue) e responda 200 em segundos. Se você processar tudo de forma síncrona e estourar o timeout, a API tratará como falha e reenviará o evento — gerando duplicatas.

O Job que reage ao evento

// app/Jobs/ProcessSignDocsEvent.php (trecho do handle) public function handle(): void { \App\Models\WebhookEvent::create(['id_evento' => $this->event['id']]); match ($this->event['eventType']) { 'SIGNING_SESSION.COMPLETED' => $this->onSessaoConcluida(), 'TRANSACTION.COMPLETED' => $this->onCompleted(), 'SIGNING_SESSION.CANCELLED' => $this->onCancelada(), default => Log::info('Evento não tratado', $this->event), }; }

Passo 6 — Baixar o PDF assinado

Quando a transação é concluída, você recebe o evento TRANSACTION.COMPLETED. Nesse momento, use o SDK para obter as URLs temporárias de download do PDF final (já com as assinaturas embutidas) e do pacote de evidências, e salve os arquivos no Storage do Laravel ou no S3:

<?php use Illuminate\Support\Facades\Storage; use SignDocsBrasil\Api\SignDocsBrasilClient; private function onCompleted(): void { $signdocs = app(SignDocsBrasilClient::class); // o transactionId vem no topo do payload do webhook $txId = $this->event['transactionId']; // PDF final assinado — a resposta traz uma URL temporária (signedUrl) $download = $signdocs->documents->download($txId); Storage::put("assinados/{$txId}.pdf", file_get_contents($download->signedUrl)); // Pacote de evidências .p7m: GET /v1/transactions/{id}/evidence // devolve os metadados e a URL temporária (downloadUrl) do arquivo $evidencia = Http::withToken($token) ->get("https://api.signdocs.com.br/v1/transactions/{$txId}/evidence") ->json(); Storage::put("assinados/{$txId}.p7m", file_get_contents($evidencia['downloadUrl'])); }

O pacote de evidências .p7m contém o container PKCS#7/CMS com a cadeia de certificação, o hash SHA-256 do documento, os carimbos de hora do servidor e a trilha de auditoria que sustentam a validade jurídica. Para entender o que ele inclui, veja o pacote de evidências P7M como prova jurídica. Qualquer pessoa pode conferir a autenticidade do documento no verificador público.

Boas práticas para PHP/Laravel em produção

  • Segredos no ambiente, nunca no código: use .env + config(); em produção, prefira um cofre de segredos (AWS Secrets Manager, Vault) em vez de arquivos.
  • Filas para webhooks: conecte o queue do Laravel a Redis, SQS ou banco. Processe eventos em workers, mantendo a rota de webhook leve.
  • Idempotência sempre: persista o id de cada evento em uma tabela com índice único. A entrega de webhooks é at-least-once, então duplicatas vão acontecer.
  • Timeout e retries no cliente HTTP: o SDK já trata renovação de token; configure timeouts sensatos para chamadas de criação de sessão.
  • Homologação antes de produção: rode o fluxo completo em api-hml.signdocs.com.br e só então troque a base URL e as credenciais.
  • Observabilidade: logue o id do evento e o transactionId em cada etapa para facilitar a reconciliação.

Padrões de assinatura suportados

As assinaturas geradas seguem PAdES (para PDF), CAdES (para qualquer tipo de arquivo) e usam containers PKCS#7/CMS, no nível baseline (PAdES-B / CAdES-B): assinatura + cadeia de certificados ICP-Brasil + carimbo de hora do servidor + trilha de auditoria com o hash SHA-256 do documento. Os níveis com carimbo de tempo de uma ACT e LTV não são gerados atualmente pela API. Para o aprofundamento técnico desses padrões, veja PKCS#7, CMS, PAdES e CAdES no glossário.

Resumo do fluxo end-to-end

  1. Composer: composer require signdocs-brasil/signdocs-brasil-php e configure as credenciais no .env.
  2. OAuth2: instancie o cliente; o SDK obtém e renova o bearer token automaticamente.
  3. Criar sessão: signingSessions->create() (expressa) ou envelopes->create() (multi-signatário).
  4. Adicionar signatários: envelopes->addSession() com signerIndex — cada sessão tem sua URL de assinatura.
  5. Webhook: rota POST em Laravel + verificação HMAC (WebhookVerifier::verify() ou hash_equals()) + idempotência pelo id do evento.
  6. Download: ao receber TRANSACTION.COMPLETED, baixe o PDF assinado e o .p7m de evidências pelas URLs temporárias.

Se quiser começar ainda mais rápido, há um caminho enxuto em primeiros passos com a API em 5 minutos. E se a sua stack for outra, os guias de Node.js e Java seguem exatamente a mesma lógica.

Perguntas Frequentes

Existe um SDK oficial de assinatura digital para PHP?

Sim. A SignDocs mantém um SDK PHP oficial distribuído via Composer, ao lado dos SDKs para TypeScript/Node, Python, Go, Java e C#/.NET. O SDK PHP encapsula a autenticação OAuth2, a renovação automática de token, as chamadas REST tipadas e os helpers de verificação de webhook, de modo que você não precisa montar requisições cURL manualmente. Como a API REST é agnóstica de linguagem, você também pode usar Guzzle ou cURL puro caso prefira não adicionar uma dependência ao seu projeto.

O SDK PHP funciona com Laravel e Symfony?

Sim. O SDK é um pacote Composer PSR-4 sem acoplamento a nenhum framework, então funciona em Laravel, Symfony, Slim, WordPress ou PHP puro. Em Laravel, o padrão recomendado é registrar o cliente como singleton em um Service Provider, ler as credenciais do arquivo .env via config(), e tratar os webhooks com uma rota POST mais um controller dedicado que verifica a assinatura HMAC antes de processar o evento.

Como autenticar na API de assinatura a partir de PHP?

A autenticação usa OAuth2 no fluxo client-credentials: você envia seu client_id e client_secret ao endpoint de token e recebe um bearer token JWT (assinado com ECDSA ES256) que expira em 15 minutos. O SDK PHP faz essa troca automaticamente e renova o token antes de expirar, então você só precisa configurar as credenciais uma vez. Para integrações enterprise reguladas (BACEN/Open Finance) há ainda suporte a mTLS. Nunca exponha o client_secret no frontend; mantenha-o em variáveis de ambiente no servidor.

Como verificar a assinatura HMAC de um webhook em Laravel?

Os webhooks da SignDocs chegam como HTTP POST com um header contendo uma assinatura HMAC-SHA256 calculada sobre o corpo bruto da requisição. Em Laravel, leia o corpo bruto com $request->getContent(), recalcule o HMAC com hash_hmac('sha256', $payload, $secret) e compare com a assinatura recebida usando hash_equals(), que é uma comparação timing-safe. Só processe o evento se as assinaturas coincidirem; caso contrário, retorne 401. Implemente também idempotência usando o id do evento para tolerar reentregas — ou use o helper WebhookVerifier::verify() do SDK, que já valida assinatura e timestamp.

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

Sim. A política de assinatura aceita o perfil DIGITAL_CERTIFICATE, que aplica o certificado ICP-Brasil A1 (o formato usado em integrações via API; titulares de token A3 usam o assinador desktop do aplicativo SignDocs), além de perfis com clickwrap, OTP por SMS/e-mail e biometria facial. Você define o perfil ao criar a sessão de assinatura via SDK PHP. As assinaturas resultantes seguem os padrões PAdES e CAdES no nível baseline (assinatura + cadeia de certificados ICP-Brasil + carimbo de hora do servidor + trilha de auditoria), gerando um pacote de evidências .p7m que vincula o hash SHA-256 do documento, com validade jurídica conforme a MP 2.200-2/2001.

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

Use o ambiente de homologação apontando a base URL do SDK para api-hml.signdocs.com.br (forma com hífen, não api.hml). Você cria credenciais de homologação separadas e roda todo o fluxo (token, criação de sessão, assinatura e webhook) sem custo nem efeitos em produção. Lembre-se de que as entidades de homologação têm TTL de 7 dias, então transações e evidências de teste são removidas após esse período. Quando o fluxo estiver validado, basta trocar a base URL e as credenciais para produção.

Como baixar o PDF final assinado a partir do PHP?

Quando a transação é concluída, você recebe o evento TRANSACTION.COMPLETED via webhook. A partir daí, chame o método de download de documento do SDK PHP passando o transactionId; a resposta traz uma URL temporária (signedUrl) do PDF assinado, que você baixa e salva no Storage do Laravel ou no S3. Para uso jurídico, baixe também o pacote de evidências .p7m, que contém o container PKCS#7/CMS com a cadeia de certificação, o hash SHA-256 do documento, os carimbos de hora do servidor e a trilha de auditoria.

Integre assinatura digital ao seu app PHP/Laravel hoje

SDK PHP oficial via Composer, OAuth2 automático, webhooks com HMAC-SHA256 e certificado ICP-Brasil. LGPD-first, com validade jurídica pela MP 2.200-2/2001. O acesso à API é um plano sob medida, com sandbox de homologação gratuito — comece por ele e suba para produção trocando uma única variável.

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