Assinatura Digital em Apps Mobile (Flutter e React Native): Backend-for-Frontend, Link de Assinatura e Deep Link de Retorno
Você tem um app em Flutter ou React Native e precisa que o usuário assine um contrato sem sair dele. A tentação é "embutir a API no app": guardar as credenciais no bundle, chamar a SignDocs direto do celular, abrir o link em uma WebView. Cada um desses três atalhos cria um problema de segurança ou de conversão. Este guia mostra o desenho que funciona em produção: backend-for-frontend cria a sessão, o app abre o link de assinatura no navegador do sistema, um deep link de retorno traz o usuário de volta e só o webhook (ou a consulta de status) diz se a assinatura aconteceu.
O fluxo de API por baixo é o mesmo de qualquer integração: POST /v1/signing-sessions devolve um url e um clientSecret, e o link final é url?cs=clientSecret. O que muda no mobile é onde cada passo roda e como o usuário volta. Se você ainda não tem credenciais, veja como obter as credenciais da API e o guia de autenticação OAuth2.
Por que o app nunca fala diretamente com a SignDocs
O token de acesso vem de POST /oauth2/token com client_id e client_secret. Qualquer segredo colocado em um app instalável é público: um APK ou IPA é descompactado em minutos, e mesmo ofuscação ou variáveis de build acabam em texto legível na memória. Com o segredo em mãos, um terceiro cria sessões na sua conta, consome a sua cota e assina documentos em seu nome.
O padrão correto é o backend-for-frontend (BFF): o app se autentica no seu backend com o mecanismo que já usa (JWT, sessão, Firebase Auth), e é o backend que chama a SignDocs, guarda os identificadores e devolve ao app apenas o que ele precisa mostrar.
| Passo | Onde roda | O que trafega para o app |
|---|---|---|
| 1. Usuário toca em "Assinar" | App | Pedido ao seu backend com o id do contrato |
| 2. Criar sessão de assinatura | Seu backend | Nada da SignDocs ainda |
| 3. Devolver o link | Seu backend | Apenas signingUrl (com ?cs=) e expiresAt |
| 4. Assinar | Navegador do sistema, na página hospedada da SignDocs | Nada: a página é da plataforma |
| 5. Voltar ao app | Deep link (returnUrl) |
session_id na query string |
| 6. Confirmar que assinou | Seu backend (webhook ou GET status) | O estado do contrato, vindo do seu backend |
O clientSecret da sessão, aquele que vai no ?cs=, é diferente do client_secret OAuth2: ele autentica aquele signatário naquela sessão e precisa mesmo chegar ao dispositivo do usuário. É por isso que o link só deve ser entregue ao próprio signatário, dentro do app autenticado, nunca a uma tela compartilhada.
O backend: criar a sessão com returnUrl de deep link
O endpoint do BFF é curto. Ele monta o returnUrl apontando para o seu app e usa metadata para amarrar a sessão ao registro local. Escolha um perfil de autenticação por documento: CLICK_PLUS_OTP para a maioria dos aceites, BIOMETRIC quando a dúvida é quem está assinando; a comparação está em autenticação multimétodo.
Sobre o returnUrl: prefira um link universal / app link em HTTPS (https://app.seudominio.com.br/...) a um esquema customizado (meuapp://). O link HTTPS abre o app quando ele está instalado e cai em uma página web de fallback quando não está, o que também cobre o caso de o usuário ter assinado em outro dispositivo. Esquemas customizados funcionam, mas exigem que o app esteja instalado e são mais fáceis de sequestrar por outro app.
O app: abrir o link no navegador do sistema
A página hospedada de assinatura é uma aplicação web completa: mostra o documento, coleta o aceite, envia o OTP, aciona a câmera para a prova de vida nos perfis biométricos e registra as evidências. O melhor lugar para ela rodar é o navegador do sistema (Custom Tab no Android, SFSafariViewController no iOS), por três motivos práticos:
- Câmera e permissões: a prova de vida usa
getUserMedia; no navegador do sistema a permissão de câmera funciona como em qualquer site. Em uma WebView você precisa interceptar o pedido de permissão em código nativo, e o comportamento varia entre versões de Android e iOS. - Deep link de retorno: o navegador do sistema resolve links universais e app links nativamente. Em uma WebView, a navegação para o
returnUrlprecisa ser interceptada manualmente. - Geolocalização e user-agent reais: a trilha de auditoria registra IP, geolocalização, user-agent e horário em UTC. Um navegador padrão entrega isso sem surpresas; WebViews customizadas às vezes mascaram o user-agent ou bloqueiam a geolocalização.
Flutter
React Native
O redirecionamento não é prova. A página hospedada redireciona para o returnUrl ao final e anexa session_id, mas qualquer pessoa pode abrir https://app.seudominio.com.br/assinatura/retorno?session_id=xyz à mão. O app usa o deep link só para saber que deve perguntar; quem responde "assinado" é o seu backend, com base no webhook SIGNING_SESSION.COMPLETED ou em GET /v1/transactions/{id}.
E se o usuário fechar o navegador no meio?
Acontece o tempo todo: a pessoa abre o link, é interrompida, volta ao app pelo seletor de tarefas. O deep link nunca dispara, e o app fica sem saber em que ponto ela parou. Trate os três cenários:
- Voltou sem assinar, link ainda válido: mostre a tela "Assinatura pendente" com um botão "Continuar", que reabre o mesmo
signingUrl. O link vale atéexpiresAt(72 horas por padrão) e a sessão continua de onde parou. Guarde o link no estado seguro do app (Keychain / EncryptedSharedPreferences), não em texto plano. - Voltou e o link expirou:
GET /v1/transactions/{id}devolve EXPIRED. O backend cria uma nova sessão e o app recebe um novo link; nada do que foi feito na sessão antiga é reaproveitado. - Assinou em outro dispositivo (abriu o e-mail de convite no computador): quando o app voltar ao primeiro plano, consulte o seu backend. Um
onResume/AppState.change === 'active'que chama/contratos/estadoresolve isso sem polling contínuo.
Se o usuário perdeu o e-mail com o convite e o fluxo depende dele, POST /v1/signing-sessions/{id}/resend-invite reenvia. Se ele desistiu, POST /v1/signing-sessions/{id}/cancel encerra a sessão e o app pode oferecer recomeçar mais tarde.
WebView: quando faz sentido e como fazer direito
Há casos legítimos para uma WebView: quiosques, apps corporativos com MDM que bloqueia o navegador, ou requisitos de marca que exigem a assinatura "dentro" da tela. Se for por esse caminho, três cuidados:
- Permissão de câmera: no Android, implemente
onPermissionRequestnoWebChromeCliente concedaRESOURCE_VIDEO_CAPTUREdepois de obter a permissão nativa; no iOS, oWKWebViewpede a permissão de câmera sozinho a partir do iOS 15 desde queNSCameraUsageDescriptionexista no Info.plist. Teste os perfis biométricos em dispositivos reais, não só no emulador. - Intercepte o returnUrl: use o callback de navegação (
NavigationDelegatenowebview_flutter,onShouldStartLoadWithRequestnoreact-native-webview) para detectar oreturnUrl, ler osession_ide fechar a WebView, em vez de deixar a página de retorno carregar dentro dela. - Não altere o user-agent nem bloqueie geolocalização: isso empobrece a trilha de auditoria que protege o seu próprio contrato.
Mesmo na WebView, a regra do BFF não muda: credenciais OAuth2 ficam no servidor e o app só recebe o link.
Vários signatários e o contrato com testemunhas
Quando o documento precisa de mais de uma assinatura (o usuário do app e um avalista, ou contratante e contratado), o backend cria um envelope em vez de uma sessão avulsa: POST /v1/envelopes com signingMode (PARALLEL ou SEQUENTIAL), totalSigners, o documento e expiresInMinutes (de 5 minutos a 7 dias); depois POST /v1/envelopes/{id}/sessions para cada pessoa, com signerIndex a partir de 1 e a política daquele signatário. O usuário do app recebe o link dele pelo BFF, como antes; os demais recebem o convite por e-mail e assinam no navegador de onde estiverem, sem instalar nada. O webhook ENVELOPE.ALL_SIGNED avisa quando o último assinou. Detalhes de ordem e paralelismo estão em ordem de assinatura com múltiplos signatários.
Checklist antes de publicar nas lojas
- Nenhum
client_id/client_secretno bundle; grep no build final antes de submeter. - Link de assinatura entregue ao app apenas em endpoint autenticado do seu backend.
- Abertura em Custom Tab / SFSafariViewController; WebView só com os cuidados acima.
returnUrlcomo link universal / app link, com página de fallback web.- Deep link apenas dispara consulta ao backend; "assinado" vem do webhook ou do GET.
- Tela de "pendente" com reabertura do mesmo link e consulta ao voltar ao primeiro plano.
- Perfis biométricos testados em aparelhos reais, Android e iOS.
- Homologação com
api-hml.signdocs.com.brem build de teste; produção só com a URL de produção.
Se a sua necessidade é apenas que a sua equipe assine e envie documentos pelo celular, sem construir nada, os aplicativos da SignDocs Brasil para Android e iOS já fazem isso, com o plano Grátis de 5 documentos por mês. A integração deste guia é para quando a assinatura precisa acontecer dentro do seu produto. O sandbox de homologação é gratuito, sem cartão, com biometria simulada e entidades que expiram em 7 dias; em produção, o acesso à API é por plano sob medida com o time comercial.
Perguntas Frequentes
Posso chamar a API da SignDocs diretamente do app Flutter ou React Native?
Não deveria. O token de acesso vem de POST /oauth2/token com client_id e client_secret, e qualquer segredo dentro de um APK ou IPA é público: o pacote é descompactado em minutos e ofuscação não resolve. Com o segredo, um terceiro cria sessões na sua conta e consome a sua cota. O desenho correto é o backend-for-frontend: o app se autentica no seu backend com o mecanismo que já usa, o backend chama a SignDocs e devolve ao app apenas o link de assinatura e a data de expiração.
Devo abrir o link de assinatura em uma WebView ou no navegador?
No navegador do sistema (Custom Tab no Android, SFSafariViewController no iOS), via url_launcher com LaunchMode.externalApplication no Flutter ou Linking / InAppBrowser no React Native. A página hospedada usa a câmera para a prova de vida nos perfis biométricos, resolve links universais de retorno e entrega user-agent e geolocalização reais para a trilha de auditoria. Tudo isso funciona sem esforço no navegador; em uma WebView exige interceptar permissões e navegação em código nativo. WebView só com esses cuidados implementados e testados em aparelhos reais.
O que é o returnUrl e o que chega nele?
É a URL para onde a página hospedada redireciona o signatário ao terminar. A SignDocs anexa session_id à query string (com ? ou &, conforme a URL já tenha parâmetros). Para apps, use um link universal / app link em HTTPS: abre o app quando instalado e cai em uma página web quando não está. O app lê o session_id e pergunta ao seu backend o estado do contrato. O redirecionamento em si não prova nada, porque qualquer pessoa pode digitar essa URL: quem confirma a assinatura é o webhook SIGNING_SESSION.COMPLETED ou GET /v1/transactions/{id}.
O usuário fechou o navegador antes de assinar. E agora?
Guarde o link de assinatura no armazenamento seguro do app e mostre uma tela de pendência com o botão de continuar, que reabre o mesmo link: ele vale até expiresAt (72 horas por padrão) e a sessão retoma de onde parou. Ao voltar ao primeiro plano, consulte o seu backend para cobrir o caso de a pessoa ter assinado em outro dispositivo. Se o link expirou, GET /v1/transactions/{id} devolve EXPIRED e o backend cria uma nova sessão. Se o e-mail de convite se perdeu, POST /v1/signing-sessions/{id}/resend-invite reenvia.
Como faço quando o contrato precisa de mais de um signatário?
O backend cria um envelope com POST /v1/envelopes (signingMode PARALLEL ou SEQUENTIAL, totalSigners, documento e expiresInMinutes entre 5 minutos e 7 dias) e adiciona cada pessoa com POST /v1/envelopes/{id}/sessions, informando signerIndex a partir de 1 e a política de autenticação dela. O usuário do app recebe o link dele pelo backend; os outros recebem o convite por e-mail e assinam no navegador, sem instalar nada. O webhook ENVELOPE.ALL_SIGNED avisa quando o último assinou. Um envelope carrega um único documento.
Preciso construir isso ou os apps da SignDocs resolvem?
Depende de quem assina e onde. Se a necessidade é a sua equipe enviar e assinar documentos pelo celular, os aplicativos da SignDocs Brasil para Android e iOS já fazem isso, com o plano Grátis de 5 documentos por mês. A integração deste guia é para quando a assinatura precisa acontecer dentro do seu próprio produto, com os seus usuários. Nesse caso, o sandbox de homologação é gratuito e sem cartão, com biometria simulada, e o acesso em produção é por plano sob medida com o time comercial.
Teste o fluxo mobile no sandbox
Credenciais de homologação gratuitas, sem cartão: crie a sessão pelo seu backend, abra o link no aparelho e receba o deep link de retorno com biometria simulada. Em produção, plano sob medida com o time comercial.
Criar credenciais de homologação Fale com o time comercial