Português · Read in English
SDK Ruby para a API Assinafy v1 — plataforma brasileira de assinatura eletrônica de documentos.
O SDK expõe todas as operações da API Assinafy v1, incluindo OAuth 2.1, autenticação em dois
fatores, endpoints de webhook com assinatura nativa, e o ciclo de vida completo de templates. O
spec/api_coverage_spec.rb, versionado no repositório, valida que cada
rota mapeia de forma única para um método público do SDK — se uma operação deixar de ter cobertura,
a suíte falha.
Este documento acompanha uma integração do início ao fim, na ordem em que ela acontece. Para consulta rápida por recurso, veja README.en.md; para consulta por operação, docs/API_REFERENCE.md.
- Instalação, configuração e ambientes
- Autenticação
- Conexão OAuth 2.1 (apps de marketplace)
- Enviar o documento e esperar os metadados
- Signatários
- Métodos de verificação e notificação
- Estimar o custo
- Abrir o assignment
- A experiência do signatário
- Webhooks: endpoints e verificação de assinatura
- Andamento, download e verificação
- Organizar e limpar
- Erros
- Paginação
- Recursos do cliente
- Assinaturas RBS
- Ruby 3.2+ (suporte mantido: 3.3+; 3.2 é compatibilidade legada/EOL)
- Bundler
- TLS 1.2 ou superior (o SDK recusa TLS 1.0 e 1.1)
Do RubyGems.org:
# Gemfile
gem 'assinafy'bundle installDo GitHub Packages (mirror), com um personal access token de escopo read:packages:
source 'https://rubygems.pkg.github.com/assinafy' do
gem 'assinafy'
endbundle config https://rubygems.pkg.github.com/assinafy USUARIO:TOKEN| Ambiente | base_url |
Servidor de autorização OAuth |
|---|---|---|
| Produção | https://api.assinafy.com.br/v1 (padrão) |
https://auth.assinafy.com.br |
| Sandbox | https://sandbox.assinafy.com.br/v1 |
https://auth-sandbox.assinafy.com.br |
O sandbox é gratuito: use-o para testar a integração antes de ir para produção, trocando a
base_url e, no OAuth, o servidor de autorização. Nunca envie uma chave de API do sandbox para a
produção, nem o contrário. Os endpoints de webhook e a autenticação em dois fatores estão
disponíveis em produção (api.assinafy.com.br).
require 'assinafy'
require 'logger'
client = Assinafy::Client.new(
api_key: ENV.fetch('ASSINAFY_API_KEY'),
account_id: ENV.fetch('ASSINAFY_ACCOUNT_ID'),
base_url: ENV.fetch('ASSINAFY_BASE_URL', 'https://api.assinafy.com.br/v1'),
webhook_secret: ENV['ASSINAFY_WEBHOOK_SECRET'], # whsec_... do endpoint de webhook
timeout: 30,
logger: Logger.new($stdout)
)base_url:precisa ser uma URLhttp/httpsabsoluta. Qualquer outra coisa — um host sem esquema, um caminho relativo, outro esquema — levantaAssinafy::ValidationErrorem vez de anexar suas credenciais a ela. A barra final é removida.Configuration#base_url=e#timeout=validam da mesma forma que o construtor.- Métodos com escopo de conta aceitam uma sobrescrita de
account_idpor chamada, para tenants com múltiplas workspaces. - O
logger:recebe mensagens de ciclo de vida do SDK, nunca corpos de requisição ou credenciais. - Toda requisição envia
User-Agent: Assinafy-Ruby-SDK/v<Assinafy::VERSION>. Client.from_config(hash)aceita hashes com chaves string ou símbolo (por exemplo, YAML já interpretado).
A Assinafy aceita três credenciais. Escolha pela pergunta "quem está agindo?".
| Credencial | Quem age | Quando usar |
|---|---|---|
Chave de API (api_key:) |
a própria workspace | integrações de back-end. Permanente. Enviada como X-Api-Key. |
Token de sessão (token:) |
o usuário que fez login | depois de client.auth.login. JWT, expira em ~1 hora. |
OAuth 2.1 (token:) |
um aplicativo, em nome de um usuário | apps de marketplace, integrações de terceiros, assistentes de IA. Veja a seção 3. |
Configure exatamente uma credencial por cliente; se api_key: e token: forem informados, o
SDK envia apenas X-Api-Key. Um cliente sem credenciais serve para login, OAuth e endpoints
públicos ou de signatário — o SDK remove X-Api-Key/Authorization dessas chamadas de qualquer
forma.
publico = Assinafy::Client.new
sessao = publico.auth.login(email: 'usuario@example.com', password: ENV.fetch('ASSINAFY_PASSWORD'))Quando o usuário tem autenticação em dois fatores ativa, o login não devolve access_token:
devolve um desafio com mfa_token. Troque-o pelo código do aplicativo autenticador, ou por um
código de recuperação:
if sessao['mfa_token']
sessao = publico.auth.verify_mfa(
mfa_token: sessao['mfa_token'],
code: '123456' # ou um código de recuperação, como "ABCD-EFGH-JKMN"
)
end
usuario = Assinafy::Client.new(
token: sessao.fetch('access_token'),
account_id: sessao.fetch('accounts').first.fetch('id')
)verify_mfa é enviado sem credenciais de workspace. O desafio vale uma única vez e expira 5
minutos depois do login; um código errado responde 400, e um desafio expirado, já usado ou com
tentativas demais responde 401 — nesse caso, faça login de novo. O login social
(client.auth.social_login) devolve o mesmo formato.
Estas chamadas agem sobre o usuário autenticado:
inscricao = usuario.auth.start_totp_enrollment(label: 'Meu celular')
inscricao['provisioning_uri'] # => "otpauth://totp/...": mostre como QR code
inscricao['secret'] # exibido só nesta resposta
codigos = usuario.auth.confirm_totp_enrollment(method_id: inscricao.fetch('id'), code: '123456')
codigos['recovery_codes'] # => ["ABCD-EFGH-JKMN", ...] — exibidos uma única vez
usuario.auth.mfa_methods
# => { 'methods' => [{ 'id' => 'mfa-method-id', 'type' => 'Totp', 'label' => 'Meu celular', ... }],
# 'recovery_codes_remaining' => 10 }
usuario.auth.regenerate_recovery_codes(password: ENV.fetch('ASSINAFY_PASSWORD'))
usuario.auth.delete_mfa_method('mfa-method-id', code: '123456') # => { 'is_mfa_enabled' => false }- A autenticação em dois fatores só fica ativa depois de
confirm_totp_enrollment. - Confirmar um novo autenticador quando já existe um substitui o anterior e exige
reautenticação:
password:oureauth_code:(código do dispositivo atual ou de recuperação). regenerate_recovery_codesedelete_mfa_methodexigempassword:oucode:; sem nenhum dos dois, o SDK levantaValidationErrorantes de enviar. Um código de recuperação usado como prova é consumido. Remover o último método também descarta os códigos de recuperação.
usuario.auth.create_api_key(password: ENV.fetch('ASSINAFY_PASSWORD')) # => { 'api_key' => '...' } — exibida uma única vez
usuario.auth.get_api_key # => { 'api_key' => '****...' } — mascarada
usuario.auth.delete_api_key # => nilGerar uma chave nova invalida a anterior. Nunca exponha a chave em um front-end.
Use OAuth quando um aplicativo age em nome de um usuário, com a permissão dele. Diferente da
chave de API, o token vale para uma workspace e carrega apenas os escopos que o usuário aprovou.
O fluxo é authorization code com PKCE obrigatório (o servidor aceita apenas S256).
| Escopo | Concede |
|---|---|
documents:read |
ler documentos, páginas, tags, signatários, assignments, atividades e entregas de webhook |
documents:write |
criar, alterar e excluir documentos e gerenciar seus signatários e assignments |
templates:read |
ler templates, páginas, papéis, campos e tags |
templates:write |
criar, alterar e excluir templates |
account:read |
ler perfil, tema, logo e endpoints de webhook da workspace |
webhooks:write |
criar, alterar, desativar e excluir endpoints de webhook da workspace |
openid |
identificar o usuário (claim sub) e habilitar /oauth/userinfo |
profile |
incluir o nome do usuário nas claims |
email |
incluir o e-mail e seu status de verificação nas claims |
offline_access |
receber um refresh token (nunca aparece no scope devolvido) |
Um token OAuth nunca alcança faturamento, ciclo de vida da conta, gerenciamento de credenciais, segredos de assinatura de webhook ou superfícies administrativas — independentemente do escopo.
Registre no aplicativo Assinafy uma URL de callback HTTPS. Para cada tentativa de autorização, gere
e guarde o verificador PKCE, o state e o emissor esperado:
verificador = Assinafy::OAuth.generate_code_verifier
state = Assinafy::OAuth.generate_state
session[:assinafy_code_verifier] = verificador
session[:assinafy_state] = state
session[:assinafy_issuer] = Assinafy::OAuth::AUTHORIZATION_SERVER # emissor desta tentativa
redirect_to Assinafy::OAuth.authorization_url(
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
redirect_uri: 'https://app.example.com/oauth/callback',
code_verifier: verificador,
state: state,
scope: %w[documents:read documents:write offline_access]
)O code_challenge é derivado do verificador (S256) — o verificador nunca vai para a URL.
Assinafy::OAuth::AUTHORIZATION_SERVER é o emissor de produção. No sandbox, passe
authorization_endpoint: 'https://auth-sandbox.assinafy.com.br/oauth/authorize' e guarde
https://auth-sandbox.assinafy.com.br como emissor.
unless params[:state] == session.delete(:assinafy_state) &&
params[:iss] == session.delete(:assinafy_issuer)
raise 'resposta de autorização inválida'
end
raise "autorização não concedida: #{params[:error]}" if params[:error] # access_denied, invalid_scope, ...
tokens = Assinafy::Client.new.oauth.exchange_code(
code: params.fetch(:code),
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
code_verifier: session.delete(:assinafy_code_verifier),
redirect_uri: 'https://app.example.com/oauth/callback'
)
tokens['access_token'] # => "..."
tokens['expires_in'] # => 3600
tokens['refresh_token'] # => presente apenas com offline_access
tokens['scope'] # => "documents:read documents:write"Confira state e iss contra os valores guardados nesta tentativa antes de qualquer outra
coisa, inclusive num retorno com error= — é a proteção contra CSRF e contra respostas que não
são suas. O código vale uma vez e expira 60 segundos depois da aprovação: troque-o na hora, sem
repetir. O SDK confere localmente o formato do code_verifier, porque o servidor reporta um
verificador malformado como invalid_grant, indistinguível de um código expirado.
access_token = tokens.fetch('access_token')
# O token só vale na workspace que o usuário escolheu.
workspace_id = Assinafy::Client.new(token: access_token).accounts.list[:data].first.fetch('id')
conexao = Conexao.create!(
workspace_id: workspace_id,
scope: tokens.fetch('scope'),
access_token: access_token, # guarde criptografado
refresh_token: tokens['refresh_token'], # guarde criptografado
expires_at: Time.now + tokens.fetch('expires_in')
)
usuario = Assinafy::Client.new(token: conexao.access_token, account_id: conexao.workspace_id)
usuario.documents.list
usuario.oauth.userinfo # => { 'sub' => ..., 'name' => ..., 'email' => ... } (escopo openid)Cada renovação devolve um refresh token novo, válido por mais 30 dias, e aposenta o anterior: a conexão só expira se passar 30 dias sem renovar. Reutilizar um refresh token aposentado encerra a conexão inteira. Por isso, renove sob um lock por conexão e guarde os tokens novos atomicamente, antes de usá-los:
conexao.with_lock do # lock de linha: uma renovação por vez por conexão
next if conexao.expires_at > Time.now + 60 # outro processo já renovou
novos = Assinafy::Client.new.oauth.refresh(
refresh_token: conexao.refresh_token,
client_id: ENV.fetch('ASSINAFY_CLIENT_ID')
)
conexao.update!(
refresh_token: novos.fetch('refresh_token'),
access_token: novos.fetch('access_token'),
expires_at: Time.now + novos.fetch('expires_in')
)
end
usuario = Assinafy::Client.new(token: conexao.access_token, account_id: conexao.workspace_id)O SDK não renova o token automaticamente e envia cada pedido de token uma única vez — nunca adicione middleware de retry à conexão.
refreshlevantaAssinafy::Errorem vez de devolver um sucesso sem refresh token novo; trate comoinvalid_grant.Se a renovação falhar sem resposta clara — timeout, conexão interrompida,
5xx—, o servidor pode ter trocado o token sem que a resposta chegasse. Releia o refresh token guardado: se ainda for o que você enviou, nunca o reenvie; peça ao usuário para conectar de novo. Siga em frente só se outro processo já tiver guardado um token diferente. Só é seguro repetir uma falha que comprovadamente aconteceu antes do envio: DNS, conexão recusada, handshake TLS. Num401da API, renove uma vez; se falhar, ou eminvalid_grant, peça ao usuário para conectar de novo.
Sem offline_access não há refresh token: quando o access token expirar, mande o usuário pelo
fluxo de autorização de novo.
Revogue o refresh token guardado agora — o mais recente — e só depois apague os tokens:
Assinafy::Client.new.oauth.revoke(
token: conexao.reload.refresh_token,
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
token_type_hint: 'refresh_token'
)
conexao.destroy!Revogar um refresh token invalida também os access tokens emitidos a partir dele. A revogação
responde 200 inclusive para um token já aposentado, então revogar uma cópia antiga parece dar
certo enquanto a conexão continua ativa.
recurso = client.oauth.protected_resource_metadata
recurso['authorization_servers'] # => ["https://auth.assinafy.com.br"]
servidor = client.oauth.authorization_server_metadata
servidor['token_endpoint'] # => "https://api.assinafy.com.br/v1/oauth/token"
servidor['code_challenge_methods_supported'] # => ["S256"]authorization_server_metadata acessa outro host e é enviado sem credenciais de workspace. Ele
aceita uma URL alternativa, desde que seja HTTPS absoluta; qualquer outra levanta
ValidationError.
O servidor também publica o grant RFC 8693 urn:ietf:params:oauth:grant-type:token-exchange,
reservado a clientes confidenciais internos provisionados pela Assinafy; apps de marketplace usam
autorização com PKCE e renovação. client.oauth.token aceita esse grant, com client_secret,
subject_token, subject_token_type e resource. Ele não emite refresh token.
Os endpoints OAuth respondem com o objeto plano da RFC 6749, não com o envelope da API. O SDK
levanta Assinafy::OAuthError (subclasse de Assinafy::ApiError):
begin
client.oauth.exchange_code(...)
rescue Assinafy::OAuthError => e
e.error # => "invalid_grant"
e.error_description # => "The authorization code is invalid or has expired."
e.status_code # => 400
endNum 403 de qualquer recurso, e.context[:www_authenticate] traz o desafio que nomeia o escopo
faltante: peça ao usuário para conectar de novo incluindo esse escopo, sem repetir a chamada. Um
403 sem esse desafio indica outra workspace, o papel do usuário ou uma área que tokens OAuth não
alcançam.
documento = client.documents.upload('./contrato-acme.pdf')
documento['id'] # => "document-id"
documento['name'] # => "contrato-acme.pdf"
documento['status'] # => "uploaded"Aceita um caminho, um Hash com :file_path, ou :buffer + :file_name para bytes em memória:
client.documents.upload(buffer: pdf_bytes, file_name: 'contrato-acme.pdf')O documento recebe o nome do arquivo enviado; para mudar, use
client.documents.rename(documento['id'], 'Contrato Acme'). Somente PDF, no máximo 25 MB: o SDK
confere a extensão, o tamanho e o cabeçalho %PDF- antes de enviar.
A Assinafy extrai páginas e metadados de forma assíncrona. Só é possível abrir um assignment depois disso:
documento = client.documents.wait_until_ready(
documento['id'],
max_wait_seconds: 30,
poll_interval_seconds: 2
)
documento['status'] # => "metadata_ready"
documento['pages'] # => [{ 'id' => 'page-id', 'number' => 1, 'height' => 1651, 'width' => 1275 }]Erros de rede durante a espera são tolerados; um status terminal (failed, expired,
rejected_by_*) ou o fim do prazo interrompe com Assinafy::Error.
Um template é um PDF reutilizável com papéis e campos definidos. Configure os papéis e campos no
aplicativo Assinafy: o upload cria só um papel Editor, e a geração exige pelo menos um papel
Signer.
template = client.templates.create('./contrato-modelo.pdf', name: 'Contrato padrão') # até 25 MB
client.templates.list
client.templates.get(template['id'])
client.templates.update(template['id'], name: 'Contrato padrão v2')
client.templates.download_page(template['id'], 'page-id') # bytes da imagem da páginaPara gerar, informe uma entrada por papel, cada uma com role_id (de template['roles']) e o ID de
um signatário já existente e diferente para cada papel. A geração cria o documento e o assignment
juntos:
papel = template.fetch('roles').find { |r| r['assignment_type'] == 'Signer' }
papeis = [{ role_id: papel.fetch('id'), id: signatario['id'],
verification_method: 'Email', notification_methods: ['Email'] }]
client.documents.estimate_cost_from_template(template['id'], papeis)
documento = client.documents.create_from_template(
template['id'], papeis,
name: 'contrato-acme.pdf', message: 'Por favor, assine.', expires_at: '2099-12-31T23:59:00-03:00'
)Definições de campo reutilizáveis, usadas em assignments collect e templates:
campo = client.fields.create(name: 'CPF', type: 'text', regex: '/^\d{11}$/')
client.fields.types # tipos de campo disponíveis
client.fields.list
client.fields.validate(campo['id'], '12345678901')
client.fields.validate_multiple([{ field_id: campo['id'], value: '12345678901' }])Signatários pertencem à conta e podem ser reaproveitados entre documentos:
signatario = client.signers.find_by_email('ana@example.com') || # paginação percorrida pelo SDK
client.signers.create(full_name: 'Ana Silva', email: 'ana@example.com')
signatario['id'] # => "signer-id"Para notificar por WhatsApp, informe whatsapp_phone_number (ou o alias phone:) em E.164. Para
certificado digital, informe o CPF ou CNPJ em government_id já na criação:
certificado = client.signers.create(
full_name: 'Ana Silva',
email: 'ana@example.com',
government_id: ENV.fetch('ASSINAFY_SIGNER_GOVERNMENT_ID') # CPF/CNPJ real autorizado
)
# Ou num signatário existente:
client.signers.update(signatario['id'], government_id: ENV.fetch('ASSINAFY_SIGNER_GOVERNMENT_ID'))client.signers.validate_create!(payload) valida e normaliza o corpo sem fazer requisição.
Cada signatário do assignment tem um método de verificação (como prova a identidade antes de
assinar) e um método de notificação (como recebe o convite). Verificação e notificação são
acopladas: envie um, os dois, ou nenhum — o lado que faltar é inferido do outro. Sem nenhum dos
dois, ambos assumem Email.
| Verificação | Como funciona | Notificação permitida | Custo por signatário |
|---|---|---|---|
Email (padrão) |
código de uso único (OTP) por e-mail, exigido antes de assinar | Email |
0 crédito |
Whatsapp |
código de uso único (OTP) por WhatsApp; exige whatsapp_phone_number e plano pago |
Whatsapp |
0,45 crédito (a notificação por WhatsApp) |
DigitalCertificate |
o signatário assina com o próprio certificado ICP-Brasil (A1/A3), gerando uma assinatura PAdES qualificada | Email ou Whatsapp |
0,5 crédito + a notificação (0 ou 0,45) |
- Informe exatamente um canal em
notification_methods(um array de um elemento). - O custo do certificado digital aparece na estimativa com o código
SignatureDigitalCertificate. Reenviar uma notificação cobra a notificação de novo. - Os valores aceitos estão em
Assinafy::Resources::AssignmentResource::VERIFICATION_METHODSe::NOTIFICATION_METHODS. O SDK valida enum e combinação localmente: um valor fora do enum, ou um par inválido, levantaValidationErrorantes de a requisição sair.
Exige o recurso Certificado Digital na conta (planos Standard e Pro), CPF ou CNPJ em
government_id do signatário, e que cada signatário por certificado esteja sozinho no seu
passo. Um CPF exige o certificado daquela pessoa (e-CPF, ou e-CNPJ que a nomeie como
representante legal); um CNPJ exige um e-CNPJ da empresa, de qualquer um de seus representantes.
Antes de enviar — principalmente com WhatsApp ou certificado digital:
estimativa = client.assignments.estimate_cost(
documento['id'],
signers: [
{ verification_method: 'Email' },
{ verification_method: 'DigitalCertificate', notification_methods: ['Whatsapp'] }
]
)
estimativa['total_credits'] # => 0.95
estimativa['breakdown'] # itens com code, quantity, unit_cost, cost
estimativa['has_sufficient_resources'] # => true
estimativa['blocking_reason'] # => nil, ou "InsufficientCredits", ...Na estimativa, os signatários podem ser descritos só pelo método, sem id. Para um template, use
client.documents.estimate_cost_from_template; para um reenvio,
client.assignments.estimate_resend_cost.
Um assignment é o convite para assinar um documento. Todo assignment exige ao menos um signatário.
virtual — sem campos posicionados; o signatário aceita o documento inteiro.
assignment = client.assignments.create(
documento['id'],
method: 'virtual',
signers: [
{ id: signatario['id'], verification_method: 'Email', notification_methods: ['Email'], step: 1 },
{ id: certificado['id'], verification_method: 'DigitalCertificate', notification_methods: ['Email'], step: 2 }
],
message: 'Por favor, assine o contrato em anexo.',
expires_at: '2099-12-31T23:59:00-03:00',
copy_receivers: [] # IDs de signatários que só recebem cópia
)
assignment['signing_urls']
# => [{ 'signer_id' => 'signer-id', 'url' => 'https://.../sign/...' }, ...]collect — campos posicionados página a página, além da lista de signatários:
client.assignments.create(
documento['id'],
method: 'collect',
signers: [{ id: signatario['id'] }],
entries: [{
page_id: documento['pages'].first['id'],
fields: [{
signer_id: signatario['id'],
field_id: campo['id'],
display_settings: { left: 100, top: 100, width: 240, height: 48, fontSize: 16 }
}]
}]
)O SDK aceita signers: ['id1', 'id2'] (IDs puros), signers: [{ id: ... }] (descritores completos)
e o formato legado signer_ids:. Tudo é normalizado para o corpo que a API espera.
stepdefine a ordem: todos do passo 1 assinam antes do passo 2; passos iguais assinam em paralelo. Se informado para um signatário, informe para todos, em uma sequência contínua iniciada em 1. Omita para assinatura simultânea.- Cada signatário é notificado quando seu passo é ativado: o passo 1 na criação, os seguintes quando todos do passo anterior concluírem.
- Um signatário por certificado digital fica sozinho no seu passo.
expires_atdeve ser ISO 8601 com fuso horário e estar pelo menos uma hora no futuro.
Gerenciar depois de criado:
client.assignments.estimate_resend_cost(documento['id'], assignment['id'], signatario['id'])
client.assignments.resend_notification(documento['id'], assignment['id'], signatario['id'])
client.assignments.reset_expiration(documento['id'], assignment['id'], '2099-01-31T23:59:00-03:00')
client.assignments.whatsapp_notifications(documento['id'], assignment['id'])
client.assignments.list # assignments da contaresultado = client.upload_and_request_signatures(
source: './contrato-acme.pdf',
signers: [{ full_name: 'Ana Silva', email: 'ana@example.com' }],
message: 'Por favor, assine o contrato em anexo.'
)
resultado[:document]['id'] # => "document-id"
resultado[:assignment]['id'] # => "assignment-id"
resultado[:signer_ids] # => ["signer-id"]O helper faz upload, espera o processamento, cria os signatários (incluindo government_id, quando
informado) e abre um assignment virtual. Ele valida o payload inteiro antes de enviar
qualquer coisa, então um expires_at malformado não deixa um documento órfão.
Não é transacional. Se uma chamada posterior falhar, o documento enviado e os signatários já criados continuam existindo. Em caso de erro,
e.context[:document]ee.context[:signer_ids]trazem o que foi criado, para você limpar.
O signatário recebe o link de acesso pelo canal de notificação. As chamadas abaixo usam o código
de acesso do signatário (enviado como parâmetro de query signer-access-code), não suas
credenciais de workspace — o SDK remove X-Api-Key/Authorization delas. Um cliente sem
credenciais basta:
signatario_client = Assinafy::Client.new
codigo = 'codigo-de-acesso-do-link-de-assinatura'
# 1. Carregar o documento e aceitar os termos
dados = signatario_client.signers.self_data(signer_access_code: codigo)
signatario_client.signers.accept_terms(signer_access_code: codigo) unless dados['has_accepted_terms']
doc = signatario_client.assignments.signer_document(signer_access_code: codigo, has_accepted_terms: true)
# 2. Confirmar a identidade
signatario_client.signers.confirm_data(
doc['id'], { full_name: 'Ana Silva', government_id: '00000000000' },
signer_access_code: codigo
)
# 3. Verificar o código de uso único (OTP) recebido por e-mail ou WhatsApp
signatario_client.signers.verify_email(verification_code: '123456', signer_access_code: codigo)
# 4. Enviar a imagem da assinatura (PNG)
signatario_client.signers.upload_signature(
File.binread('assinatura.png'), signer_access_code: codigo, type: 'signature'
)verify_email envia o código de qualquer canal (POST /verify). Em seguida, assine.
Assignment virtual:
signatario_client.signer_documents.sign_multiple([doc['id']], signer_access_code: codigo)Assignment collect — envie cada item posicionado:
itens = doc.fetch('assignment').fetch('items').map do |item|
{
item_id: item.fetch('id'),
field_id: item.dig('field', 'id'),
page_id: item.dig('page', 'id'),
value: 'Aceito'
}
end
signatario_client.assignments.sign(doc['id'], doc.dig('assignment', 'id'), itens, signer_access_code: codigo)O SDK converte as chaves item_id/field_id/page_id para o itemId/fieldId/pageId que a API
espera.
Recusar:
signatario_client.assignments.decline(
doc['id'], doc.dig('assignment', 'id'),
decline_reason: 'Valores divergentes da proposta',
signer_access_code: codigo
)
# Vários documentos de uma vez:
signatario_client.signer_documents.decline_multiple([doc['id']], decline_reason: 'Não', signer_access_code: codigo)O signatário por certificado digital conclui a assinatura no fluxo hospedado da Assinafy, aberto
pelo link de assignment['signing_urls']: ele aceita os termos, confirma os dados e assina com o
certificado A1 ou A3 pela extensão de navegador Web PKI. O endpoint comum de assinatura rejeita
signatários por certificado, e o SDK não envolve o handshake Web PKI
(/signers/certificate/start, /signers/certificate/complete), cujos esquemas não fazem parte do
contrato OpenAPI. Concluído o fluxo, o artefato pades traz a assinatura PAdES qualificada.
Para acompanhar sem polling, registre um endpoint de webhook. Uma conta pode ter 1 endpoint, ou até 3 em planos pagos; cada um tem sua própria URL (distinta das demais), lista de eventos e configuração de assinatura. Todo endpoint ativo inscrito num evento o recebe.
client.webhooks.list_event_types # => [{ 'id' => 'document_ready', 'description' => '...' }, ...]
endpoint = client.webhooks.create_endpoint(
url: 'https://app.example.com/webhooks/assinafy',
email: 'ops@example.com', # avisos de falha de entrega
events: %w[document_ready signer_signed_document signer_rejected_document],
name: 'ERP',
signing_enabled: true
)
endpoint['id'] # => "webhook-endpoint-id"
client.webhooks.list_endpoints # do mais antigo ao mais novo
client.webhooks.get_endpoint('webhook-endpoint-id')
client.webhooks.update_endpoint('webhook-endpoint-id', is_active: false) # só os campos enviados
client.webhooks.delete_endpoint('webhook-endpoint-id') # => nil, libera a vagacreate_endpointexigeurl,emaileevents;name,is_active(padrãotrue) esigning_enabled(padrãofalse) são opcionais. Chaves desconhecidas levantamValidationErrorlocalmente.- Criar além do limite do plano responde
403; uma URL já usada por outro endpoint responde400. - Em
update_endpoint,signing_enabled: truegera um segredo se o endpoint não tiver um (e mantém o atual se tiver);signing_enabled: falsedescarta o segredo.
segredo = client.webhooks.endpoint_secret('webhook-endpoint-id')
segredo['secret'] # => "whsec_..."
novo = client.webhooks.rotate_endpoint_secret('webhook-endpoint-id')A rotação vale imediatamente: entregas posteriores são assinadas só com o segredo novo, então
atualize o receptor na mesma hora. Ler e rotacionar o segredo não está disponível para
aplicativos OAuth — use uma chave de API ou um token de sessão de usuário. Com assinatura
desativada, ambos respondem 400.
| Propriedade | Valor |
|---|---|
| Requisição | POST, Content-Type: application/json |
webhook-id |
ID da mensagem, igual em todas as tentativas do mesmo evento para o mesmo endpoint — use para deduplicar |
webhook-timestamp |
horário Unix (segundos) da tentativa |
webhook-signature |
só com assinatura ativa: entradas v1,<base64> separadas por espaço |
| Sucesso | qualquer resposta 2xx |
| Tentativas | até 2 por evento, com 3 segundos de intervalo |
| Circuit breaker | depois de 10 eventos consecutivos com falha, a entrega pausa e só uma amostra de eventos é testada até um dar certo |
Responda 2xx rapidamente e processe em segundo plano. Para forçar uma nova entrega, use
retry_dispatch:
client.webhooks.list_dispatches(endpoint_id: 'webhook-endpoint-id', delivered: false, per_page: 20)
client.webhooks.retry_dispatch('dispatch-id')O corpo segue o envelope { id, event, message, payload, origin, created_at, subject, object, account_id }. subject é quem agiu e object é a entidade afetada, cada um com uma propriedade
type (User, Signer, Account, Document ou Template). Os horários do corpo são Unix em
segundos.
As assinaturas seguem a especificação Standard Webhooks: um
HMAC-SHA256 sobre "{webhook-id}.{webhook-timestamp}.{corpo bruto}", com a chave obtida
decodificando em base64 a parte do segredo depois de whsec_. verify_delivery confere essa
assinatura em tempo constante e rejeita horários a mais de 5 minutos do relógio local:
verificador = Assinafy::Support::WebhookVerifier.new(ENV.fetch('ASSINAFY_WEBHOOK_SECRET'))
# ou client.webhook_verifier, que usa o webhook_secret do cliente
verificador.verify_delivery(corpo_bruto, headers) # => true / false
verificador.verify_delivery(corpo_bruto, headers, tolerance: 120) # janela em segundosheaders pode ser o request.headers do Rails, o env do Rack (HTTP_WEBHOOK_ID) ou um Hash
simples. O método devolve false — nunca levanta — para segredo ausente ou malformado, cabeçalho
faltando, assinatura errada ou horário fora da janela. Sempre passe o corpo bruto, exatamente
como recebido, nunca o JSON reserializado. Com mais de um endpoint, cada um tem seu segredo.
Receptor Rails:
class AssinafyWebhooksController < ActionController::API
VERIFICADOR = Assinafy::Support::WebhookVerifier.new(ENV.fetch('ASSINAFY_WEBHOOK_SECRET'))
def create
corpo = request.raw_post
return head(:unauthorized) unless VERIFICADOR.verify_delivery(corpo, request.headers)
webhook_id = request.headers['webhook-id']
return head(:ok) if EventoAssinafy.exists?(webhook_id: webhook_id) # já processado
evento = VERIFICADOR.extract_event(corpo)
EventoAssinafy.create!(webhook_id: webhook_id, tipo: VERIFICADOR.event_type(evento), corpo: corpo)
ProcessarEventoAssinafyJob.perform_later(webhook_id)
head :ok
end
endReceptor Rack (Sinatra):
post '/webhooks/assinafy' do
corpo = request.body.read
halt 401 unless VERIFICADOR.verify_delivery(corpo, request.env)
evento = VERIFICADOR.extract_event(corpo)
case VERIFICADOR.event_type(evento)
when 'signer_signed_document' then processar(VERIFICADOR.event_object(evento)) # o Document
when 'document_ready' then baixar_assinado(VERIFICADOR.event_object(evento)['id'])
end
200
endextract_event devolve nil para JSON inválido; event_type, event_payload, event_object e
event_subject leem os campos do envelope.
O método verify(corpo, assinatura_hex) continua disponível apenas para receptores cujo próprio
gateway assina o corpo com um HMAC-SHA256 em hexadecimal e um segredo compartilhado.
client.webhooks.register, #get e #inactivate (/webhooks/subscriptions) continuam funcionando e
agem sobre o endpoint mais antigo da conta. register aceita só url, email, events e
is_active; outras chaves levantam ValidationError. Prefira as operações de endpoint.
client.documents.signing_progress(documento['id'])
# => { signed: 1, total: 3, pending: 2, percentage: 33.33 }
client.documents.fully_signed?(documento['id']) # => false
client.documents.activities(documento['id']) # trilha completa de eventos, com origin (ip, user-agent)| Artefato | Conteúdo |
|---|---|
original |
O PDF enviado, como recebido |
certificated |
O documento assinado, com a certificação da plataforma |
certificate-page |
Apenas a página de certificação |
pades |
Assinaturas ICP-Brasil dos signatários + caixa de certificação — só em documentos com signatários por certificado digital |
bundle |
Zip com original, certificated e certificate-page, mais o pades quando houver |
File.binwrite('assinado.pdf', client.documents.download(documento['id'], 'certificated'))
File.binwrite('pades.pdf', client.documents.download(documento['id'], 'pades'))
File.binwrite('pacote.zip', client.documents.download(documento['id'], 'bundle'))A verificação pública confere um documento assinado pelo hash da assinatura, sem autenticação:
verificacao = Assinafy::Client.new.documents.verify('hash-da-assinatura')
verificacao['is_valid'] # => true
verificacao['agreement_code'] # código impresso no certificado do documentoEla devolve o resultado da Assinafy; não valida independentemente a assinatura do PDF nem a cadeia de certificação.
tag = client.tags.create(name: 'Contratos 2026', color: '2072b9')
client.documents.append_tags(documento['id'], [tag['id']])
client.documents.replace_tags(documento['id'], [tag['id']])
client.documents.detach_tag(documento['id'], tag['id'])
# Documentos que carregam TODAS as tags informadas (o SDK junta o Array com vírgulas):
client.documents.list(tags: [tag['id'], 'outra-tag-id'])
client.documents.rename(documento['id'], 'Contrato Acme — assinado')Apague só o que a sua aplicação criou, e só depois de concluir o que depende do recurso. Alguns
respondem 409 enquanto ainda estão em processamento ou referenciados. Mantenha signatários e
templates compartilhados.
client.documents.delete(documento['id'])
client.signers.delete(signatario['id'])
client.webhooks.delete_endpoint('webhook-endpoint-id')
client.tags.delete(tag['id'], force: true) # force desvincula de documentos e templatesbegin
client.documents.details(documento_id)
rescue Assinafy::ValidationError => e
# entrada inválida — detectada ANTES de qualquer requisição
warn e.errors.inspect
rescue Assinafy::OAuthError => e
# falha em endpoint OAuth (subclasse de ApiError)
warn "#{e.error}: #{e.error_description}"
rescue Assinafy::ApiError => e
# a API respondeu com erro
warn "Assinafy respondeu #{e.status_code}: #{e.message}"
warn e.response_data.inspect
rescue Assinafy::NetworkError => e
# conexão, timeout ou TLS
warn "Falha de rede: #{e.message}"
rescue Assinafy::Error => e
# qualquer outra falha do SDK
warn e.context.inspect
endToda exceção do SDK deriva de Assinafy::Error e carrega um #context com detalhes para
depuração. ApiError trata as duas formas de corpo de erro da API — o erro de framework
({"name":..., "code":..., "status":...}) e o envelope de aplicação
({"status":..., "data":null, "message":...}) — inclusive quando um 200 traz um status
interno de falha. Num 403, e.context[:www_authenticate] traz o desafio OAuth, quando houver.
Os métodos *.list* devolvem { data: [...], meta: { ... } } quando a API envia cabeçalhos de
paginação:
pagina = client.documents.list(page: 1, per_page: 50)
pagina[:data] # => [Documento, ...]
pagina[:meta] # => { current_page: 1, per_page: 50, total: 128, last_page: 3 }O per_page: em estilo Ruby é convertido para o parâmetro per-page documentado. Valores acima
do máximo são limitados pelo servidor. Endpoints que não paginam (por exemplo accounts.list)
devolvem só { data: [...] }, sem meta.
def cada_documento(client)
return to_enum(:cada_documento, client) unless block_given?
pagina = 1
loop do
resultado = client.documents.list(page: pagina, per_page: 50)
resultado[:data].each { |documento| yield documento }
meta = resultado[:meta]
break unless meta && meta[:last_page] && pagina < meta[:last_page]
pagina += 1
end
end| Acessor | Cobre |
|---|---|
client.auth |
login, autenticação em dois fatores, login social, senha, chaves de API |
client.oauth |
OAuth 2.1: token, refresh, revoke, userinfo, descoberta |
client.accounts |
workspaces, tema, KPIs, logo |
client.users |
perfil próprio, KPIs, preferências de notificação |
client.documents |
upload, listagem, download, ciclo de vida, tags, verificação |
client.signers |
CRUD de signatários e todo o autoatendimento do signatário |
client.signer_documents |
documentos do signatário, assinar/recusar em lote |
client.assignments |
pedidos de assinatura, custos, reenvios, assinatura, recusa |
client.templates |
ciclo de vida de templates |
client.tags |
CRUD de tags |
client.fields |
definições de campo e validação |
client.webhooks |
endpoints, segredos de assinatura, entregas, reenvio |
client.webhook_verifier |
verificação de assinatura das entregas recebidas |
KPIs: client.users.stats e client.accounts.stats aceitam granularity: ('monthly' ou
'daily') e month: ('AAAA-MM'), validados localmente:
client.accounts.stats(granularity: 'daily', month: '2026-06')client.faraday_connection expõe a conexão Faraday para inspeção em testes. Nunca adicione
middleware que repita requisições: pedidos de token não podem ser reenviados.
sig/assinafy.rbs acompanha a gem, então consumidores que usam RBS têm as assinaturas
publicadas. spec/rbs_signature_spec.rb garante que todo método
público de recurso, do Client e de Assinafy::OAuth tenha uma assinatura declarada.
- README.en.md — referência completa por recurso, em inglês
- docs/API_REFERENCE.md — referência por operação
- CHANGELOG.md — histórico de versões
- Documentação da API
bundle exec rake spec
bundle exec rubocop
bundle exec bundler-audit check --update
rbs -I sig validate
ruby scripts/check_api_contract.rbA suíte sandbox fica fora da execução padrão.
Veja os pré-requisitos e variáveis, incluindo um template
com papel Signer para testar a geração. Os testes enviam e-mails e removem os recursos criados.
Distribuído sob a licença MIT.