Português · Read in English
Cliente Java para a API Assinafy — plataforma brasileira de assinatura eletrônica de documentos. Cobre as 93 operações documentadas — upload e certificação de documentos, gestão de signatários, solicitações de assinatura, templates, definições de campo, tags, workspaces, webhooks, OAuth 2.1 e os fluxos self-service do signatário — atrás de modelos tipados, exceções tipadas e um único cliente thread-safe.
Este documento é o guia completo em português: leia de cima para baixo e você terá percorrido o caminho inteiro, do PDF em disco ao arquivo assinado. O manual de referência operação por operação está em README.en.md, e o contrato de requisição/resposta de cada rota em docs/API_REFERENCE.md.
- Requisitos
- Instalação
- Autenticação
- Configuração
- Como o SDK é organizado
- O ciclo de vida da assinatura
- Métodos de verificação e notificação
- Certificado digital ICP-Brasil (A1 e A3)
- O atalho de uma chamada só
- OAuth 2.1 — agir no workspace de outra pessoa
- Templates, campos e tags
- Self-service do signatário
- Webhooks
- Workspaces, usuários e chaves de API
- Tratamento de erros
- Paginação
- Ambientes
- Desenvolvimento
- Documentação
- Licença
- JDK 25 (LTS). O build exige Java
>=25,<26. - Maven Wrapper fixado no Maven 3.9.16 — não é necessário ter Maven instalado no sistema.
- TLS 1.2 ou superior: o cliente HTTP padrão recusa TLS 1.0 e 1.1.
As dependências de runtime são OkHttp e Jackson. O jar publicado declara
Automatic-Module-Name: com.assinafy.sdk.
As tags de release publicam no GitHub Packages, e o GitHub exige autenticação mesmo para pacotes
Maven públicos. Exporte um usuário do GitHub e um personal access token clássico com read:packages:
export GITHUB_ACTOR=seu-usuario-github
export GITHUB_TOKEN=seu-personal-access-token-classicoReferencie essas variáveis no ~/.m2/settings.xml:
<settings>
<servers>
<server>
<id>github</id>
<username>${env.GITHUB_ACTOR}</username>
<password>${env.GITHUB_TOKEN}</password>
</server>
</servers>
</settings>Depois adicione o repositório e a dependência ao seu projeto:
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/assinafy/java-sdk</url>
</repository>
</repositories>
<dependency>
<groupId>com.assinafy</groupId>
<artifactId>assinafy-sdk</artifactId>
<version>1.12.0</version>
</dependency>Trabalhando a partir de um checkout do código? Instale o artefato no seu repositório local primeiro:
./mvnw installA API aceita três credenciais, e a escolha depende de em qual workspace o seu código age.
| Credencial | Age em | Use quando |
|---|---|---|
Chave de API (X-Api-Key) |
O seu próprio workspace | Você automatiza a sua própria conta. É a opção recomendada para integrações de servidor. |
Token de acesso (Authorization: Bearer) |
O workspace da sessão | Você já tem um token de uma sessão interativa, ou um token OAuth. |
| Token OAuth 2.1 | O workspace de outra pessoa, com a permissão dela | Você constrói um produto que outros clientes Assinafy conectam. Veja OAuth 2.1. |
// Preferido: header X-Api-Key
AssinafyClient apiKeyClient = new AssinafyClient(
AssinafyClientOptions.builder()
.apiKey("sua-chave-de-api")
.accountId("seu-account-id")
.build()
);
// Authorization: Bearer — token de acesso, vindo de authentication().login(...) ou do OAuth
AssinafyClient bearerClient = new AssinafyClient(
AssinafyClientOptions.builder()
.token("jwt-token")
.accountId("seu-account-id")
.build()
);Quando as duas estão configuradas, a chave de API vence. As operações voltadas ao signatário usam uma
quarta credencial — o código de acesso do signatário — passada por chamada, e não configurada no
cliente. O transporte padrão omite X-Api-Key e Authorization nas rotas públicas, de login e
recuperação de senha, nos grants OAuth e nas chamadas com código do signatário. Chamadas autenticadas
continuam usando a credencial configurada.
| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
apiKey |
String | — | Credencial preferida, enviada como X-Api-Key. |
token |
String | — | Token de acesso bearer, usado quando não há chave de API. |
accountId |
String | — | Workspace padrão para operações com escopo de conta. |
baseUrl |
String | https://api.assinafy.com.br/v1 |
URL base HTTPS da API. HTTP puro é rejeitado, exceto em testes de loopback; use AssinafyClientOptions.SANDBOX_BASE_URL para o sandbox. |
timeoutMs |
long | 30000 |
Timeout de chamada, conexão, leitura e escrita, em milissegundos. |
logger |
Logger | No-op | Callback de diagnóstico estruturado. Um logger que lança exceção nunca afeta a chamada à API. |
Dois métodos de fábrica cobrem os casos comuns:
AssinafyClient client = AssinafyClient.create("chave-de-api", "account-id");
AssinafyClientOptions extras = AssinafyClientOptions.builder()
.baseUrl(AssinafyClientOptions.SANDBOX_BASE_URL)
.timeoutMs(60_000)
.build();
AssinafyClient sandboxClient = AssinafyClient.create("chave-de-api", "account-id", extras);Um cliente, vários recursos. AssinafyClient é dono do transporte HTTP e expõe um acessor por
área da API: documents(), signers(), assignments(), templates(), fields(), tags(),
workspaces(), webhooks(), users(), apiKeys(), authentication(), oauth() e
publicDocuments(). Os acessores devolvem as instâncias que o cliente possui, então guardar a
referência de um recurso equivale a guardar o cliente.
O cliente é thread-safe com o transporte OkHttp padrão e foi feito para ser criado uma vez e compartilhado. O OkHttp libera conexões e threads ociosas sozinho; não há shutdown a chamar.
Um tipo Java por recurso da API. A API devolve o mesmo schema Document no upload, na listagem,
na busca, no get, no rename e na criação a partir de template — e o SDK espelha isso: todos esses
métodos devolvem Document. Um campo que determinada resposta não preenche vem null: um documento
recém-enviado não tem assignment nem pages até o processamento chegar em metadata_ready.
Workspace, Template, Signer e Assignment funcionam do mesmo jeito.
Os envelopes são desembrulhados para você. Os corpos JSON de sucesso são
{ "status": inteiro, "message": string, "data": ... }, e os métodos do SDK devolvem o data. Um
método void descarta o envelope de sucesso e também aceita um corpo 2xx vazio. Métodos binários
devolvem byte[] cru, sem decodificação JSON. Um HTTP não-2xx — ou um status numérico não-2xx
dentro de um envelope 200 — vira ApiException.
As rotas OAuth são a exceção deliberada: por exigência do RFC 6749 e do OpenID Connect, elas
respondem com objetos JSON planos (access_token na raiz, ou {error, error_description}) em vez do
envelope. O SDK trata as duas formas.
Escopo de conta. Operações com escopo de conta usam o accountId configurado no cliente. Todas
elas também têm uma sobrecarga que recebe o ID explicitamente, então um único cliente atende vários
workspaces. workspaces() sempre recebe o ID explícito, porque suas operações são sobre o
workspace, e não dentro dele.
O caminho mais curto, do PDF em disco à solicitação de assinatura despachada:
import com.assinafy.sdk.AssinafyClient;
import com.assinafy.sdk.AssinafyClientOptions;
import com.assinafy.sdk.models.Assignment;
import com.assinafy.sdk.models.Document;
import com.assinafy.sdk.models.Signer;
import com.assinafy.sdk.request.CreateAssignmentRequest;
import com.assinafy.sdk.request.CreateSignerRequest;
import com.assinafy.sdk.request.SignerReference;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
AssinafyClient client = new AssinafyClient(
AssinafyClientOptions.builder()
.apiKey(System.getenv("ASSINAFY_API_KEY"))
.accountId(System.getenv("ASSINAFY_ACCOUNT_ID"))
.build()
);
byte[] fileData = Files.readAllBytes(Path.of("contrato.pdf"));
Document document = client.documents().upload(fileData, "contrato.pdf");
client.documents().waitUntilReady(document.getId());
Signer signer = client.signers().findOrCreate(
CreateSignerRequest.builder()
.fullName("Maria Silva")
.email("maria@example.invalid")
.build()
);
Assignment assignment = client.assignments().create(
document.getId(),
CreateAssignmentRequest.builder()
.method("virtual")
.signers(List.of(SignerReference.ofId(signer.getId())))
.message("Por favor, assine este documento")
.build()
);Cada passo, em detalhe:
O seu backend guarda accountId, document.getId(), assignment.getId() e os IDs dos signatários
junto ao registro do contrato. A URL de cada signatário está em assignment.getSigningUrls();
ela é uma credencial, portanto não a publique nem a registre em logs. O signatário abre essa URL,
aceita os termos, confirma os dados, valida o OTP de e-mail/WhatsApp ou usa o certificado A1/A3,
e assina. Os webhooks avisam seu backend sobre o andamento; ele consulta os detalhes para confirmar
o estado atual e só disponibiliza o arquivo final quando a certificação terminou.
documents().upload(bytes, nomeDoArquivo) envia um PDF como multipart/form-data e devolve o
Document criado. O limite é 25 MB, verificado no cliente antes do envio. Há uma sobrecarga que
aceita metadados e um account ID explícito.
O documento nasce com status uploaded e ainda não tem páginas nem assignment.
A plataforma extrai páginas e metadados de forma assíncrona. documents().waitUntilReady(id) faz
polling até o status chegar a metadata_ready, pending_signature ou certificated, e lança se o
documento terminar em failed, rejected_by_signer, rejected_by_user ou expired. A sobrecarga
waitUntilReady(id, maxWaitMs, pollIntervalMs) controla o tempo total e o intervalo.
O método collect exige metadata_ready, porque os campos apontam para páginas específicas.
O método virtual também aceita uploaded ou metadata_processing; o servidor promove o documento
para pending_signature quando termina o processamento. Esperar continua sendo útil quando você
precisa das páginas ou quer detectar uma falha no PDF antes de solicitar assinaturas.
Signatários são recursos persistentes do workspace, reutilizados entre documentos.
signers().findOrCreate(...) devolve o signatário cujo e-mail bate, sem distinguir maiúsculas,
ou cria um novo. signers().create(...) sempre cria.
O cadastro exige fullName. Para solicitar uma assinatura, o signatário também precisa do canal
compatível com a verificação escolhida: email ou whatsappPhoneNumber.
O CPF/CNPJ vai em cpf e é gravado pelo SDK através do update documentado
(government_id), com os não-dígitos removidos.
Signer porWhatsapp = client.signers().findOrCreate(
CreateSignerRequest.builder()
.fullName("João Souza")
.whatsappPhoneNumber("+5511999999999")
.cpf("123.456.789-09")
.build()
);assignments().estimateCostTyped(documentId, request) devolve a previsão de créditos sem criar
nada, incluindo hasSufficientResources. Use antes de gastar créditos de notificação.
assignments().create(documentId, request) cria o assignment e dispara as notificações. O method
é virtual (assinatura à distância, o padrão) ou collect (coleta de campos preenchidos, que exige
entries não vazio).
CreateAssignmentRequest pedido = CreateAssignmentRequest.builder()
.method("virtual")
.signers(List.of(
SignerReference.builder().id(maria.getId()).step(1).build(),
SignerReference.builder().id(joao.getId()).step(2)
.verificationMethod("Whatsapp")
.notificationMethods(List.of("Whatsapp"))
.build()
))
.message("Contrato de prestação de serviços")
.expiresAt("2026-12-31T23:59:59Z")
.build();
CostEstimate previsao = client.assignments().estimateCostTyped(document.getId(), pedido);
if (Boolean.TRUE.equals(previsao.getHasSufficientResources())) {
Assignment assignment = client.assignments().create(document.getId(), pedido);
}Ordem de assinatura. Defina step para assinar em etapas. Ou todos os signatários informam
step, ou nenhum informa, e os passos precisam ser contíguos a partir de 1 — o SDK valida isso antes
de enviar. O signatário do passo 1 é notificado na criação; os passos seguintes só são notificados
quando o passo anterior termina.
expiresAt aceita um timestamp ISO-8601 com fuso, pelo menos uma hora no futuro. Por exemplo,
Instant.now().plus(7, ChronoUnit.DAYS).truncatedTo(ChronoUnit.SECONDS).toString() define sete dias
de prazo. O SDK valida o prazo antes de criar a solicitação; os métodos de estimativa ignoram o prazo.
SigningProgress progresso = client.documents().getSigningProgress(document.getId());
boolean pronto = client.documents().isFullySigned(document.getId());
List<DocumentActivity> trilha = client.documents().activities(document.getId());A trilha de atividades devolve todos os eventos registrados, cada um com um snapshot do payload do
evento e a origin da requisição (ip, user-agent).
Precisa reenviar a notificação de um signatário? assignments().estimateResendCostTyped(...) prevê o
custo e assignments().resendNotification(...) reenvia. Para prorrogar o prazo,
assignments().resetExpiration(documentId, assignmentId, novaData).
| 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ó existe em documentos que tiveram signatários por certificado digital |
bundle |
Zip com original, certificated e certificate-page, mais o pades quando houver |
byte[] assinado = client.documents().download(document.getId()); // certificated
byte[] pades = client.documents().download(document.getId(), "pades");
byte[] miniatura = client.documents().thumbnail(document.getId());Um download não-2xx lança em vez de devolver o corpo de erro como se fossem os bytes do arquivo.
isFullySigned pode retornar true enquanto o documento ainda está em certificating, pois todos
os signatários já terminaram. Para guardar o PDF final, confirme certificated nos detalhes:
Document finalizado = client.documents().details(document.getId());
if ("certificated".equals(finalizado.getStatus())) {
Files.write(Path.of("contrato-assinado.pdf"), client.documents().download(finalizado.getId()));
Files.write(Path.of("contrato-certificacao.pdf"),
client.documents().download(finalizado.getId(), "certificate-page"));
}Se ainda não estiver certificado, agende uma nova consulta. waitUntilReady espera o processamento
inicial; ele também aceita pending_signature e não espera todas as pessoas assinarem.
A verificação pública confere um documento assinado pelo hash da assinatura, sem autenticação:
documents().verifyTyped(hash).
documents().delete(id) remove o documento. O uploadAndRequestSignatures usa a mesma rota para
desfazer o próprio upload quando um passo posterior falha.
Guarde os artefatos necessários antes de excluir. documents().getStatuses() informa quais estados
permitem a exclusão; documentos em processamento podem precisar terminar essa etapa primeiro.
Definidos por signatário ao criar o assignment. O método de verificação (como o signatário prova
quem é antes de assinar) e o de notificação (como ele é avisado) são acoplados: envie um, os
dois ou nenhum — o lado que faltar é inferido. Sem nenhum dos dois, ambos assumem Email.
| Método de verificação | Como funciona | Requisitos | Custo por signatário |
|---|---|---|---|
Email (padrão) |
Código de uso único (OTP) por e-mail, exigido antes de assinar | Signatário com e-mail | 0 créditos |
Whatsapp |
Código de uso único (OTP) por WhatsApp | whatsappPhoneNumber no signatário; só em planos pagos |
0,45 crédito (a notificação WhatsApp, que este método exige) |
DigitalCertificate |
O signatário assina com o próprio certificado ICP-Brasil (A1 ou A3), pela extensão de navegador Web PKI, gerando uma assinatura PAdES qualificada | Recurso Certificado Digital na conta; CPF/CNPJ em governmentId; um signatário por certificado em cada passo |
2 créditos + o custo da notificação |
| Método de notificação | Entrega | Requisitos | Custo por signatário |
|---|---|---|---|
Email |
E-mail com o link para assinar | Signatário com e-mail | 0 créditos |
Whatsapp |
Mensagem de WhatsApp com o link para assinar | whatsappPhoneNumber; só em planos pagos |
0,45 crédito |
Combinações permitidas — qualquer outra devolve 400:
| Verificação | Notificações aceitas |
|---|---|
Email |
Email |
Whatsapp |
Whatsapp |
DigitalCertificate |
Email ou Whatsapp |
Apenas um método de notificação por signatário. Nenhum método de verificação tem preço próprio: o que
se cobra é a notificação com que ele anda junto — mais, no caso do certificado digital, a própria
assinatura. Use assignments().estimateCostTyped(...) para ver o total exato antes de criar.
Exige o recurso Certificado Digital na conta (planos Standard e Pro), CPF ou CNPJ em
governmentId do signatário, e exatamente um signatário por certificado naquele 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. O SDK valida a regra de "um por passo" antes de enviar.
Antes de abrir o assignment, o signatário precisa confirmar os dados de identidade
(signers().confirmSignerData(...)) e aceitar os termos (signers().acceptTerms(...)). O endpoint
comum de assinatura rejeita signatários por certificado com 400 — a assinatura deles é
produzida por um handshake de dois passos com a extensão Web PKI:
// 1. Abre a operação e devolve o token que o navegador precisa assinar
String token = client.signers().startCertificateSignature(signerAccessCode);
// 2. O navegador assina o token com o certificado A1/A3 do signatário, pela extensão Web PKI,
// e devolve o valor assinado ao seu backend
// 3. Conclui a assinatura
String nomeNoCertificado =
client.signers().completeCertificateSignature(signerAccessCode, tokenAssinado);As duas rotas estão disponíveis em produção e sandbox, mas não constam do documento OpenAPI. O workspace precisa ter o recurso Certificado Digital habilitado. O SDK não lê arquivos A1 nem acessa dispositivos A3: a chave privada fica no dispositivo do signatário, operado pelo Web PKI.
Concluído o fluxo, baixar o artefato pades devolve a assinatura PAdES qualificada.
uploadAndRequestSignatures faz o caminho inteiro numa chamada: envia o PDF, espera o processamento,
resolve os signatários (reaproveitando por e-mail), grava o CPF e cria o assignment virtual que
dispara as notificações. Entradas só com WhatsApp usam WhatsApp para verificação e notificação
automaticamente.
UploadAndRequestSignaturesResult resultado = client.uploadAndRequestSignatures(
UploadAndRequestSignaturesRequest.builder()
.fileData(fileData)
.fileName("contrato.pdf")
.signers(List.of(
UploadAndRequestSignaturesRequest.SignerEntry.builder()
.name("Maria Silva")
.email("maria@example.invalid")
.cpf("123.456.789-09")
.build()
))
.message("Por favor, assine este documento")
.build()
);O método bloqueia por padrão e tem efeitos colaterais visíveis de fora — cria signatários e dispara notificações. Se um passo depois do upload falhar, ele apaga o documento enviado e os signatários que ele mesmo criou; falhas na limpeza são anexadas à exceção original como suppressed. Quando o resultado da criação do assignment fica indeterminado (erro 5xx ou de rede), o SDK reconcilia pelos detalhes do documento antes de desfazer, e em último caso preserva os recursos em vez de apagar uma solicitação que pode ter sido despachada.
Use OAuth quando o seu produto é conectado pelos seus usuários e age no workspace deles, sem você nunca tocar na senha ou na chave de API dessas pessoas. Automatizando a sua própria conta? Continue com a chave de API; nada desta seção se aplica.
| Chave de API | OAuth | |
|---|---|---|
| Age em | O seu workspace | O workspace de outra pessoa, com a permissão dela |
| Pode fazer | Tudo que a sua conta pode | Só o que o usuário aprovou |
| O usuário pode desligar | Não | Sim, a qualquer momento |
Dois hosts, de propósito: a tela de consentimento vive no servidor de autorização
(https://auth.assinafy.com.br) e os endpoints de token, revogação e userinfo vivem nesta API. O SDK
lê as URLs dos documentos de metadados publicados, em vez de fixá-las no código.
No app da Assinafy, em Configurações → Aplicações OAuth → Nova aplicação. Você precisa ser owner do workspace e o plano precisa incluir aplicações OAuth.
| Campo | O que colocar |
|---|---|
| Nome | O que o usuário vê na tela de aprovação |
| Descrição | Uma frase sobre o que a aplicação faz com os documentos dele |
| URIs de redirecionamento | Onde o usuário volta depois de aprovar. Precisa ser https://, sem #, e é comparada caractere a caractere: …/callback e …/callback/ são diferentes. Cadastre uma por ambiente |
| Permissões | O máximo que a aplicação vai pedir algum dia. Dá para pedir menos na hora de conectar, nunca mais |
| Tipo | Confidencial se o código roda num servidor seu; Público se roda no dispositivo do usuário e não consegue guardar segredo. Não muda depois |
O client_secret é mostrado uma única vez. Aplicações públicas não recebem segredo nenhum e se
autenticam só por PKCE.
O enum OAuthScope cobre a lista publicada:
| Escopo | Permite |
|---|---|
DOCUMENTS_READ |
Ler documentos, páginas, tags, signatários, assignments e atividades |
DOCUMENTS_WRITE |
Criar, alterar e apagar documentos e enviá-los para assinatura |
TEMPLATES_READ |
Ler templates, suas páginas, papéis, campos e tags |
TEMPLATES_WRITE |
Criar, alterar e apagar templates |
ACCOUNT_READ |
Ler perfil, tema e logo do workspace |
WEBHOOKS_WRITE |
Configurar e desativar a assinatura de webhooks do workspace |
OPENID |
Receber um id_token identificando o usuário e habilitar o userinfo |
PROFILE |
Ler o nome do usuário |
EMAIL |
Ler o e-mail do usuário e se ele é verificado |
OFFLINE_ACCESS |
Receber um refresh token, para continuar funcionando com o usuário ausente |
Peça o mínimo: o usuário aprova tudo o que você pediu, ou nada. DOCUMENTS_WRITE gasta créditos de
notificação do workspace, porque enviar para assinatura notifica signatários. Cobrança, assinatura do
plano, quadro de membros, credenciais e administração nunca ficam acessíveis a um token OAuth,
qualquer que seja o escopo.
import com.assinafy.sdk.models.OAuthAuthorizationRequest;
import com.assinafy.sdk.models.OAuthTokens;
import com.assinafy.sdk.models.enums.OAuthScope;
import com.assinafy.sdk.request.AuthorizationUrlRequest;
import com.assinafy.sdk.request.OAuthClient;
// Um cliente sem credenciais basta para todo o fluxo OAuth.
AssinafyClient client = new AssinafyClient(new AssinafyClientOptions());
OAuthClient app = OAuthClient.confidential(
System.getenv("ASSINAFY_CLIENT_ID"),
System.getenv("ASSINAFY_CLIENT_SECRET")
);
// Aplicação pública: OAuthClient.publicClient(System.getenv("ASSINAFY_CLIENT_ID"))
// 1. Antes de redirecionar o navegador
OAuthAuthorizationRequest pedido = client.oauth().createAuthorizationUrl(
AuthorizationUrlRequest.builder()
.clientId(app.clientId())
.redirectUri("https://meuapp.com/oauth/callback")
.scopes(OAuthScope.DOCUMENTS_READ, OAuthScope.DOCUMENTS_WRITE, OAuthScope.OFFLINE_ACCESS)
.build()
);
session.setAttribute("assinafy.oauth", pedido); // guarda state + codeVerifier + issuer
response.sendRedirect(pedido.url()); // navegação de página inteira, não AJAXO SDK gera o par PKCE (S256, obrigatório inclusive para aplicações confidenciais), o state e —
quando você pede OPENID — o nonce. Gere um novo a cada tentativa de conexão: reaproveitar um
verifier ou um state anula o PKCE e a proteção contra CSRF.
// 2 e 3. No seu redirect URI
OAuthAuthorizationRequest guardado =
(OAuthAuthorizationRequest) session.getAttribute("assinafy.oauth");
String code = client.oauth().readAuthorizationCallback(request.getQueryString(), guardado);
// 4. Troca o código por tokens — no servidor. O código vale 60 segundos e é de uso único.
OAuthTokens tokens = client.oauth().exchangeCode(
app, code, guardado.codeVerifier(), "https://meuapp.com/oauth/callback");readAuthorizationCallback confere, antes de qualquer outra coisa, que o state é o seu (em tempo constante) e que o iss é exatamente o emissor armazenado. Parâmetros repetidos e codificação percentual
inválida são rejeitados. Só então confere se o servidor reportou erro. Um
consentimento recusado chega como ?error=access_denied e vira OAuthException, não uma falha de
HTTP. Um iss ausente é tratado como um iss errado, porque o servidor sempre o envia (RFC 9207).
// 5. O token vale para exatamente um workspace — descubra qual
AssinafyClient conectado = new AssinafyClient(
AssinafyClientOptions.builder().token(tokens.getAccessToken()).build());
String accountId = conectado.workspaces().list().getData().get(0).getId();Guarde o accountId junto dos tokens e leia tokens.getScope() em vez de supor que tudo o que você
pediu foi concedido.
// Access token dura 1 hora. Com OFFLINE_ACCESS, renove sem o usuário:
OAuthTokens novos = client.oauth().refreshToken(app, conexao.getRefreshToken());
conexao.salvar(novos.getRefreshToken()); // ANTES de usar qualquer outra coisa da resposta
// Daqui em diante, chame a API com o access token renovado
conectado = new AssinafyClient(
AssinafyClientOptions.builder().token(novos.getAccessToken()).build());
// Quem aprovou? (exige OPENID; nome exige PROFILE e e-mail exige EMAIL)
OAuthUserInfo quem = conectado.oauth().userInfo();
// Ao desconectar, revogue em vez de só apagar a sua cópia — e revogue o refresh token salvo por
// último, nunca uma cópia antiga: cada renovação aposentou o anterior
client.oauth().revokeToken(app, conexao.getRefreshToken(), "refresh_token");Refresh tokens rodam. Cada renovação devolve um novo e aposenta o anterior. Um refresh token reapresentado não pode ser distinguido de um roubado sendo replicado, então ele encerra a conexão inteira e o usuário precisa conectar de novo. Portanto: salve o novo refresh token antes de fazer qualquer outra coisa com a resposta, e nunca rode duas renovações ao mesmo tempo na mesma conexão.
refreshTokensó retorna quando a resposta traz um refresh token novo; caso contrário, lançaValidationExceptione o usuário precisa conectar de novo.Nunca reenvie um refresh token depois de uma falha que pode ter chegado ao servidor — timeout, conexão caída,
5xx: a primeira tentativa pode já tê-lo aposentado. Releia o token salvo; se ainda for o que você enviou, peça ao usuário para conectar de novo. Só é seguro repetir uma falha que comprovadamente aconteceu antes do envio: umaNetworkExceptioncausada porUnknownHostException(DNS),ConnectException(conexão recusada) ouSSLHandshakeException. O próprio SDK nunca reenvia uma chamada ao endpoint de token: timeout ou conexão caída chega comoNetworkException, e um503comoApiException.
Duas verdades por trás da maioria dos bugs de integração: um token vale para um único workspace
(qualquer outro devolve 403, mesmo um do mesmo usuário — conecte cada workspace separadamente), e
um refresh token vale 30 dias: cada renovação devolve um novo, válido por mais 30 dias, então a
conexão só expira depois de 30 dias sem renovação — e aí o usuário precisa conectar de novo.
tokens.getIdToken() volta exatamente como chegou. Valide-o com uma biblioteca OpenID Connect antes
de confiar nele — chave RS256 do jwks_uri pelo kid, iss, aud igual ao seu client_id, exp
e nonce igual a guardado.nonce() — ou leia os dados do usuário em userInfo().
As URLs vêm dos metadados publicados, e o SDK as lê sozinho. Se você quiser inspecioná-las:
OAuthProtectedResourceMetadata recurso = client.oauth().protectedResourceMetadata();
OAuthAuthorizationServerMetadata servidor = client.oauth().authorizationServerMetadata();O SDK valida que o issuer do documento coincide com o host de onde ele foi buscado (RFC 8414 §3.3):
um documento que discorda não é autoritativo e é rejeitado. Para pular a descoberta — e a chamada de
rede que ela custa — informe authorizationEndpoint e issuer no
AuthorizationUrlRequest.
As chamadas aos endpoints de token e revogação nunca carregam o seu X-Api-Key nem o seu bearer:
elas autenticam a aplicação pelo client_id/client_secret, e mandar uma credencial de workspace
para uma rota que não tem uso para ela seria vazá-la.
OAuthException (subtipo de ApiException) expõe o código legível por máquina em getError() e a
explicação em getErrorDescription().
| Código | Causa habitual |
|---|---|
access_denied |
O usuário recusou |
invalid_grant |
Código expirado ou já usado, code_verifier ou redirect_uri errado; refresh token já usado, ou o usuário reconectou com outras permissões |
invalid_client |
client_id ou segredo errado, ou aplicação desativada |
invalid_scope |
Escopo para o qual a aplicação não está registrada |
invalid_target |
resource diferente do que foi autorizado |
unsupported_grant_type |
Só existem authorization_code e refresh_token |
Nas chamadas comuns da API com um token OAuth: 401 significa token expirado, revogado ou não
enviado como Bearer — renove e, se não der, peça para reconectar. Um 403 com
WWW-Authenticate: Bearer error="insufficient_scope" nomeia a permissão que falta; trate como um
convite a reconectar pedindo aquele escopo, não como algo a repetir. Um 403 sem esse header tem
outra causa: outro workspace, o papel do usuário, ou uma área que tokens OAuth nunca alcançam.
- Um verifier PKCE e um
statenovos a cada tentativa de conexão stateeissconferidos no redirect URI — o SDK faz isso emreadAuthorizationCallbackclient_secretsó no servidor, nunca em app mobile, código de navegador ou repositório- O novo refresh token salvo antes de usar, e uma renovação por vez por conexão
401tratado: renovar e, falhando, pedir para reconectar- O ID do workspace guardado por conexão, e o
scopedevolvido realmente lido - Toda URI de redirecionamento de produção cadastrada,
https://e exata - Só as permissões necessárias
- Tokens revogados quando o usuário desconecta
Templates são documentos reutilizáveis com papéis e campos posicionados.
documents().createFromTemplate(templateId, request) gera um documento a partir de um template, e
documents().estimateCostFromTemplateTyped(...) prevê o custo antes. As mesmas regras de acoplamento
verificação/notificação e de ordem de assinatura valem aqui.
Definições de campo (fields()) são tipos de campo reutilizáveis do workspace — CPF, data, texto
livre. fields().validate(fieldId, valor, signerAccessCode) e
fields().validateMultiple(entradas, signerAccessCode) conferem valores contra a definição antes de
você enviá-los. fields().listTypes() lista os tipos disponíveis.
Tags (tags()) são rótulos do workspace. Documentos recebem tags por nome: a plataforma
vincula uma tag existente pelo nome e cria uma nova quando o nome não existe.
client.documents().appendTags(document.getId(), List.of("contratos", "2026"));
client.documents().replaceTags(document.getId(), List.of("arquivado"));
client.documents().detachTag(document.getId(), tagId); // desanexar usa o IDappendTagIds e replaceTagIds existem para quem tem os IDs em mãos: o SDK resolve os nomes
correspondentes antes de enviar. Apagar uma tag ainda vinculada a um documento devolve 409 a menos
que você force.
As rotas voltadas ao signatário usam o código de acesso do signatário, passado a cada chamada e não configurado no cliente. São elas que um portal de assinatura próprio consome:
Signer eu = client.signers().getSelf(codigo);
client.signers().acceptTerms(codigo);
client.signers().confirmSignerData(documentId, codigo, Map.of("government_id", "12345678909"));
client.signers().verifyEmail(codigo, "123456"); // OTP de e-mail ou WhatsApp
client.signers().uploadSignature(codigo, "signature", pngBytes);
Map<String, Object> paraAssinar = client.assignments().getForSigner(codigo);
client.assignments().sign(documentId, assignmentId, codigo, itens);
client.assignments().decline(documentId, assignmentId, codigo, "Valores divergentes");Também há listagem, busca e download dos documentos do signatário, além de assinar ou recusar vários
de uma vez (signMultiple, declineMultiple).
client.webhooks().register(RegisterWebhookRequest.builder()
.url("https://meuapp.com/webhooks/assinafy")
.email("operacoes@example.invalid")
.events(List.of("document_ready", "signer_signed_document", "document_processing_failed"))
.build());
WebhookSubscription atual = client.webhooks().get();
List<WebhookEventTypeInfo> tipos = client.webhooks().listEventTypes();
PaginatedResult<WebhookDispatch> entregas = client.webhooks().listDispatches();
client.webhooks().retryDispatch(dispatchId);
client.webhooks().inactivate(); // para a entrega sem apagar a inscriçãoHá uma inscrição por workspace: registrar de novo substitui a anterior.
O endpoint recebe um POST JSON, responde 2xx rapidamente e processa o evento em segundo plano.
Guarde o id do evento para evitar processamento duplicado e confirme o account_id da conexão.
subject e object são objetos polimórficos; object contém a entidade com seus relacionamentos.
O evento document_ready informa que a última pessoa assinou, enquanto a certificação final ainda
pode estar em processamento. Consulte documents().details(...) antes de baixar o arquivo final.
O contrato da API não define uma assinatura HMAC de webhook; não invente um header de autenticação.
PaginatedResult<Workspace> meus = client.workspaces().list();
Workspace w = client.workspaces().get(accountId);
AccountTheme tema = client.workspaces().getTheme(accountId);
byte[] logo = client.workspaces().downloadLogo(accountId);
List<DocumentStatsRow> kpis = client.workspaces().stats(accountId, "monthly", null);
AuthUser eu = client.users().get();
NotificationPreferences prefs = client.users().getNotificationPreferences();
ApiKey nova = client.apiKeys().create("sua-senha"); // devolve a chave inteira uma única vezworkspaces().delete(accountId, true) cancela uma assinatura paga ativa e apaga o workspace — sem o
force, o servidor responde 400 listando os bloqueios.
Todas as falhas do SDK são RuntimeException, sob uma raiz comum:
AssinafyException
├── ValidationException entrada do chamador inválida, detectada antes de enviar
├── NetworkException a requisição não chegou a completar
└── ApiException a API respondeu com erro (statusCode, responseData, headers)
├── AuthenticationException 401 e 403
├── RateLimitException 429
└── OAuthException corpo de erro RFC 6749 (getError, getErrorDescription)
try {
client.assignments().create(documentId, pedido);
} catch (RateLimitException e) {
// recuar e repetir
} catch (AuthenticationException e) {
// credencial ausente, expirada ou sem permissão
} catch (ApiException e) {
log.error("A API respondeu {}: {}", e.getStatusCode(), e.getMessage());
} catch (ValidationException e) {
log.error("Requisição inválida: {}", e.getErrors());
}As listagens devolvem PaginatedResult<T>, com getData() e getMeta() (página atual, itens por
página, total e número de páginas, lidos dos headers X-Pagination-*).
ListParams params = ListParams.builder()
.page(1)
.perPage(50) // máximo 100
.search("contrato")
.sort("-created_at")
.build();
PaginatedResult<Document> pagina = client.documents().list(params);page e perPage são validados no cliente antes do envio.
| Produção | https://api.assinafy.com.br/v1 |
| Sandbox | AssinafyClientOptions.SANDBOX_BASE_URL |
O sandbox permite testar a integração sem usar documentos de produção. As funcionalidades
disponíveis dependem do plano do workspace: WhatsApp e Certificado Digital podem retornar 403
quando não estão habilitados. As notificações WhatsApp são simuladas no sandbox; consulte
assignments().getWhatsappNotificationsTyped(...) para obter os botões e códigos do fluxo de teste.
Uma assinatura A1/A3 completa continua exigindo um certificado ICP-Brasil válido e o Web PKI.
# Testes unitários, sem chamadas à API
./mvnw test
# Build completo: testes, Javadoc, jar, sources e javadoc jars
./mvnw verify
# Testes de integração contra a API real (exige credenciais no ambiente)
./mvnw -Plive-api verifyO build é estrito: -Xlint:all -Werror no compilador e doclint:all com failOnWarnings no
Javadoc, então um aviso quebra o verify.
O SDK não adiciona um loop de retentativas da aplicação. O transporte OkHttp padrão pode repetir
falhas de conexão e certas respostas 408/503. Token e revogação OAuth usam um transporte separado
sem repetição. Após um resultado indeterminado de criação, consulte o estado remoto antes de criar
novamente.
O perfil ao vivo aceita somente a URL exata do sandbox. ASSINAFY_API_KEY e
ASSINAFY_ACCOUNT_ID vêm do ambiente ou de um cofre de segredos. Os casos com convites usam
ASSINAFY_TEST_EMAIL_PRIMARY e ASSINAFY_TEST_EMAIL_SECONDARY; o reset de senha usa
ASSINAFY_TEST_USER_EMAIL, que precisa ser um usuário cadastrado, não apenas um signatário.
Destinatários .invalid permitem testar a criação sem entregar e-mail, mas não confirmam o OTP.
Os testes criam registros temporários e os removem; confira a conta após uma execução interrompida.
WhatsApp e Certificado Digital exigem os recursos habilitados no plano. Os certificados A1/A3 e
Web PKI são reais também no sandbox; um 403 de plano não valida a conclusão da assinatura.
- README.en.md — referência completa por operação, em inglês
- docs/API_REFERENCE.md — contrato de requisição/resposta rota por rota
- Documentação da API
Distribuído sob a licença MIT.