SDK Python da Assinafy
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 cobre as 93 operações publicadas hoje pela 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
Requisitos
- Python 3.10+
httpx(instalado automaticamente)
Instalação
pip install assinafy
Autenticação
Prefira 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 retém 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")
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 |
Configuração do cliente
| 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 usado pelo WebhookVerifier. |
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,
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.
Início rápido
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.
Métodos de verificação do signatário
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 | 2 créditos, 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 responde 400.
Certificado digital ICP-Brasil (A1/A3)
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": "39053344705"})
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.
POST /v1/signers/certificate/start -> data.token (token da operação Web PKI)
v o navegador assina o token com o certificado do signatário
POST /v1/signers/certificate/complete -> data.signerName
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.
O ciclo de vida da assinatura
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"],
)
1. Prepare os signatários
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",
})
Um signatário precisa de full_name mais pelo menos um canal de contato: 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.
2. Envie o documento
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")
3. Estime o custo
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.
4. Solicite as assinaturas
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.
5. O lado do signatário
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": "39053344705"},
)
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:
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.
6. Acompanhe o progresso
Webhooks são o canal confiável. Registre a assinatura única do workspace e então interprete as entregas:
client.webhooks.register({
"url": "https://example.com/webhooks/assinafy",
"email": "ops@example.com",
"events": ["document_ready", "signer_signed_document", "signer_rejected_document"],
"is_active": True,
})
raw_body = request.get_data()
event = client.webhook_verifier.extract_event(raw_body)
event_type = client.webhook_verifier.get_event_type(event) # ex.: "document_ready"
target = client.webhook_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).
7. Baixe o documento assinado
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.
Partindo de um template
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"])
role_id = template["roles"][0]["id"]
client.documents.estimate_cost_from_template(
template["id"],
[{"role_id": role_id, "verification_method": "Email"}],
)
document = 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."},
)
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.
OAuth para aplicações de marketplace
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 |
OAuth é servido apenas em produção hoje.
https://sandbox.assinafy.com.br/v1/oauth/*responde404.
O recurso é uma fábrica, e não um atributo, porque uma aplicação OAuth tem credenciais próprias:
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.
1. Registre a aplicação
No app da Assinafy (https://app.assinafy.com.br) abra Configurações → Aplicações OAuth → Nova aplicação. 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 e atividades |
documents:write |
criar documentos e enviá-los para assinatura |
templates:read / templates:write |
ler / alterar templates |
account:read |
ler o perfil, o tema e o logo 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.
2. Leve o usuário até a Assinafy
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.
3. Trate o callback e troque o código
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.
4. Chame a API
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.
5. Renove e desconecte
Tokens de acesso duram 1 hora. Com offline_access você os renova sem o usuário; a conexão em
si dura 30 dias a partir da aprovação e renovar não estende esse prazo, então planeje
reconexões mensais.
tokens = oauth.refresh(refresh_token_guardado)
salvar(tokens["refresh_token"]) # antes de fazer qualquer outra coisa com a resposta
Toda 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(refresh_token_guardado, "refresh_token")
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.
Descoberta e OpenID Connect
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()
Checklist de OAuth
- 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.
Referência por recurso
Todo método abaixo aparece em contexto acima; esta seção é o índice plano.
Autenticação
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.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")
Contas
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.
Usuário autenticado
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.
Documentos
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"], email="signatario@example.com")
# Corpo legado alternativo:
# client.documents.send_token(doc["id"], "signatario@example.com", "email")
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
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
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)
Signatários
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 e whatsapp_phone_number. government_id (o CPF ou CNPJ que
o certificado digital exige) só existe em update(), então crie o signatário e depois complete o
documento dele.
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": "39053344705"},
)
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.
Assignments
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
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.
Documentos do signatário
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.
Definições de campo
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.
Webhooks
client.webhooks.get()
client.webhooks.list_event_types()
client.webhooks.list_dispatches({"delivered": False, "page": 1, "per_page": 20})
# Chamadas mutantes afetam a assinatura única do workspace ou reenviam um evento
# existente. Guarde e restaure a assinatura ao redor de mudanças de teste.
# client.webhooks.register({
# "url": "https://example.com/webhooks/assinafy",
# "email": "admin@example.com",
# "events": ["document_ready", "signer_signed_document"],
# "is_active": True,
# })
# client.webhooks.inactivate()
# client.webhooks.retry_dispatch(dispatch_id)
Um workspace tem uma única assinatura de webhook. Não existe endpoint DELETE documentado — chame
inactivate() para parar a entrega (preserva a URL e os eventos configurados) e register() de
novo para reativar. Como a assinatura é única, register() preenche um events ou is_active
omitido a partir da assinatura atual, para que uma chamada parcial (trocar só a url, por
exemplo) não reative silenciosamente uma assinatura inativada nem colapse uma lista de eventos
customizada. Passe events=[] explicitamente para realmente limpar todos os eventos.
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.
raw_body = request.get_data()
event = client.webhook_verifier.extract_event(raw_body)
event_type = client.webhook_verifier.get_event_type(event) # ex.: "document_ready"
params = client.webhook_verifier.get_event_payload(event) # parâmetros do evento
subject = client.webhook_verifier.get_event_subject(event) # autor (+ "type")
target = client.webhook_verifier.get_event_object(event) # alvo (+ "type")
# get_event_data(event) é um alias retrocompatível de get_event_object(event)
Verificação de assinatura. O Contrato de Entrega documentado especifica o método HTTP, o
Content-Type, as retentativas e o comportamento do circuit breaker, mas não define nenhum header
de assinatura nem esquema de segredo compartilhado. verify() existe apenas para contas que
combinaram separadamente um esquema HMAC-SHA256 com a Assinafy:
signature = request.headers.get("X-Assinafy-Signature", "")
if not client.webhook_verifier.verify(raw_body, signature):
return "Assinatura inválida", 401
Parâmetros de consulta
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.
Formatos de resposta
Endpoints JSON normalmente devolvem {"status": 200, "message": "", "data": ...}; o SDK devolve
data. As cinco chamadas 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"
}
}
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.
Erros
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:", err.errors)
except ApiError as err: # a API devolveu uma resposta não-2xx
print(f"Erro da API {err.status_code}:", err.response_data)
except NetworkError as err: # a requisição nunca chegou à API
print("Erro de rede:", err)
except AssinafyError as err: # formato de resposta inesperado, etc.
print("Erro do SDK:", err, err.context)
Ambientes
| Produção | https://api.assinafy.com.br/v1 |
| Sandbox | https://sandbox.assinafy.com.br/v1 |
O sandbox é gratuito e espelha a produção para testar a integração de ponta a ponta — com duas exceções: as rotas de OAuth existem apenas em produção, e o certificado digital depende de um certificado ICP-Brasil real no dispositivo do signatário.
Desenvolvimento
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 scripts
Teste de fumaça ao vivo
Para 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.py
Para 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.py
O 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. A
mutação de webhook é ignorada a menos que um endpoint de teste explícito seja fornecido; quando
habilitada, a assinatura única anterior do workspace é restaurada. 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.
Checklist de release
- 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.
Documentação
- README.en.md — este mesmo documento, em inglês
- Documentação da API
Licença
Distribuído sob a licença MIT.
Release files for assinafy 1.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| assinafy-1.8.0.tar.gz | 108.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| assinafy-1.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 190.8 kB
Release files / assinafy-1.8.0.tar.gz
| Download URL | assinafy-1.8.0.tar.gz |
|---|---|
| Size | 108.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
520fe38340f3ee01b4a1a73bcff55cb1a7fd87c03cf1311c3cf8ca378d58661a
|
|
BLAKE2b-256 checksum How to use checksums |
ed579ccb3ee662bfeda379581fbefe3e73aeaffacc74cb20787985cc9fa6fbd7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / assinafy-1.8.0-py3-none-any.whl
| Download URL | assinafy-1.8.0-py3-none-any.whl |
|---|---|
| Size | 82.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ecf4620c8078752977a1d56a26364c1154e67d79cba7ad7daf4c3be8ca6c05c5
|
|
BLAKE2b-256 checksum How to use checksums |
8545de3471cd7f5240bfccf81366a21ee45308eee37b6fe58a658d9169d26f73
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log