Português · Read in English
Cliente Rust assíncrono e idiomático para a API de assinatura eletrônica da Assinafy — plataforma brasileira de assinatura de documentos.
Cobre toda a superfície REST pública documentada em https://api.assinafy.com.br/v1/docs: contas, signatários, documentos, assignments, templates, tags, campos, webhooks, atividades, chaves de API, OAuth 2.1 e os endpoints voltados ao signatário.
- Instalação
- Início rápido
- Como o SDK é organizado
- Autenticação
- Ambientes
- Fluxo completo do documento
- Métodos de verificação do signatário
- Certificado digital ICP-Brasil (A1/A3)
- Fluxo do signatário
- Templates
- Tags
- Campos personalizados
- Webhooks
- Atividades e artefatos
- Paginação
- Erros e limites de taxa
- Índice de operações
- Features do Cargo
- Testes de integração
- Licença
Requer Rust 1.86 ou superior e usa a edição Rust 2024. A CI testa tanto o mínimo declarado (1.86) quanto o stable atual — Rust não tem canal LTS. O cliente HTTP padrão exige TLS 1.2 ou superior.
[dependencies]
assinafy = "3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }use assinafy::Client;
#[tokio::main]
async fn main() -> assinafy::Result<()> {
let client = Client::builder()
.api_key(std::env::var("ASSINAFY_API_KEY").unwrap())
.build()?;
let signatarios = client
.signers("acc_1234567890abcdef12345678")
.list()
.per_page(50)
.send()
.await?;
for s in &signatarios.data {
println!("{} <{:?}>", s.full_name, s.email);
}
Ok(())
}Um único Client expõe um módulo por área da API. O handle é barato de criar a cada chamada e
empresta do cliente; o próprio Client é Clone (estado interno com contagem de referências),
então construa um na inicialização e reutilize-o.
| Área | Acessor |
|---|---|
| Autenticação (login, senha, social) | Client::auth_api |
| OAuth 2.1 / OpenID Connect | Client::oauth |
| Contas | Client::accounts_api / Client::account |
| Chaves de API | Client::api_keys |
| Usuário autenticado | Client::users |
| Signatários | Client::signers |
| Signatário (self) | Client::signer_self |
| Documentos | Client::documents |
| Assignments | Client::assignments |
| Tags | Client::tags |
| Campos | Client::fields |
| Templates | Client::templates |
| Webhooks | Client::webhooks |
| Atividades | Client::activities |
| Endpoints públicos | Client::public |
Toda chamada é async e devolve Result<T, Error>. As respostas usam o mesmo envelope da
API ({ status, message, data }), do qual o SDK entrega apenas o data já tipado — exceto os
endpoints OAuth, que por especificação respondem com um objeto plano.
O SDK aceita os quatro esquemas de credencial da API. Troque a credencial em tempo de execução
com Client::with_auth.
Enviada no cabeçalho X-Api-Key. É o padrão para integrações servidor-a-servidor.
use assinafy::Client;
fn criar_cliente() -> assinafy::Result<Client> {
Client::builder().api_key("chave-de-api").build()
}Obtido por login e enviado como Authorization: Bearer <token>.
use assinafy::{Auth, Client};
use assinafy::resources::LoginBody;
async fn entrar() -> assinafy::Result<Client> {
let anonimo = Client::builder().build()?;
let sessao = anonimo
.auth_api()
.login(&LoginBody::new("usuario@example.invalid", "senha"))
.await?;
// `LoginResult` também traz o usuário e as contas que ele enxerga.
Ok(anonimo.with_auth(Auth::Bearer(sessao.access_token)))
}Há ainda a forma legada por query string (?access-token=...), disponível via
ClientBuilder::access_token, para integrações que a exijam.
Use OAuth quando a aplicação age no workspace de um usuário, com a permissão dele — ao contrário da chave de API e do token de usuário, que autenticam o workspace ou o usuário diretamente. O token resultante carrega apenas os escopos aprovados, vale para um workspace e nunca alcança faturamento, ciclo de vida da conta, gestão de credenciais ou superfícies administrativas, quaisquer que sejam seus escopos.
O fluxo é o authorization code com PKCE obrigatório (S256). O SDK cobre todas as etapas, exceto o redirecionamento do navegador:
1. descoberta GET /.well-known/oauth-protected-resource
GET {issuer}/.well-known/oauth-authorization-server
2. autorização navegador → {authorization_endpoint}?code_challenge=...
3. consentimento usuário aprova → redirect_uri?code=...&state=...&iss=...
4. troca POST /v1/oauth/token (code + code_verifier)
5. uso Authorization: Bearer <access_token>
POST /v1/oauth/token (refresh_token)
POST /v1/oauth/revoke
Gere um par PKCE e um state novos a cada tentativa. No retorno ao redirect_uri, antes de
qualquer outra coisa — inclusive quando vier error=... —, confira se state é o valor guardado
e se iss é https://auth.assinafy.com.br; se algum diferir, pare: a resposta não é sua.
use assinafy::resources::{AuthorizationRequest, PkceChallenge, TokenRequest, scope};
use assinafy::{Auth, Client};
async fn autorizar(client_id: &str, redirect_uri: &str) -> assinafy::Result<()> {
let client = Client::builder().build()?;
// 1. Descoberta: a API nomeia seu servidor de autorização, que por sua vez
// publica o endpoint de autorização usado pelo navegador.
let recurso = client.oauth().protected_resource_metadata().await?;
let servidor = client
.oauth()
.authorization_server_metadata(&recurso.authorization_servers[0])
.await?;
// 2. Par PKCE e `state` novos a cada tentativa; guarde os dois até o callback.
let pkce = PkceChallenge::generate()?;
let url = AuthorizationRequest::new(client_id, redirect_uri, &pkce)
.scopes([scope::DOCUMENTS_READ, scope::DOCUMENTS_WRITE, scope::OPENID])
.state("valor-aleatório-por-tentativa")
.resource(&recurso.resource)
.url(&servidor.authorization_endpoint)?;
println!("abra {url}");
// 4. Conferidos `state` e `iss`, troque o `code` que voltou no redirecionamento,
// repetindo o `resource` enviado no passo 2.
let token = client
.oauth()
.token(
&TokenRequest::authorization_code(
client_id,
"code-do-redirecionamento",
redirect_uri,
&pkce,
)
.resource(&recurso.resource),
)
.await?;
// 5. Passe a agir pelo usuário. O token alcança um único workspace, o único que
// `GET /v1/accounts` devolve: guarde o id dele junto dos tokens.
let como_usuario = client.with_auth(Auth::Bearer(token.access_token.clone()));
let workspace = &como_usuario.accounts_api().list().await?[0].id;
let quem = como_usuario.oauth().userinfo().await?;
println!("agindo por {} no workspace {workspace}", quem.sub);
Ok(())
}Escopos disponíveis (constantes em assinafy::resources::scope):
| Escopo | Concede |
|---|---|
documents:read |
Ler documentos, páginas, tags, signatários, assignments e atividades |
documents:write |
Criar, atualizar e excluir documentos e gerir seus signatários e assignments |
templates:read |
Ler templates, páginas, papéis, campos e tags |
templates:write |
Criar, atualizar e excluir templates e seus componentes |
account:read |
Ler perfil, tema e logo do workspace |
webhooks:write |
Configurar e desativar a assinatura de webhooks do workspace |
openid |
Identificar o usuário autenticado e habilitar /oauth/userinfo |
profile |
Incluir o nome do usuário no id_token/userinfo |
email |
Incluir o e-mail e sua verificação no id_token/userinfo |
offline_access |
Emitir refresh token — só para clientes que pedem explicitamente |
Peça sempre o conjunto mínimo. Um escopo ausente responde 403 com
WWW-Authenticate: Bearer error="insufficient_scope" nomeando o que falta; o SDK entrega esses
escopos em Error::insufficient_scope. Reconecte pedindo-os — repetir a chamada não resolve. Um
403 sem esse desafio indica outro workspace, o papel do usuário ou uma área que tokens OAuth
nunca alcançam.
Renovação: o access token dura 1 hora e, com offline_access, é renovado sem o usuário. Cada
renovação devolve um refresh token novo e aposenta o anterior, e reutilizar um refresh token
aposentado encerra a conexão inteira. Por isso, salve o novo refresh token antes de qualquer outra
coisa e faça uma renovação por vez em cada conexão. O SDK envia cada pedido de token uma única
vez — nunca o repete nem segue redirecionamentos — e rejeita uma renovação bem-sucedida que não
traga um refresh token novo.
Quando uma renovação falha sem resposta definitiva — timeout, conexão interrompida, 5xx, sucesso
sem refresh token novo —, suponha que o token enviado foi aposentado. Releia o token guardado: se
outro worker salvou um diferente, siga com ele; se continua o mesmo, nunca o envie de novo —
marque a conexão como desconectada e peça ao usuário para conectar de novo. Só as falhas que
comprovadamente ocorreram antes do envio — resolução de DNS, conexão recusada, handshake TLS: um
Error::Http cujo is_connect() é verdadeiro — deixam o token intacto e podem ser repetidas.
Um refresh token vale 30 dias, e cada renovação devolve um novo com mais 30 dias: a conexão só expira se passar 30 dias sem renovação.
As falhas do endpoint de token seguem o objeto plano { error, error_description } da RFC 6749
§5.2. O SDK preserva ambos: a descrição vira a mensagem do erro e o código fica acessível em
ApiError::oauth_error.
use assinafy::Client;
use assinafy::models::TokenResponse;
use assinafy::resources::TokenRequest;
/// Renova o access token. `salvar` grava de forma durável o refresh token novo da
/// resposta; se falhar, o erro é devolvido e o access token não é usado.
async fn renovar(
client: &Client,
client_id: &str,
refresh_token: &str,
salvar: impl FnOnce(&TokenResponse) -> std::io::Result<()>,
) -> assinafy::Result<Option<String>> {
match client
.oauth()
.token(&TokenRequest::refresh_token(client_id, refresh_token))
.await
{
Ok(novo) => {
// O refresh token enviado acaba de ser aposentado, e uma renovação bem-sucedida
// sempre traz um novo: salve-o antes de usar o access token.
salvar(&novo)?;
Ok(Some(novo.access_token))
}
// A conexão acabou (refresh token já usado, 30 dias sem renovação, revogado ou
// nova aprovação com outras permissões): marque-a como desconectada e peça ao
// usuário para conectar de novo, sem repetir a renovação.
Err(e) if e.oauth_error() == Some("invalid_grant") => Ok(None),
// Qualquer outra falha pode ter aposentado o token enviado: a menos que
// `is_connect()` mostre que o pedido nem saiu, releia o token guardado e nunca o
// reenvie inalterado.
Err(outro) => Err(outro),
}
}Em um 401 da API, renove uma vez; se a renovação falhar, peça ao usuário para conectar de novo.
Para desconectar, revogue o refresh token atual e só então apague os tokens guardados:
use assinafy::Client;
use assinafy::resources::RevokeRequest;
async fn desconectar(
client: &Client,
client_id: &str,
refresh_token: &str,
) -> assinafy::Result<()> {
// A revogação responde 200 para qualquer resultado — inclusive token
// inexistente —, então nunca serve para sondar se um token existe.
client
.oauth()
.revoke(&RevokeRequest::refresh_token(client_id, refresh_token))
.await
}Os endpoints OAuth existem somente em produção. Outras implantações respondem
404 Página não encontrada.e não publicam/.well-known/oauth-protected-resource.
Os endpoints voltados ao signatário usam o código de acesso entregue fora de banda (e-mail ou
WhatsApp), enviado como ?signer-access-code=....
use assinafy::{Auth, Client};
fn como_signatario(client: &Client) -> Client {
client.with_auth(Auth::AccessCode("codigo-do-signatario".into()))
}| Ambiente | Base URL | Builder |
|---|---|---|
| Produção | https://api.assinafy.com.br/v1 |
padrão, ou .production() |
| Outra implantação | qualquer URL HTTPS | .base_url(BaseUrl::custom(...)?) |
BaseUrl::custom aponta para qualquer outro deployment. URLs personalizadas exigem HTTPS
(HTTP só para loopback), sem credenciais embutidas, query string ou fragmento.
use assinafy::{BaseUrl, Client};
fn cliente_local() -> assinafy::Result<Client> {
Client::builder()
.base_url(BaseUrl::custom("http://127.0.0.1:8080/v1")?)
.api_key("chave-de-teste")
.build()
}O caminho ponta a ponta de uma assinatura, do upload ao PDF certificado:
1. criar signatário POST /accounts/{conta}/signers
2. enviar o PDF POST /accounts/{conta}/documents → status: uploaded
3. aguardar metadados GET /documents/{id} → status: metadata_ready
4. estimar o custo POST /documents/{id}/assignments/estimate-cost
5. pedir assinaturas POST /documents/{id}/assignments → status: pending_signature
(a API notifica cada signatário e devolve signing_urls)
6. o signatário assina confirma dados → verifica OTP → assina os campos
7. acompanhar GET /documents/{id} → status: certificated
ou webhook DocumentCompleted
8. baixar o resultado GET /documents/{id}/download/certificated
use assinafy::Client;
use assinafy::models::{ArtifactName, AssignmentMethod, DocumentStatus};
use assinafy::resources::{CreateAssignmentBody, CreateSignerBody, UploadDocumentRequest};
async fn assinar_contrato() -> assinafy::Result<()> {
let conta = std::env::var("ASSINAFY_ACCOUNT_ID").unwrap();
let client = Client::builder()
.api_key(std::env::var("ASSINAFY_API_KEY").unwrap())
.build()?;
// 1. O signatário precisa de e-mail ou WhatsApp para ser notificado.
let signatario = client
.signers(&conta)
.create(&CreateSignerBody::new("Maria Silva").email("maria@example.invalid"))
.await?;
// 2. O upload é multipart; o documento nasce em `uploaded` e a extração de
// metadados ocorre de forma assíncrona.
let arquivo = UploadDocumentRequest::from_path("./contrato.pdf").await?;
let documento = client.documents().upload(&conta, arquivo).await?;
// 3. Só é possível pedir assinaturas a partir de `metadata_ready`.
let pronto = client.documents().get(&documento.id).await?;
if !matches!(pronto.status, DocumentStatus::MetadataReady) {
println!("ainda processando: {}", pronto.status);
return Ok(());
}
// 4–5. Peça as assinaturas. `Virtual` notifica remotamente; `Collect`
// coleta presencialmente no dispositivo do remetente.
let pedido = CreateAssignmentBody::new(
AssignmentMethod::Virtual,
[signatario.id.as_str()],
)
.message("Segue o contrato para assinatura.");
let custo = client
.assignments()
.estimate_cost(&documento.id, &pedido.clone().into())
.await?;
println!("custo estimado: {} créditos", custo.total_credits);
let assignment = client.assignments().create(&documento.id, &pedido).await?;
// Os links de assinatura dão acesso ao documento: entregue-os por canal
// privado e nunca os registre em log.
for link in &assignment.signing_urls {
println!("link emitido para o signatário {}", link.signer_id);
}
// 7–8. Depois que todos assinarem, o documento fica `certificated`.
let atual = client.documents().get(&documento.id).await?;
if matches!(atual.status, DocumentStatus::Certificated) {
let (pdf, tipo) = client
.documents()
.download_artifact(&documento.id, ArtifactName::Certificated)
.await?;
assert_eq!(tipo, "application/pdf");
tokio::fs::write("./contrato-certificado.pdf", pdf).await?;
}
Ok(())
}Estados do documento, em ordem de ciclo de vida:
| Status | Significado |
|---|---|
uploading / uploaded |
Upload em andamento / concluído |
metadata_processing / metadata_ready |
Extração de metadados / pronto para assignment |
pending_signature |
Aguardando um ou mais signatários |
certificating / certificated |
Gerando o certificado / assinado e certificado |
rejected_by_signer / rejected_by_user |
Recusado pelo signatário / cancelado pelo remetente |
expired |
Prazo de assinatura vencido |
failed |
Processamento falhou |
Valores futuros que o SDK ainda não modela chegam como DocumentStatus::Unknown(String) em vez
de falharem na desserialização.
Definidos por signatário ao criar o assignment. O método de verificação e o de notificação são
acoplados: envie um, os dois ou nenhum — o lado que faltar é inferido. Sem nenhum dos dois,
ambos assumem Email.
| Método | Como funciona | Custo por signatário |
|---|---|---|
Email (padrão) |
Código de uso único (OTP) por e-mail, exigido antes de assinar | Gratuito |
Whatsapp |
Código de uso único (OTP) por WhatsApp | Verificação gratuita; notificação 0,45 crédito, só em planos pagos |
DigitalCertificate |
O signatário assina com o próprio certificado ICP-Brasil (A1/A3), pela extensão de navegador Web PKI, gerando uma assinatura PAdES qualificada | 2 créditos, somados ao custo da notificação |
Combinações permitidas: Email → notifica por Email; Whatsapp → notifica por Whatsapp;
DigitalCertificate → notifica por Email ou Whatsapp. Apenas um método de notificação
por signatário.
use assinafy::Client;
use assinafy::models::{AssignmentMethod, NotificationMethod, VerificationMethod};
use assinafy::resources::{CreateAssignmentBody, CreateAssignmentSigner};
async fn pedir_com_metodos(documento: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?;
let pedido = CreateAssignmentBody::from_signers(
AssignmentMethod::Virtual,
[
// OTP por e-mail (padrão explícito), assina primeiro.
CreateAssignmentSigner::new("sig_email")
.step(1)
.verification_method(VerificationMethod::Email),
// OTP por WhatsApp: verificação e notificação viajam juntas.
CreateAssignmentSigner::new("sig_whatsapp")
.step(2)
.verification_method(VerificationMethod::Whatsapp)
.notification_methods(vec![NotificationMethod::Whatsapp]),
// Certificado ICP-Brasil, notificado por e-mail.
CreateAssignmentSigner::new("sig_certificado")
.step(3)
.verification_method(VerificationMethod::DigitalCertificate)
.notification_methods(vec![NotificationMethod::Email]),
],
)
.expires_at("2026-12-31T23:59:59Z");
client.assignments().create(documento, &pedido).await?;
Ok(())
}O step define assinatura sequencial: o signatário do passo 2 só é notificado quando o passo 1
termina. Sem step, todos assinam em paralelo.
Estime antes de enviar — a estimativa informa saldo, custo total e o motivo de bloqueio quando os créditos não bastam:
use assinafy::Client;
use assinafy::models::AssignmentMethod;
use assinafy::resources::EstimateAssignmentCostBody;
async fn estimar(documento: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?;
let estimativa = client
.assignments()
.estimate_cost(
documento,
&EstimateAssignmentCostBody::new(AssignmentMethod::Virtual, ["sig_1", "sig_2"]),
)
.await?;
println!("total: {} créditos", estimativa.total_credits);
println!("saldo: {}", estimativa.credit_balance);
for item in &estimativa.breakdown {
println!(" {} × {} = {}", item.quantity, item.name, item.cost);
}
if let Some(motivo) = &estimativa.blocking_reason {
let detalhe = estimativa.message.as_deref().unwrap_or("");
println!("bloqueado: {motivo:?} — {detalhe}");
}
Ok(())
}Exige o recurso Certificado Digital na conta (planos Standard e Pro), CPF ou CNPJ em
government_id 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.
Antes de abrir o assignment, o signatário precisa confirmar os dados de identidade e aceitar os termos. O endpoint comum de assinatura rejeita signatários por certificado — a assinatura deles é produzida por um handshake de dois passos com a extensão Web PKI:
POST /v1/signers/certificate/start → data.token (token da operação Web PKI)
↓ o navegador assina o token com o certificado do signatário
POST /v1/signers/certificate/complete → data.signerName
Essas duas rotas são extensões implantadas somente em produção: outras implantações não as expõem e elas não constam do documento OpenAPI publicado, portanto o SDK não as envolve.
Concluído o fluxo, baixar o artefato pades devolve a assinatura PAdES qualificada.
Com Auth::AccessCode, o cliente passa a falar pelos endpoints do signatário. O código chega ao
signatário por e-mail ou WhatsApp e não pode ser obtido pela API.
use assinafy::models::{SignDocumentItem, SignerType};
use assinafy::resources::{ConfirmSignerDataBody, VerifyCodeBody};
use assinafy::{Auth, Client};
async fn assinar_como_signatario(documento: &str, assignment: &str) -> assinafy::Result<()> {
let client = Client::builder()
.auth(Auth::AccessCode("codigo-do-signatario".into()))
.build()?;
let signer = client.signer_self();
// 1. Quem sou eu e o que devo assinar.
let eu = signer.me().await?;
let doc = signer.signable_document().await?;
println!("{} vai assinar {}", eu.full_name, doc.name);
// 2. Aceitar os termos e confirmar os dados de identidade.
signer.accept_terms().await?;
signer
.confirm_data(
documento,
&ConfirmSignerDataBody::new()
.full_name("Maria Silva")
.government_id("123.456.789-09"),
)
.await?;
// 3. Validar o código de uso único recebido por e-mail ou WhatsApp.
signer.verify(&VerifyCodeBody::new("123456")).await?;
// 4. Enviar a rubrica e preencher os campos do assignment.
let png = tokio::fs::read("./assinatura.png").await?;
signer
.upload_signature(SignerType::Signature, "image/png", png)
.await?;
let itens: Vec<SignDocumentItem> = doc
.assignment
.iter()
.flat_map(|a| a.items.iter())
.filter_map(|item| {
let campo = item.field.as_ref()?;
let pagina = item.page.as_ref()?;
Some(SignDocumentItem::new(
&item.id,
&campo.id,
&pagina.id,
"Maria Silva",
))
})
.collect();
signer.sign(documento, assignment, itens).await?;
Ok(())
}Recusar é igualmente explícito, e o motivo fica registrado no documento:
use assinafy::{Auth, Client};
async fn recusar(documento: &str, assignment: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?
.with_auth(Auth::AccessCode("codigo-do-signatario".into()));
client
.signer_self()
.decline(documento, assignment, "Dados incorretos no contrato")
.await?;
Ok(())
}Um signatário com vários documentos pendentes pode assinar ou recusar em lote com
sign_multiple e decline_multiple.
Um template é um documento reutilizável com papéis e campos já posicionados. Criar um documento a partir dele dispensa upload e posicionamento.
use assinafy::Client;
use assinafy::models::VerificationMethod;
use assinafy::resources::{CreateDocumentFromTemplateBody, TemplateDocumentSigner};
async fn gerar_do_template(conta: &str, template: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?;
// Cada papel do template recebe um signatário concreto.
let corpo = CreateDocumentFromTemplateBody::default()
.name("Contrato — Maria Silva")
.message("Segue para assinatura.")
.signers(vec![
// Um signatário já cadastrado na conta…
TemplateDocumentSigner::existing("role_contratante", "sig_1")
.verification_method(VerificationMethod::Email),
// …ou um criado na hora, a partir do nome e do contato.
TemplateDocumentSigner::inline("role_contratado", "João Souza")
.whatsapp("+5511999999999")
.verification_method(VerificationMethod::Whatsapp),
]);
let estimativa = client.templates(conta).estimate_cost(template, &corpo).await?;
println!("custo: {} créditos", estimativa.total_credits);
let documento = client.templates(conta).create_document(template, &corpo).await?;
println!("documento {} criado e já em assinatura", documento.id);
Ok(())
}Listar, obter, criar (multipart), atualizar e excluir templates também estão disponíveis, assim como o download de cada página renderizada.
use assinafy::Client;
use assinafy::resources::CreateTagBody;
async fn etiquetar(conta: &str, documento: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?;
let tags = client.tags(conta);
let contratos = tags.create(&CreateTagBody::new("Contratos").color("3399ff")).await?;
println!("tag {} criada", contratos.id);
// Anexar e substituir recebem **nomes**; remover recebe o **id**.
tags.add_to_document(documento, ["Contratos", "Urgente"]).await?;
tags.set_on_document(documento, ["Assinado"]).await?;
tags.remove_from_document(documento, &contratos.id).await?;
Ok(())
}add_to_document e set_on_document fazem upsert por nome: a API compara sem diferenciar
maiúsculas com as tags existentes da conta e cria a que faltar. Passar um id de tag aqui cria uma
tag nova com aquele id como nome. remove_from_document é a única operação de tag de documento
que recebe um id real, como devolvido por create ou list_for_document.
Campos definem os dados que um signatário preenche. Cada conta tem os campos padrão da plataforma mais os que você criar; a validação por expressão regular acontece no servidor.
use assinafy::Client;
use assinafy::resources::{CreateFieldBody, ValidateFieldEntry};
async fn campos(conta: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?;
let fields = client.fields(conta);
for tipo in fields.list_types().await? {
println!("tipo disponível: {} ({})", tipo.name, tipo.kind);
}
// A regex precisa vir delimitada por barras — `^.{2,}$` sem barras é
// recusada com `400 Padrão RegEx inválido.`
let campo = fields
.create(
&CreateFieldBody::new("text", "Matrícula")
.regex("/^[0-9]{6}$/")
.required(true),
)
.await?;
let resultado = fields.validate(&campo.id, "123456").await?;
println!("válido: {}", resultado.success);
// Ou valide vários de uma vez.
let lote = fields
.validate_multiple([ValidateFieldEntry::new(&campo.id, "123456")])
.await?;
println!("{} resultados", lote.len());
Ok(())
}Uma assinatura de webhook por conta; a API entrega cada evento e guarda o histórico de entregas, que pode ser reenviado.
use assinafy::Client;
use assinafy::resources::RegisterWebhookBody;
async fn webhooks(conta: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?;
let hooks = client.webhooks(conta);
for tipo in hooks.event_types().await? {
println!("{}: {}", tipo.id, tipo.description);
}
hooks
.register(
&RegisterWebhookBody::new("https://app.example.invalid/hooks", "ops@example.invalid")
.events(["DocumentCompleted", "SignerDeclined"])
.active(true),
)
.await?;
// Histórico de entregas, com reenvio das que falharam.
let entregas = hooks.list_dispatches().delivered(false).per_page(50).send().await?;
for entrega in &entregas.data {
println!("{} → HTTP {:?}", entrega.event, entrega.http_status);
hooks.retry_dispatch(&entrega.id).await?;
}
// Desligar é um `inactivate`; não existe rota de exclusão.
hooks.inactivate().await?;
Ok(())
}Client::activities devolve todos os eventos registrados de um documento, cada um com um
snapshot do payload do evento e a origin da requisição (ip, user-agent).
use assinafy::Client;
async fn trilha(documento: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?;
let eventos = client.activities().list(documento).per_page(100).send().await?;
for evento in &eventos.data {
let ip = evento.origin.as_ref().and_then(|o| o.ip.as_deref()).unwrap_or("-");
let mensagem = evento.message.as_deref().unwrap_or("");
println!("{} — {mensagem} (de {ip})", evento.event);
}
Ok(())
}Artefatos disponíveis para download:
| 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 |
thumbnail não é um valor válido na rota download/{artefato}; o SDK redireciona
ArtifactName::Thumbnail para GET /documents/{id}/thumbnail de forma transparente.
A verificação pública confere um documento assinado pelo hash da assinatura, sem autenticação:
use assinafy::Client;
async fn verificar(hash: &str) -> assinafy::Result<()> {
let client = Client::builder().build()?;
let resultado = client.documents().verify(hash).await?;
println!("válido: {} — {}", resultado.is_valid, resultado.message);
Ok(())
}Todo endpoint paginado devolve Page<T> com data e meta, esta lida dos cabeçalhos
X-Pagination-*. meta também traz o estado do limite de taxa (X-Rate-Limit-*), útil para se
autorregular antes de levar um 429.
use assinafy::Client;
async fn percorrer(conta: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?;
let mut pagina = Some(1);
while let Some(n) = pagina {
let resultado = client.signers(conta).list().page(n).per_page(100).send().await?;
println!("página {n}: {} itens", resultado.data.len());
if let Some(restantes) = resultado.meta.rate_limit_remaining {
println!(" {restantes} requisições restantes na janela");
}
pagina = resultado.next_page();
}
Ok(())
}Toda operação devolve Result<T, Error>. Erros da API preservam o status HTTP, a mensagem do
servidor e o payload data bruto, então dá para ramificar por código sem perder informação.
use assinafy::{Client, Error};
async fn tratar(conta: &str) -> assinafy::Result<()> {
let client = Client::from_api_key("chave-de-api")?;
match client.signers(conta).get("inexistente").await {
Ok(signatario) => println!("{}", signatario.full_name),
Err(e) if e.is_rate_limited() => {
println!("aguarde {:?}s antes de tentar de novo", e.retry_after());
}
Err(Error::Api(e)) if e.status == 404 => println!("não encontrado: {}", e.message),
Err(Error::Api(e)) => println!("erro {}: {} — {:?}", e.status, e.message, e.data),
Err(outro) => return Err(outro),
}
Ok(())
}Variantes de Error: Config (configuração inválida), Http (transporte), Serde, Io,
Url, Api (resposta não-2xx) e UnexpectedResponse (payload indecifrável). Credenciais nunca
aparecem em Display/Debug — a URL é removida dos erros de transporte, e tokens em Debug
saem como **redacted**.
| Recurso | Operações |
|---|---|
| Autenticação | login, social_login, change_password, request_password_reset, reset_password, link_social_login, social_login_url |
| OAuth 2.1 | protected_resource_metadata, authorization_server_metadata, token, revoke, userinfo |
| Contas | list, create, get, update, delete, delete_forcing, theme, stats, download_logo, upload_logo, delete_logo |
| Chaves de API | create, get, delete |
| Usuários | me, stats, notification_preferences, update_notification_preferences |
| Signatários | create, list, get, update, delete |
| Documentos | statuses, list, upload, search, get, rename, delete, download_artifact, download_thumbnail, download_page, verify |
| Assignments | list, list_current, create, estimate_cost, reset_expiration, reset_expiration_at, resend_to_signer, estimate_resend_cost, whatsapp_notifications, sign, reject |
| Tags | list, create, update, delete, delete_with_force, list_for_document, add_to_document, set_on_document, remove_from_document |
| Campos | create, list, get, update, delete, validate, validate_multiple, list_types |
| Templates | list, get, create, update, delete, download_page, create_document, estimate_cost |
| Webhooks | register, get_subscription, inactivate, event_types, list_dispatches, retry_dispatch |
| Atividades | list |
| Públicos | document, send_token |
| Signatário (self) | me, accept_terms, verify, confirm_data, signable_document, signable_document_with_accepted_terms, current_document, list_documents, search_documents, sign, decline, sign_multiple, decline_multiple, download_document, upload_signature, upload_signature_with_reuse, download_signature |
Cada método traz no Rustdoc a rota HTTP que chama e os payloads de requisição e resposta.
Exemplos executáveis ficam em examples/.
rustls-tls(padrão) — TLS via rustls.native-tls— TLS pela pilha nativa do sistema operacional.
export ASSINAFY_BASE_URL=<url-https-da-implantacao>
export ASSINAFY_API_KEY=<chave>
export ASSINAFY_ACCOUNT_ID=<conta>
export ASSINAFY_TEST_EMAIL_PRIMARY=<caixa-de-teste>
export ASSINAFY_TEST_EMAIL_SECONDARY=<caixa-de-teste-secundária>
cargo test --test live -- --ignored --test-threads=1O --ignored é obrigatório porque esses testes chamam a API de verdade, e --test-threads=1
mantém consistente o estado compartilhado do workspace. As variáveis de e-mail são lidas apenas
em tempo de execução e cobrem a entrega de notificações; nada disso é compilado no SDK.
Os testes de descoberta OAuth dispensam credenciais — eles usam os endpoints públicos de produção:
cargo test --test live -- --ignored oauthDistribuído sob a licença MIT.