Português · Read in English
SDK oficial em Python para a API Assinafy — plataforma brasileira de assinatura eletrônica de documentos.
O SDK é síncrono, construído sobre httpx, e oferece os recursos da API Assinafy:
contas, usuários, autenticação, OAuth, documentos, signatários, documentos do signatário,
assignments, definições de campo, templates, tags e webhooks. Todo método público nomeia o verbo e o
caminho que chama e documenta o corpo da requisição e a resposta já desembrulhada; os formatos de
recurso compartilhados são documentados uma vez e referenciados pelos métodos que os devolvem.
- Requisitos
- Instalação
- Autenticação
- Início rápido
- Métodos de verificação do signatário
- O ciclo de vida da assinatura
- OAuth para aplicações de marketplace
- Referência por recurso
- Parâmetros de consulta
- Formatos de resposta
- Erros
- Ambientes
- Desenvolvimento
- Documentação
- Licença
- Python 3.10+; para produção, prefira Python 3.14, com manutenção ativa. Python não possui uma edição LTS; 3.10 permanece aceito por compatibilidade.
httpx(instalado automaticamente)- TLS 1.2 ou superior: o cliente recusa TLS 1.0 e 1.1
pip install assinafyPrefira api_key — é enviada no header documentado X-Api-Key. token envia
Authorization: Bearer <token>, para fluxos de token de usuário. Se os dois forem informados, a
chave de API tem precedência.
client = AssinafyClient(api_key="k_xxx", account_id="acc_xxx")
client = AssinafyClient(token="jwt_xxx", account_id="acc_xxx")O SDK omite as duas credenciais nas rotas cuja segurança publicada é pública ou apenas por
código de acesso do signatário — assim uma chave de API nunca é enviada a um endpoint que não a
espera. Toda requisição de produção e sandbox usa
User-Agent: Assinafy-Python-SDK/v<versão-do-pacote>.
Clientes sem autenticação são permitidos, para endpoints públicos e por código de acesso:
public_client = AssinafyClient()
session = public_client.authentication.login("usuario@example.com", "senha")Quando o usuário tem autenticação em dois fatores ativada, login() devolve um mfa_token em vez
de uma sessão. Conclua o login em até 5 minutos com o código do aplicativo autenticador do usuário
ou com um dos seus códigos de recuperação:
if "mfa_token" in session:
session = public_client.authentication.verify_mfa(session["mfa_token"], "123456")
token = session["access_token"]Há quatro formas de autenticar, e a escolha depende de qual workspace você está operando:
| Modo | Enviado como | Use quando |
|---|---|---|
| Chave de API | X-Api-Key |
você automatiza o seu próprio workspace — recomendado para back ends |
| Token de acesso | Authorization: Bearer |
você tem uma sessão de usuário obtida em authentication.login() |
| Código de acesso do signatário | parâmetro de consulta signer-access-code |
você conduz os endpoints voltados ao signatário |
| OAuth 2.1 + PKCE | Authorization: Bearer |
você constrói um app que outras pessoas conectam ao workspace delas — veja OAuth para aplicações de marketplace |
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
api_key |
str | None | Enviada como X-Api-Key. |
token |
str | None | Enviado como Authorization: Bearer <token>. |
account_id |
str | None | ID padrão da conta / workspace para métodos com escopo de conta. |
base_url |
str | https://api.assinafy.com.br/v1 |
URL base da API. |
webhook_secret |
str | None | Segredo de assinatura do endpoint de webhook (whsec_...) usado por client.webhook_verifier. |
timeout |
float | 30.0 |
Timeout da requisição, em segundos. |
logger |
object | no-op | Objeto com métodos debug/info/warning/error. |
Use https://sandbox.assinafy.com.br/v1 como base_url para trabalhar no sandbox.
A base_url deve conter apenas esquema, host, porta e caminho. O construtor rejeita uma URL que
embuta credenciais (https://usuario:senha@host/v1, que substituiria silenciosamente sua chave de
API por autenticação HTTP Basic) ou que carregue query string ou fragmento (que colaria o caminho da
requisição no componente errado da URL).
http:// em texto puro é rejeitado para qualquer host que não seja loopback: login, verify_mfa,
social_login, change_password, reset_password e create_api_key enviam segredos no corpo da
requisição mesmo quando o cliente não tem api_key nem token, então a interface de loopback é o
único lugar onde texto puro é seguro. Aponte servidores locais e de mock para
localhost/127.0.0.1.
O cliente é um context manager e mantém um pool de conexões HTTP — use with ou chame close() ao
terminar.
upload_and_request_signatures executa o caso comum de ponta a ponta:
import os
from assinafy import AssinafyClient
with AssinafyClient(
api_key=os.environ["ASSINAFY_API_KEY"],
account_id=os.environ["ASSINAFY_ACCOUNT_ID"],
webhook_secret=os.environ.get("ASSINAFY_WEBHOOK_SECRET"),
) as client:
resultado = client.upload_and_request_signatures(
source={"file_path": "./contrato.pdf"},
signers=[
{"full_name": "João Silva", "email": "joao@example.com"},
{"full_name": "Maria Souza", "email": "maria@example.com"},
],
message="Por favor, assine este contrato",
)
print(resultado["document"]["id"])Ele encadeia três chamadas — upload, criação de cada signatário e criação do assignment — e não é
transacional: uma falha no meio do caminho não desfaz o que já deu certo. Todo AssinafyError
levantado depois do upload carrega document_id e os signer_ids já criados no seu context, para
que você possa apagar os órfãos ou repetir os passos restantes. Aceita também wait_timeout /
wait_poll_interval para sobrescrever o polling padrão de prontidão do documento.
Signatários só com telefone usam verificação e notificação por WhatsApp, o que exige
disponibilidade na conta e consome créditos — use assignments.estimate_cost() quando o custo
precisar ser conhecido antes do envio.
Quando você precisar de IDs explícitos, controle de custo ou limpeza, conduza o mesmo ciclo de vida pelos recursos individuais — é o que a próxima seção percorre.
Definidos por signatário em verification_method 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; a notificação que ela exige custa 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 | 0,5 crédito, mais a 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, e uma combinação inválida gera ValidationError antes da requisição.
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.
signatario = client.signers.update(signer_id, {"government_id": "00000000000"})
custo = client.assignments.estimate_cost(document_id, {
"method": "virtual",
"signers": [{"verification_method": "DigitalCertificate",
"notification_methods": ["Email"]}],
})
client.assignments.create(document_id, {
"method": "virtual",
"signers": [{
"id": signatario["id"],
"step": 1,
"verification_method": "DigitalCertificate",
"notification_methods": ["Email"],
}],
})Antes de abrir o assignment, o signatário precisa confirmar os dados de identidade e aceitar os
termos — confirm_data(..., {"has_accepted_terms": True}) resolve os dois em uma chamada, e
accept_terms() nunca é bloqueado. Passar has_accepted_terms para get_for_signer() é tarde
demais para abrir essa trava.
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.
O navegador conduz a operação Web PKI com o certificado escolhido pelo signatário.
As duas rotas estão implantadas em produção e no sandbox, mas não constam como operações no
documento OpenAPI, então o contrato de requisição e resposta delas não é publicado e este SDK não as
chama — conduza esse passo pelo fluxo de assinatura hospedado pela Assinafy. Todo o resto está
coberto: criar o assignment, confirm_data, estimativa de custo e o download do artefato pades
depois que o fluxo termina.
Um documento vai do upload ao PDF certificado em sete estágios. Cada estágio abaixo é uma chamada real que você pode executar em ordem.
import os
from assinafy import AssinafyClient
client = AssinafyClient(
api_key=os.environ["ASSINAFY_API_KEY"],
account_id=os.environ["ASSINAFY_ACCOUNT_ID"],
)Signatários são registros do workspace, reaproveitados entre documentos. Procure antes de criar uma duplicata:
signer = client.signers.find_by_email("signatario@example.com")
if signer is None:
signer = client.signers.create({
"full_name": "Signatário de Exemplo",
"email": "signatario@example.com",
})O cadastro exige full_name. Para solicitar uma assinatura, forneça o canal escolhido: email ou
whatsapp_phone_number no formato E.164 (+5548999990000). O canal informado determina como aquele
signatário pode ser verificado e notificado no estágio 4.
Uploads usam o formato multipart documentado e são limitados localmente a PDFs de até 25 MB (a API ainda limita documentos a 2000 páginas):
document = client.documents.upload({"file_path": "./contrato.pdf"})
# ou, a partir da memória:
# client.documents.upload({"buffer": pdf_bytes, "file_name": "contrato.pdf"})O documento fica em status uploaded enquanto a Assinafy renderiza as imagens das páginas e extrai
os metadados. Espere isso terminar antes de posicionar campos ou solicitar assinaturas:
document = client.documents.wait_until_ready(document["id"])wait_until_ready consulta documents.get até o status chegar a metadata_ready,
pending_signature ou certificated; levanta erro em um status de falha terminal e não repete um
404, que esperar nunca resolveria. Renomeie o documento aqui, se precisar — a API tranca o nome
assim que existe um assignment:
document = client.documents.rename(document["id"], "Contrato de prestação de serviços.pdf")Criar um assignment envia notificações reais e consome créditos. Coloque preço antes, quando o custo importa:
estimativa = client.assignments.estimate_cost(document["id"], {
"method": "virtual",
"signers": [{
"verification_method": "Email",
"notification_methods": ["Email"],
}],
})
if not estimativa["has_sufficient_resources"]:
raise SystemExit(estimativa["blocking_reason"])O corpo da estimativa aceita apenas descritores de preço — o SDK remove os IDs de signatário do fio, já que não fazem parte do contrato de estimativa.
Um assignment virtual pede a assinatura de cada signatário sem posicionar campos. step controla
a ordem: signatários no mesmo passo assinam em paralelo, e o passo seguinte é notificado quando o
anterior termina.
assignment = client.assignments.create(document["id"], {
"method": "virtual",
"signers": [{
"id": signer["id"],
"verification_method": "Email",
"notification_methods": ["Email"],
"step": 1,
}],
"message": "Por favor, revise e assine.",
"expires_at": "2030-12-31T23:59:59Z",
})Um assignment collect posiciona campos reutilizáveis em páginas específicas, com entries:
page = document["pages"][0]
field = client.fields.list()["data"][0]
assignment = client.assignments.create(document["id"], {
"method": "collect",
"signers": [{
"id": signer["id"],
"verification_method": "Email",
"notification_methods": ["Email"],
"step": 1,
}],
"entries": [{
"page_id": page["id"],
"fields": [{
"signer_id": signer["id"],
"field_id": field["id"],
"display_settings": {
"left": 69,
"top": 282,
"width": 421,
"height": 45.86,
"fontSize": 18,
"fontFamily": "Arial",
"backgroundColor": "#D5EBFF",
},
}],
}],
})left, top, width, height e fontSize são valores obrigatórios em pixels da imagem da página
a 150 DPI, medidos do canto superior esquerdo; largura, altura e tamanho de fonte devem ser
positivos e as coordenadas não negativas. A API não corta retângulos fora dos limites, então
mantenha o posicionamento dentro do width/height informado pela página. fontFamily e
backgroundColor são metadados de apresentação opcionais.
DigitalCertificate também é aceito como verification_method; veja
Certificado digital ICP-Brasil (A1/A3) para os requisitos, o
custo, e por que o SDK para antes do handshake da assinatura em si.
Com o assignment criado, você pode ajustar ou reenviar as notificações:
client.assignments.reset_expiration(document["id"], assignment["id"], "2031-01-31T00:00:00Z")
client.assignments.reset_expiration(document["id"], assignment["id"], None) # remove a expiração
client.assignments.estimate_resend_cost(document["id"], assignment["id"], signer["id"])
client.assignments.resend_notification(document["id"], assignment["id"], signer["id"])
client.assignments.whatsapp_notifications(document["id"], assignment["id"])resend_notification() envia uma mensagem real e cobra o canal de notificação novamente; chame
estimate_resend_cost() antes quando o custo precisar ser conhecido.
Signatários agem com um código de acesso de signatário de uso único, não com a sua chave de API.
O SDK o envia no parâmetro de consulta documentado signer-access-code e nunca anexa as credenciais
do seu workspace a essas rotas.
# O signatário abre o link e confirma o código recebido por e-mail ou WhatsApp.
client.signers.verify_code(signer_access_code, "123456")
client.signers.accept_terms(signer_access_code)
# Leia o que esse signatário pode ver.
view = client.assignments.get_for_signer(signer_access_code)
me = client.signers.get_self(signer_access_code)
# Confirme os dados de identidade e então submeta.
client.signers.confirm_data(
document["id"],
signer_access_code,
{"full_name": "Signatário de Exemplo", "email": "signatario@example.com",
"government_id": "00000000000"},
)Um assignment virtual submete uma lista de itens vazia; um assignment collect submete uma entrada por campo preenchido:
client.assignments.sign(document["id"], assignment["id"], [], signer_access_code)
client.assignments.sign(
document["id"],
assignment["id"],
[{"itemId": "item-1", "fieldId": "field-1", "pageId": "page-1", "value": "João Silva"}],
signer_access_code,
)Recusar é a alternativa mutuamente exclusiva a assinar: o motivo deve conter de 1 a 2000 caracteres e não pode conter apenas espaços:
client.assignments.decline(
document["id"], assignment["id"], "Não concordo com os termos.", signer_access_code
)Um signatário com vários documentos pendentes pode agir em todos de uma vez — veja Documentos do signatário.
Webhooks são o canal confiável. Uma conta pode registrar 1 endpoint de webhook, ou até 3 nos planos pagos; todo endpoint ativo inscrito num evento o recebe. Crie um endpoint assinado e guarde o segredo junto com os demais segredos da sua aplicação:
endpoint = client.webhooks.create_endpoint({
"url": "https://example.com/webhooks/assinafy",
"email": "ops@example.com",
"events": ["document_ready", "signer_signed_document", "signer_rejected_document"],
"name": "ERP",
"signing_enabled": True,
})
secret = client.webhooks.get_endpoint_secret(endpoint["id"]) # "whsec_..."Entregas assinadas seguem a especificação Standard Webhooks.
No receptor, verifique o corpo bruto e os headers antes de confiar no evento, e deduplique pelo
header webhook-id:
from assinafy import WebhookVerifier
verifier = WebhookVerifier(os.environ["ASSINAFY_WEBHOOK_SECRET"])
raw_body = request.get_data()
if not verifier.verify_delivery(raw_body, request.headers):
return "Assinatura inválida", 401
event = verifier.extract_event(raw_body)
event_type = verifier.get_event_type(event) # ex.: "document_ready"
target = verifier.get_event_object(event) # o documento afetado
if event_type == "document_ready":
signed_pdf = client.documents.download(target["id"], "certificated")Polling e a trilha de atividades também funcionam:
document = client.documents.get(document["id"])
print(document["status"])
client.documents.activities(document["id"])activities() devolve todos os eventos registrados do documento, cada um com um snapshot do
payload do evento e a origin da requisição (ip, user-agent).
Quando todos os signatários concluem, o documento chega a certificated e seus artefatos ficam
disponíveis:
document = client.documents.get(document["id"])
if document["status"] == "certificated":
signed_pdf = client.documents.download(document["id"], "certificated")
certificate_page = client.documents.download(document["id"], "certificate-page")
everything = client.documents.download(document["id"], "bundle")| 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 |
Quem tiver o hash da assinatura verifica um documento sem nenhuma credencial:
AssinafyClient().documents.verify(signature_hash)Guarde os IDs de documento, signatário e assignment devolvidos. Apague apenas recursos descartáveis que você criou, e na ordem inversa das dependências.
Quando o layout do documento é fixo, crie-o a partir de um template e pule os estágios 2 e 4 — o posicionamento de campos e os papéis já vivem no template:
templates = client.templates.list({"search": "NDA"})
template = client.templates.get(templates["data"][0]["id"])
roles = template["roles"]
client.documents.estimate_cost_from_template(
template["id"],
[{"role_id": role["id"], "verification_method": "Email"} for role in roles],
)
role_signers = []
for index, role in enumerate(roles, start=1):
role_signer = client.signers.create({
"full_name": f"Example Signer {index}",
"email": f"signer{index}@example.com",
})
role_signers.append({
"role_id": role["id"], "id": role_signer["id"],
"verification_method": "Email", "notification_methods": ["Email"],
})
document = client.documents.create_from_template(
template["id"],
role_signers,
{"name": "NDA - João Silva", "message": "Por favor, assine."},
)Signatários de template tomam uma entrada por papel do template, aceitam no máximo um método de
notificação cada, e seguem as mesmas regras de step contíguo dos assignments (papéis de cópia
ignoram step). options também aceita expires_at, editor_fields
(pares {"field_id": ..., "value": ...} gravados no documento gerado) e tags — nomes de tag que
não existem são criados automaticamente e mesclados às tags padrão do template.
Tudo acima pressupõe que você automatiza o seu próprio workspace com uma chave de API. Se você está construindo um produto que outras pessoas conectam ao workspace delas, não peça a chave de API dessas pessoas: execute o fluxo OAuth 2.1 de authorization code com PKCE e receba um token limitado ao que elas aprovaram, para o único workspace que escolheram, e que elas podem desligar a qualquer momento.
| Chave de API | OAuth | |
|---|---|---|
| Age sobre | o seu próprio workspace | o workspace de outra pessoa, com a permissão dela |
| Pode fazer | tudo que a sua conta pode | somente o que o usuário aprovou |
| O usuário pode desligar | não | sim, a qualquer momento |
O registro de aplicações e o consentimento usam a produção. O sandbox também publica os endpoints de token e os metadados do recurso protegido; configure o cliente com a URL base do ambiente que receberá o token.
O recurso é uma fábrica, e não um atributo, porque uma aplicação OAuth tem credenciais próprias:
import os
import os
from assinafy import AssinafyClient
# Um cliente sem credenciais basta para todo o fluxo até a troca do código.
oauth = AssinafyClient().oauth(
os.environ["ASSINAFY_OAUTH_CLIENT_ID"],
os.environ.get("ASSINAFY_OAUTH_CLIENT_SECRET"), # só aplicações confidenciais
)O SDK não guarda tokens, não mantém travas de renovação e não renova nada por conta própria. Essas são decisões da sua aplicação, e as seções abaixo dizem exatamente onde elas caem.
No app da Assinafy (https://app.assinafy.com.br) abra Integrações → Apps OAuth → Novo aplicativo. Você precisa ser proprietário do workspace que será dono da aplicação, e o plano dele precisa incluir aplicações OAuth.
| Campo | O que colocar |
|---|---|
| Nome, Descrição, URL do logo | o que o usuário lê na tela de aprovação |
| URIs de redirecionamento | para onde o usuário volta, ex.: https://meuapp.example.com/oauth/callback. Deve ser https://, sem #, e é comparada exatamente — …/callback e …/callback/ são diferentes. Registre uma por ambiente; em desenvolvimento local use um túnel HTTPS, porque http://localhost não é aceito |
| Permissões | o máximo que sua aplicação pedirá algum dia; você pode pedir menos na hora de conectar, nunca mais |
| Tipo | Confidencial se o código roda num servidor seu, Pública se roda no dispositivo do usuário. Não pode ser alterado depois |
Você recebe um client_id e, para aplicações confidenciais, um client_secret exibido uma única
vez. Guarde o segredo no cofre de segredos do seu servidor; nunca o publique em código de navegador,
em app mobile ou em repositório.
Os escopos:
| Escopo | Permite à sua aplicação |
|---|---|
documents:read |
ler documentos, seus signatários, assignments, atividades e histórico de webhooks |
documents:write |
criar documentos e enviá-los para assinatura |
templates:read / templates:write |
ler / alterar templates |
account:read |
ler o perfil, o tema, o logo e os endpoints de webhook do workspace |
webhooks:write |
criar, alterar, desativar e excluir os endpoints de webhook do workspace |
openid, profile, email |
identificar o usuário e ler nome e e-mail |
offline_access |
receber um refresh token, para continuar funcionando com o usuário ausente |
Peça o mínimo: cada permissão é uma linha a mais que o usuário lê antes de decidir, e ele aprova tudo ou nada. Cobrança, assinatura do plano, participação no workspace, gestão de credenciais e administração nunca são alcançáveis por um token OAuth, quaisquer que sejam os escopos.
start_authorization() gera um verificador PKCE e um state novos (e um nonce quando você pede
openid) e devolve a URL mais a transação que os passos seguintes precisam. Não faz requisição
HTTP.
start = oauth.start_authorization(
"https://meuapp.example.com/oauth/callback",
["documents:read", "documents:write", "offline_access"],
)
session["assinafy_oauth"] = start # a sessão de servidor do usuário
return redirect(start["authorization_url"]) # navegação de página completa, não AJAX{
"authorization_url": "https://auth.assinafy.com.br/oauth/authorize?response_type=code&...",
"state": "<aleatório por tentativa>",
"code_verifier": "<aleatório por tentativa>",
"code_challenge": "<sha256 do verificador, em base64url>",
"redirect_uri": "https://meuapp.example.com/oauth/callback",
"issuer": "https://auth.assinafy.com.br",
"resource": "https://api.assinafy.com.br",
}Chame uma vez por tentativa de conexão: um verificador e um state novos a cada vez são toda a
proteção contra o material de uma tentativa ser reutilizado em outra. Guarde o retorno na sessão
autenticada do usuário, não em cookie nem na URL.
Se o client_id ou a redirect_uri estiverem errados, o usuário não é devolvido a você: o
servidor de autorização mostra o erro na própria página, porque redirecionar para um endereço não
verificado seria inseguro. Usuários presos numa página de erro da Assinafy normalmente significam
que um desses dois valores está errado.
handle_callback() é o passo crítico de segurança. Compara state e iss em tempo constante
antes de o código ser usado em qualquer lugar, e levanta o código de erro do RFC quando o
usuário recusou.
from assinafy import ApiError, ValidationError
transaction = session.pop("assinafy_oauth")
try:
code = oauth.handle_callback(request.args, transaction)
except ValidationError:
return "Esta resposta não é nossa", 400 # state ou iss divergentes
except ApiError as err:
if str(err) == "access_denied":
return "Você recusou a conexão", 200
raise
tokens = oauth.exchange_code(code, transaction){
"access_token": "<access-token>",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "documents:read documents:write",
"refresh_token": "<refresh-token>", # só com offline_access
"id_token": "<id-token assinado>", # só com openid
}O código é de uso único e expira 60 segundos após a aprovação, então faça a troca no servidor e
imediatamente. Leia o scope devolvido em vez de supor que toda permissão pedida foi concedida;
offline_access é um sinal de requisição e nunca aparece ali.
Respostas de OAuth são JSON plano — são as únicas chamadas desta API que não vêm embrulhadas no
envelope {status, message, data}. Em caso de falha, str(err) é o código de erro do RFC
(invalid_grant, invalid_client, invalid_target, unsupported_grant_type) e
err.response_data carrega error_description, então ramifique pela mensagem.
Um token pertence ao único workspace que o usuário escolheu. Com um token OAuth, a listagem de workspaces devolve exatamente aquele workspace, então guarde o ID dele junto dos tokens:
api = AssinafyClient(token=tokens["access_token"])
workspace_id = api.accounts.list()[0]["id"]
documentos = api.documents.list({"per_page": 20}, account_id=workspace_id)Envie sempre o token em Authorization: Bearer; como X-Api-Key ou na query string ele é recusado.
Chamar qualquer outro workspace devolve 403, mesmo um ao qual o mesmo usuário pertence — este é o
erro de integração que mais vemos. Se o seu cliente usa vários workspaces, conecte cada um
separadamente e guarde tokens por workspace.
Uma permissão que falta responde 403 com um desafio que a nomeia:
WWW-Authenticate: Bearer error="insufficient_scope", scope="documents:write", ...
Trate isso como um pedido para reconectar com aquele escopo adicionado, não como algo a repetir. Um
403 sem esse header tem outra causa: outro workspace, o papel do próprio usuário, ou uma área que
tokens OAuth nunca alcançam.
Tokens de acesso duram 1 hora. Com offline_access você os renova sem o usuário. Um refresh
token vale por 30 dias, e cada renovação devolve um novo, válido por outros 30 dias: a conexão
só expira se a sua aplicação passar 30 dias sem renovar, e depois disso o usuário precisa
reconectar.
tokens = oauth.refresh(refresh_token_guardado)
salvar(tokens["refresh_token"]) # antes de fazer qualquer outra coisa com a respostaToda renovação aposenta o token que usou. Um refresh token reapresentado não pode ser distinguido de um roubado, então o servidor encerra a conexão inteira ao ver um: todos os tokens param de funcionar e o usuário precisa conectar de novo. Persista o novo refresh token antes de usar o access token, mantenha uma renovação por vez por conexão, e trate um timeout como "talvez tenha funcionado" — releia o que você guardou em vez de repetir com o token antigo.
Quando um usuário desconecta no seu produto, revogue em vez de apenas apagar a sua cópia. Revogar o refresh token encerra a conexão inteira:
oauth.revoke(carregar_refresh_token_salvo(), "refresh_token") # o último token salvo, nunca uma cópia antiga
apagar_tokens_salvos()O endpoint responde 200 para qualquer desfecho do token — revogado, já revogado, desconhecido,
malformado — então ele nunca pode ser usado para descobrir se um token existe, e sucesso não é prova
de que o token era real. Só falha de autenticação do cliente responde 401. Os usuários também
podem revogar sua aplicação em Aplicativos conectados; trate o 401 resultante pedindo que
reconectem.
Em vez de fixar endpoints no código, leia-os. protected_resource_metadata() (RFC 9728) descreve
esta API e nomeia o servidor de autorização dela; authorization_server_metadata() (RFC 8414)
descreve esse servidor. Os dois documentos ficam na raiz do respectivo host, acima de /v1, e são
buscados sem as credenciais do seu workspace.
resource = oauth.protected_resource_metadata()
server = oauth.authorization_server_metadata(resource["authorization_servers"][0])
start = oauth.start_authorization(
"https://meuapp.example.com/oauth/callback",
["documents:read", "openid", "email"],
issuer=server["issuer"],
authorization_endpoint=server["authorization_endpoint"],
)Peça openid (mais profile e/ou email) para autenticar usuários. Valide o id_token com uma
biblioteca OpenID Connect mantida — RS256, chaves no jwks_uri do servidor, iss igual ao issuer,
aud igual ao seu client_id, exp no futuro, e nonce igual ao da sua transação. Para nome e
e-mail, leia as claims em vez do token:
claims = oauth.userinfo(tokens["access_token"])
# {"sub": "...", "name": "Usuário de Exemplo", "email": "pessoa@example.com",
# "email_verified": True}sub é o identificador estável do usuário. userinfo() autentica como qualquer outra rota da API,
então, ao contrário dos endpoints de token, seus 401/403 chegam no envelope comum; o SDK remove
o X-Api-Key do cliente nessa chamada para que uma chave de workspace nunca responda pela
identidade errada.
Se o seu framework é o dono do material de sessão, as primitivas PKCE estão disponíveis diretamente:
verifier = OAuthResource.create_code_verifier() # 43 caracteres, gramática do RFC 7636
challenge = OAuthResource.code_challenge(verifier)
state = OAuthResource.create_state()exchange_token() implementa o grant RFC 8693 para aplicações confidenciais
cadastradas pela Assinafy como serviços internos. Aplicações públicas e
confidenciais comuns recebem invalid_client; esse grant não substitui o
consentimento do marketplace.
service = client.oauth(
os.environ["ASSINAFY_SERVICE_CLIENT_ID"],
os.environ["ASSINAFY_SERVICE_CLIENT_SECRET"],
)
derived = service.exchange_token(
subject_access_token,
resource="https://api.assinafy.com.br",
scopes=["documents:read"],
)O corpo form contém grant_type, client_id, client_secret, subject_token,
subject_token_type, resource e, quando solicitado, scope. O recurso é a
origem HTTPS da API, sem /v1; os escopos devem ser um subconjunto dos escopos
do token original. A resposta plana contém access_token, issued_token_type,
token_type, expires_in e scope, sem refresh token. A validade não excede a
do token original. O token derivado pertence à aplicação original: revogá-lo
com as credenciais do serviço retorna sucesso sem revogá-lo.
- Um verificador PKCE e um
statenovos em cada tentativa de conexão stateeissverificados na sua URI de redirecionamento —handle_callback()faz os doisclient_secretsó no seu servidor, nunca em código de navegador, app mobile ou repositório- O novo refresh token salvo antes de usar, e uma renovação por vez por conexão
401tratado: renove e, se falhar, peça ao usuário para reconectar- O ID do workspace guardado por conexão, e o
scopedevolvido lido - Toda URI de redirecionamento de produção registrada,
https://e exata - Somente as permissões necessárias
- Tokens revogados quando um usuário desconecta
Aplicações novas não são verificadas: a tela de aprovação avisa que a Assinafy não revisou o app, e ele pode ser conectado a no máximo 25 workspaces. Os endpoints de autorização e de token aceitam 50 requisições por minuto por IP — atingir esse limite normalmente significa um laço de renovação.
Todo método abaixo aparece em contexto acima; esta seção é o índice plano.
client.authentication.login("usuario@example.com", "senha")
client.authentication.social_login("google", "provider-token", True)
client.authentication.link_social_login("google", "provider-token")
client.authentication.verify_mfa("mfa-token-do-login", "123456")
# Métodos de dois fatores do usuário autenticado.
client.authentication.list_mfa_methods()
enrollment = client.authentication.start_totp_enrollment("Meu celular") # segredo + URI otpauth
client.authentication.confirm_totp_enrollment(enrollment["id"], "123456") # códigos de recuperação
client.authentication.regenerate_recovery_codes(password="senha")
client.authentication.delete_mfa_method(enrollment["id"], code="123456")
client.authentication.create_api_key("senha")
client.authentication.get_api_key()
client.authentication.delete_api_key()
client.authentication.change_password("usuario@example.com", "antiga", "nova")
client.authentication.request_password_reset("usuario@example.com")
client.authentication.reset_password("usuario@example.com", "nova", token="reset-token")accounts = client.accounts.list()
nova_conta = client.accounts.create("Workspace de Exemplo")
nova_id = nova_conta["id"]
nova_conta = client.accounts.update({"notification_sender_type": "Account"}, nova_id)
nova_conta = client.accounts.get(nova_id)
nova_conta = client.accounts.update({"name": "Workspace Atualizado"}, nova_id)
theme = client.accounts.theme(nova_id)
stats = client.accounts.stats("monthly", account_id=nova_id)
stats_diarias = client.accounts.stats("daily", "2026-08", account_id=nova_id)
client.accounts.upload_logo({"file_path": "./logo.png"}, nova_id)
logo_bytes = client.accounts.download_logo(nova_id)
client.accounts.delete_logo(nova_id)
# Apague apenas o workspace descartável criado acima.
client.accounts.delete(nova_id)delete() atinge o ID de conta informado (ou o padrão do cliente) e torna force exclusivo de
palavra-chave, para que um ID posicional nunca seja confundido com a flag. Use force=True apenas
quando você quiser intencionalmente cancelar a assinatura paga ativa daquela conta como parte da
exclusão. Criar a conta primeiro e definir notification_sender_type com update() funciona em
todas as versões implantadas da API.
user = client.users.me()
stats = client.users.stats("monthly")
preferences = client.users.notification_preferences()
preferences = client.users.update_notification_preferences({
"DocumentCompleted": True,
"SignerDeclined": True,
})update_notification_preferences mescla um mapa parcial; chaves omitidas mantêm seus valores, e a
resposta é sempre o mapa completo de nove chaves.
doc = client.documents.upload({"file_path": "./contrato.pdf"})
doc = client.documents.upload({"buffer": pdf_bytes, "file_name": "contrato.pdf"})
client.documents.statuses()
client.documents.list({"page": 1, "per_page": 20, "tags": "tag-id", "sort": "updated_at"})
client.documents.search({"search": "nda", "status": "metadata_ready"}) # leve, compacto
client.documents.get(doc["id"])
client.documents.rename(doc["id"], "Contrato.pdf") # antes de a assinatura começar
client.documents.activities(doc["id"])
client.documents.wait_until_ready(doc["id"])
client.documents.download(doc["id"], "certificated")
client.documents.download(doc["id"], "pades") # artefato de certificado ICP-Brasil
client.documents.thumbnail(doc["id"])
client.documents.download_page(doc["id"], page_id)
client.documents.verify(signature_hash)
client.documents.public_info(doc["id"])
# Escolha um formato; cada chamada envia um token real.
client.documents.send_token(doc["id"], "signatario@example.com", "email")
# Corpo legado alternativo:
# client.documents.send_token(doc["id"], email="signatario@example.com")
client.documents.list_tags(doc["id"])
client.documents.replace_tags(doc["id"], [tag_id_a, tag_id_b])
client.documents.append_tags(doc["id"], [tag_id_c])
client.documents.detach_tag(doc["id"], tag_id)
client.documents.delete(doc["id"])list() filtra por status, method, search, tags (IDs separados por vírgula, retornando
documentos que carregam todas) e sort (name ou updated_at). search() é a versão compacta,
que devolve documentos sem os campos expandidos assignment/pages. A exclusão só é permitida
enquanto o documento está num status deletável.
templates = client.templates.list({"search": "NDA", "per_page": 20})
template = client.templates.get(template_id)
client.documents.create_from_template(
template_id,
[{"role_id": "role-id", "id": signer_id, "verification_method": "Email"}],
{"name": "NDA - João Silva", "message": "Por favor, assine."},
)
client.documents.estimate_cost_from_template(
template_id,
[{"role_id": "role-id", "verification_method": "Email"}],
)O OpenAPI publicado expõe apenas list. get é mantido porque a rota está implantada e responde na
API ao vivo, e o texto do schema publicado descreve uma resposta de template único que adiciona
default_document_tags.
tags = client.tags.list({"search": "contrato"})
tag = client.tags.create({"name": "Contratos", "color": "ff8800"})
client.tags.update(tag["id"], {"name": "Contratos de Venda"})
client.tags.update(tag["id"], {"color": None}) # remove a cor
client.tags.delete(tag["id"])
# Se a tag estiver vinculada, use isto no lugar da linha anterior:
# client.tags.delete(tag["id"], force=True)signer = client.signers.create({
"full_name": "João Silva",
"email": "joao@example.com",
})
client.signers.create({
"full_name": "Maria Souza",
"whatsapp_phone_number": "+5548999990000",
})
client.signers.get(signer["id"])
client.signers.list({"search": "joão", "per_page": 50})
client.signers.update(signer["id"], {"full_name": "João P. Silva"})
client.signers.find_by_email("joao@example.com")
client.signers.delete(signer["id"])create() aceita full_name, email, whatsapp_phone_number e government_id (o CPF ou CNPJ
que o certificado digital exige), então o documento do signatário já pode nascer completo.
Endpoints por código de acesso do signatário:
client.signers.get_self(signer_access_code)
client.signers.accept_terms(signer_access_code)
client.signers.verify_code(signer_access_code, "123456")
# verify_email(...) permanece como alias retrocompatível.
client.signers.confirm_data(
document_id,
signer_access_code,
{"full_name": "João Silva", "email": "joao@example.com",
"government_id": "00000000000"},
)
client.signers.upload_signature(signer_access_code, png_bytes, "signature")
# Alternativa: client.signers.upload_signature(signer_access_code, png_bytes, reuse=True)
client.signers.download_signature(signer_access_code, "signature")update() não pode alterar um canal já verificado para um documento em curso; a API garante isso no
servidor.
client.assignments.list({"page": 1, "per_page": 20})
client.assignments.estimate_cost(document_id, {"signers": [{"verification_method": "Email"}]})
client.assignments.create(document_id, {"method": "virtual", "signers": [...]})
client.assignments.reset_expiration(document_id, assignment_id, "2031-01-31T00:00:00Z")
client.assignments.estimate_resend_cost(document_id, assignment_id, signer_id)
client.assignments.resend_notification(document_id, assignment_id, signer_id)
client.assignments.whatsapp_notifications(document_id, assignment_id)
# Voltados ao signatário:
client.assignments.get_for_signer(signer_access_code)
client.assignments.sign(document_id, assignment_id, [], signer_access_code)
client.assignments.decline(document_id, assignment_id, "Não concordo.", signer_access_code)list() é escopado pela API à conta atual da credencial autenticada. O SDK encaminha um parâmetro
de contexto accountId, mas passar um account_id diferente não reescopa este endpoint — use
uma credencial que pertença àquele workspace.
oauth = client.oauth("client-id", "client-secret")
start = oauth.start_authorization(
"https://meuapp.example.com/oauth/callback",
["documents:read", "documents:write", "offline_access"],
)
code = oauth.handle_callback(callback_query, start)
tokens = oauth.exchange_code(code, start)
tokens = oauth.refresh(tokens["refresh_token"])
claims = oauth.userinfo(tokens["access_token"])
oauth.revoke(tokens["refresh_token"], "refresh_token")
oauth.protected_resource_metadata()
oauth.authorization_server_metadata()
# Primitivas PKCE estáticas, para frameworks que são donos do material de sessão.
OAuthResource.create_code_verifier()
OAuthResource.create_state()
OAuthResource.code_challenge(verifier)start_authorization() e handle_callback() não fazem requisição HTTP. Veja
OAuth para aplicações de marketplace para o fluxo completo.
client.signer_documents.current(signer_id, signer_access_code)
client.signer_documents.list(signer_id, signer_access_code, {"page": 1, "per_page": 20})
client.signer_documents.search(signer_id, signer_access_code, "contrato") # leve
client.signer_documents.sign_multiple(["doc-1", "doc-2"], signer_access_code)
# Alternativa mutuamente exclusiva:
# client.signer_documents.decline_multiple(["doc-1"], "Termos desfavoráveis.", signer_access_code)
client.signer_documents.download(signer_id, document_id, artifact_name="original")A rota de download é pública. O argumento opcional signer_access_code existe para implantações
que o exijam.
field = client.fields.create({"type": "text", "name": "CPF"})
client.fields.list({"include_standard": True})
client.fields.get(field["id"])
client.fields.update(field["id"], {"name": "CPF atualizado"})
client.fields.update(field["id"], {"regex": None}) # remove o regex
client.fields.validate(field["id"], "390.533.447-05", signer_access_code=signer_access_code)
client.fields.validate_multiple(
[{"field_id": field["id"], "value": "390.533.447-05"}],
signer_access_code=signer_access_code,
)
client.fields.list_types()
client.fields.delete(field["id"])create() recebe type (um dos valores de list_types()) e name, opcionalmente regex e
is_required. is_read_only / is_visible são campos de resposta controlados pelo servidor, não
entrada de criação.
client.webhooks.list_event_types()
client.webhooks.list_endpoints()
client.webhooks.get_endpoint(endpoint_id)
client.webhooks.list_dispatches({"delivered": False, "page": 1, "per_page": 20})
# Chamadas mutantes alteram a entrega real. Guarde e restaure ao redor de mudanças de teste.
# endpoint = client.webhooks.create_endpoint({
# "url": "https://example.com/webhooks/assinafy",
# "email": "ops@example.com",
# "events": ["document_ready", "signer_signed_document"],
# "signing_enabled": True,
# })
# client.webhooks.update_endpoint(endpoint["id"], {"is_active": False})
# client.webhooks.get_endpoint_secret(endpoint["id"])
# client.webhooks.rotate_endpoint_secret(endpoint["id"])
# client.webhooks.delete_endpoint(endpoint["id"])
# client.webhooks.retry_dispatch(dispatch_id)
# Chamadas do endpoint mais antigo, para integrações anteriores a múltiplos endpoints.
client.webhooks.get()
# client.webhooks.register({"url": "https://example.com/webhooks/assinafy",
# "email": "ops@example.com"})
# client.webhooks.inactivate()Endpoints. Uma conta pode registrar 1 endpoint, ou até 3 nos planos pagos; criar um além do
limite levanta ApiError com status 403, e cada endpoint precisa de uma url diferente.
create_endpoint() exige url, email e events, e aceita name, is_active (padrão True) e
signing_enabled (padrão False). update_endpoint() altera apenas os campos enviados. Ativar
signing_enabled gera um segredo quando o endpoint não tem um; desativar descarta o segredo.
delete_endpoint() interrompe a entrega e libera a vaga.
Endpoint mais antigo. get(), register() e inactivate() atuam sobre o endpoint mais antigo
da conta (register() cria um quando não existe nenhum). register() preenche um events ou
is_active omitido a partir desse endpoint, para que uma chamada parcial não o reative
silenciosamente nem colapse sua lista de eventos; passe events=[] para limpar todos os eventos.
Segredos de assinatura. get_endpoint_secret() devolve o segredo whsec_... do endpoint e
rotate_endpoint_secret() o substitui. A rotação vale imediatamente, então atualize o receptor na
hora. Ambos respondem 400 enquanto a assinatura estiver desativada e não estão disponíveis para
aplicações OAuth.
Verificando entregas. Uma entrega assinada traz os headers webhook-id, webhook-timestamp e
webhook-signature. verify_delivery() calcula o HMAC-SHA256 do Standard Webhooks sobre
{webhook-id}.{webhook-timestamp}.{corpo bruto}, compara em tempo constante com cada entrada v1
de webhook-signature e rejeita timestamps a mais de 300 segundos do relógio local (tolerance=
muda a janela). Passe o corpo exatamente como recebido; JSON re-serializado não confere. Cada
endpoint tem seu próprio segredo, então use um WebhookVerifier por endpoint.
from assinafy import WebhookVerifier
verifier = WebhookVerifier("whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw")
verifier.verify_delivery(raw_body, request.headers) # True / False
verifier.verify_delivery(raw_body, request.headers, tolerance=600)verify(raw_body, signature) continua disponível para receptores escritos contra versões
anteriores; ele compara um HMAC-SHA256 hexadecimal apenas do corpo e não verifica entregas Standard
Webhooks.
Interpretando payloads. Todo corpo de webhook compartilha o envelope documentado: id, event,
message, payload (parâmetros do evento), origin, created_at, subject (a entidade que
agiu), object (a entidade sobre a qual se agiu) e account_id. Cada endpoint recebe cada evento
de forma independente, com até 2 tentativas; webhook-id é idêntico em toda tentativa ao mesmo
endpoint.
event = verifier.extract_event(raw_body)
event_type = verifier.get_event_type(event) # ex.: "document_ready"
params = verifier.get_event_payload(event) # parâmetros do evento
subject = verifier.get_event_subject(event) # autor (+ "type")
target = verifier.get_event_object(event) # alvo (+ "type")
# get_event_data(event) é um alias retrocompatível de get_event_object(event)O SDK aceita apelidos Pythônicos para os parâmetros de consulta hifenizados documentados. Por
exemplo, per_page é enviado como per-page, e signer_access_code como signer-access-code.
Valores None são descartados em vez de enviados como parâmetros vazios.
Endpoints JSON normalmente devolvem {"status": 200, "message": "", "data": ...}; o SDK devolve
data. Os endpoints de OAuth são a exceção documentada: RFC 6749, RFC 8414 e OpenID Connect
todos exigem um corpo plano, então exchange_code, refresh, userinfo e os dois métodos de
descoberta devolvem JSON de nível superior, e revoke devolve None.
Operações sem dados devolvem None ou preservam o pequeno envelope {"status", "message"} por
retrocompatibilidade, como está dito na docstring de cada método. Métodos binários devolvem bytes;
métodos paginados devolvem
{"data": [...], "meta": {"current_page", "per_page", "total", "last_page"}}, usando os headers de
paginação da API.
Os formatos estáveis de nível superior de cada recurso são:
{
"Account": {
"resource": "account", "id": "account-id", "name": "Acme Inc.",
"primary_color": "aabbcc", "secondary_color": "112233",
"notification_sender_type": "User", "roles": ["owner"],
"is_delete_allowed": true, "created_at": "2026-06-03T03:54:16Z"
},
"User": {
"id": "user-id", "name": "Usuário de Exemplo", "email": "usuario@example.com",
"telephone": null, "government_id": null, "is_email_verified": true,
"has_accepted_terms": true, "created_at": "2026-06-03T03:54:16Z",
"to_be_deleted_at": null
},
"Signer": {
"resource": "signer", "id": "signer-id", "full_name": "Signatário de Exemplo",
"email": "signatario@example.com", "whatsapp_phone_number": null,
"has_accepted_terms": false
},
"Document": {
"resource": "document", "id": "document-id", "account_id": "account-id",
"template_id": null, "name": "contrato.pdf", "status": "metadata_ready",
"artifacts": {"original": "https://api.example/document/original"},
"is_closed": false, "signing_url": "https://app.example/sign/document-id",
"decline_reason": null, "declined_by": null, "tags": [],
"assignment": null, "pages": [], "created_at": "2026-06-03T03:54:16Z",
"updated_at": "2026-06-03T03:54:17Z"
},
"Assignment": {
"resource": "assignment", "id": "assignment-id",
"sender_email": "remetente@example.com", "method": "virtual",
"expires_at": null, "message": null,
"signers": [{
"resource": "signer", "id": "signer-id", "full_name": "Signatário de Exemplo",
"email": "signatario@example.com", "whatsapp_phone_number": null,
"has_accepted_terms": false, "verification_method": "Email",
"notification_methods": ["Email"], "step": 1, "notified": true,
"completed": false, "notification_history": [{
"event": "signature_request", "status": "sent", "error_code": null,
"error_message": null, "sent_at": "2026-08-26T12:00:00Z",
"failed_at": null
}]
}],
"copy_receivers": [],
"items": [{
"id": "item-id",
"page": {"id": "page-id", "number": 1, "height": 2100,
"width": 1275, "download_url": "https://api.example/page"},
"signer": {"id": "signer-id", "full_name": "Signatário de Exemplo",
"email": "signatario@example.com"},
"field": {"id": "field-id", "name": "Assinatura", "type": "signature"},
"display_settings": {"left": 69, "top": 282, "width": 421,
"height": 45.86, "fontFamily": "Arial",
"fontSize": 18, "backgroundColor": "#D5EBFF"},
"value": null, "completed": false
}],
"summary": {"signer_count": 1, "completed_count": 0,
"signers": [{"id": "signer-id", "full_name": "Signatário de Exemplo",
"email": "signatario@example.com", "completed": false}]},
"signing_urls": [{"signer_id": "signer-id",
"url": "https://api.example/sign/document-id"}]
},
"CostEstimate": {
"documents": 1, "credits": 0.45, "needs_extra_document": false,
"extra_document_cost": 0, "total_credits": 0.45,
"breakdown": [{"code": "NotificationWhatsapp",
"name": "Whatsapp Notification", "cost": 0.45,
"quantity": 1, "unit_cost": 0.45}],
"document_balance": 10, "credit_balance": 0,
"has_sufficient_resources": true, "blocking_reason": null, "message": null
},
"Field": {
"resource": "field", "id": "field-id", "name": "CPF", "type": "text",
"regex": null, "is_pre_defined": false, "is_active": true,
"is_required": true, "is_standard": false, "is_read_only": false,
"is_visible": true
},
"Tag": {
"resource": "tag", "id": "tag-id", "name": "Contratos", "color": null,
"created_at": "2026-06-03T03:54:16Z",
"updated_at": "2026-06-03T03:54:17Z"
},
"WebhookSubscription": {
"events": ["document_ready"], "is_active": true,
"url": "https://example.com/webhooks/assinafy", "email": "ops@example.com",
"updated_at": "2026-06-03T03:54:17Z"
},
"WebhookEndpoint": {
"id": "65f1c2a9b3e4d5f60718293a4b5c6d7e", "name": "ERP",
"url": "https://example.com/webhooks/assinafy", "email": "ops@example.com",
"events": ["document_ready"], "is_active": true, "signing_enabled": true,
"created_at": "2026-10-01T12:00:00Z", "updated_at": "2026-10-01T12:00:00Z"
}
}Valores do recurso de campo são devolvidos literalmente. O valor documentado é "field";
"field_definition" também é aceito.
Contratos de template, preferências de notificação, KPIs, verificação, entregas de webhook e contratos específicos de operação estão documentados junto aos métodos públicos correspondentes; métodos que devolvem recursos compartilhados referenciam os formatos canônicos acima. O SDK preserva campos extras do servidor, de modo que mudanças aditivas na API permanecem utilizáveis.
Falhas de validação, de transporte, de HTTP e de formato de resposta levantam uma subclasse de
AssinafyError, então um único except AssinafyError captura todos os modos de falha documentados.
from assinafy import ApiError, AssinafyError, NetworkError, ValidationError
try:
client.documents.upload({"file_path": "./contrato.pdf"})
except ValidationError as err: # rejeitado antes de a requisição ser enviada
print("Validação falhou")
except ApiError as err: # a API devolveu uma resposta não-2xx
print(f"Erro da API {err.status_code}")
except NetworkError as err: # nenhuma resposta chegou (DNS, TLS, timeout)
print("Erro de rede:", err)
except AssinafyError as err: # formato de resposta inesperado, etc.
print("Erro do SDK")| Produção | https://api.assinafy.com.br/v1 |
| Sandbox | https://sandbox.assinafy.com.br/v1 |
O sandbox permite testar a integração com recursos descartáveis. Mensagens de WhatsApp são simuladas nesse ambiente. O registro e o consentimento OAuth usam o servidor de autorização de produção; os endpoints de token e os metadados também existem no sandbox. A assinatura A1/A3 exige um certificado ICP-Brasil real no dispositivo do signatário e o fluxo de assinatura hospedado pela Assinafy.
python -m pip install -e ".[dev]"
python -m pytest --cov=assinafy --cov-branch --cov-fail-under=90 --cov-report=term-missing
python -m mypy src scripts/live_smoke.py
python -m ruff check src tests scripts
python -m ruff format --check src tests scriptsPara cobertura não mutante, segura para CI (nenhum e-mail de teste necessário):
ASSINAFY_API_KEY=... \
ASSINAFY_ACCOUNT_ID=... \
ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
ASSINAFY_READ_ONLY=1 \
python scripts/live_smoke.pyPara o fluxo de escrita descartável:
ASSINAFY_API_KEY=... \
ASSINAFY_ACCOUNT_ID=... \
ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
ASSINAFY_TEST_EMAILS=primeiro@example.com,segundo@example.com \
ASSINAFY_SEND_TEST_NOTIFICATIONS=1 \
ASSINAFY_TEST_ACCOUNT_LIFECYCLE=1 \
ASSINAFY_TEST_USER_PREFERENCES=1 \
python scripts/live_smoke.pyO script recusa produção e URLs base ausentes, e nunca imprime um payload ou o valor de uma
variável de ambiente. Ele confere os endpoints de leitura, o CRUD de signatário/tag/campo (incluindo
remover o regex de um campo), a busca e a estimativa de template, o upload de documento, a marcação
por tags, o polling de wait_until_ready, a estimativa de custo e a limpeza de ponta a ponta. Todo
ID de recurso devolvido com sucesso é capturado e sua limpeza é tentada num bloco finally. Na limpeza, somente respostas HTTP 429 são repetidas,
com até três esperas de 20 segundos; outras falhas são reportadas. A
mutação de webhook é ignorada a menos que um endpoint de teste explícito seja fornecido; quando
habilitada, a configuração anterior do endpoint mais antigo é restaurada. A listagem de endpoints
e a de métodos de dois fatores são etapas somente leitura. O opt-in de notificações envia
e-mails reais no sandbox e pode consumir créditos; omita-o para cobertura apenas de CRUD. As
mutações de conta e de preferências de usuário são opt-ins separados e são restauradas no finally.
- Atualize
src/assinafy/_version.pye acrescente as notas de release voltadas ao usuário emCHANGELOG.md. - Execute os comandos de desenvolvimento acima, instale as ferramentas de release com
python -m pip install build twine, e então rodepython -m buildepython -m twine check dist/*. - Publique no GitLab e confirme que o branch e o resultado do CI chegaram ao espelho no GitHub.
- Crie e publique uma tag anotada
v<versão>igual a__version__. - Aprove o ambiente protegido
pypie confira a proveniência do Trusted Publishing após o release.
- README.en.md — este mesmo documento, em inglês
- Documentação da API
Distribuído sob a licença MIT.