Português · Read in English
Este é o caminho principal do plugin: método virtual, verificação por e-mail, todos os
signatários em paralelo na etapa 1. Os passos 0–4 rodam através de
SendService::send(). Os links, a sincronização posterior e os
downloads usam o espelho local resultante. Os chamadores do admin, dos hooks, do WooCommerce, da
CLI e do cron compartilham o mesmo serviço de envio.
Os exemplos HTTP mostram a chave de API legada/Sandbox; uma conexão OAuth de produção usa
Authorization: Bearer {ACCESS_TOKEN} nos mesmos endpoints.
O core valida a origem opcional, as credenciais, a seleção de arquivo, as identidades dos
signatários, os canais de notificação, as etapas de assinatura e o prazo antes de enviar
requisições HTTP. Em seguida ele usa DocumentResource::assertUploadable(), que verifica: o
arquivo existe → a extensão é .pdf → ele é legível → o fopen funciona → tamanho > 0 →
tamanho ≤ 25 MB → os primeiros 1024 bytes batem com /%PDF-\d\.\d/ → os últimos 1024 bytes contêm
%%EOF.
A validação local reduz a chance de consumir a franquia de upload com conteúdo malformado. Ela verifica o nome do arquivo e as assinaturas de cabeçalho/rodapé do PDF; o serviço remoto ainda toma a própria decisão e pode rejeitar um arquivo localmente válido. Uma falha de API sem ID de documento pode deixar um resultado incerto, então ela não deve disparar um novo upload automático.
Uma falha na checagem de PDF do SDK retorna WP_Error( 'assinafy_invalid_pdf', … ); a validação de
origem, anexo, signatários e prazo tem seus próprios códigos de erro. Essas falhas não produzem
upload algum.
Um lock nativo de banco de dados serializa cada chave de envio. Uma reserva local e sua origem imutável opcional são salvas antes do upload; o ID remoto do documento é salvo antes do assignment. A chave permanece no espelho depois que seu transient de cinco minutos expira. Registros já atribuídos retornam o ID local existente; envios incompletos buscam novamente o upload salvo e criam um assignment apenas se não houver um. Uma reserva sem ID remoto conhecido para para revisão do operador, porque o resultado do seu upload pode ser desconhecido. A estimativa de custo roda para novas reservas.
POST /v1/documents/{documentId}/assignments/estimate-cost
X-Api-Key: {API_KEY}
Content-Type: application/json
{"method":"virtual","signers":[{"verification_method":"Email","notification_methods":["Email"]}]}{
"status": 200, "message": "",
"data": {
"documents": 1, "credits": 0,
"needs_extra_document": false, "extra_document_cost": 0,
"total_credits": 0, "breakdown": [],
"document_balance": 480, "credit_balance": 0,
"has_sufficient_resources": true, "blocking_reason": null, "message": null
}
}Um signatário notificado por WhatsApp tem preço diferente, e uma conta sem créditos recusa — com HTTP 200:
{
"status": 200, "message": "",
"data": {
"documents": 1, "credits": 0.45,
"total_credits": 0.45,
"breakdown": [{"code":"NotificationWhatsapp","name":"WhatsApp Notification",
"cost":0.45,"quantity":1,"unit_cost":0.45}],
"document_balance": 480, "credit_balance": 0,
"has_sufficient_resources": false, "blocking_reason": "InsufficientCredits",
"message": "A conta não possui créditos suficientes."
}
}Leia has_sufficient_resources mesmo em HTTP 200, e trate também os erros de HTTP/API. O
blocking_reason é um entre PendingPayment, InsufficientDocuments ou InsufficientCredits, e
cada um mapeia para uma mensagem específica ao administrador.
A única recusa que de fato usa um código de status é um método de verificação restrito pelo
plano: DigitalCertificate em uma conta sem o recurso responde 403
"A assinatura com certificado digital não está disponível para o seu plano atual." —
antes de qualquer documento ser consumido, que é exatamente o motivo de a estimativa rodar antes do
upload.
Os ids dos signatários não são necessários aqui: apenas a combinação de métodos é precificada. A
rota, porém, é endereçada a um documento, então o plugin usa o id do documento espelhado mais
recente como substituto — saldo e plano valem para a conta inteira, então qualquer documento da
conta responde à mesma pergunta. Um site que nunca enviou nada não tem esse id e recebe
assinafy_estimate_unavailable, que nunca bloqueia um envio: uma sondagem de preço que não
pôde rodar não é motivo para recusar.
O email é único por conta, então criar um signatário com um endereço que já existe é um 400 duro:
"Um signatário com este e-mail já existe." Uma segunda submissão da mesma pessoa falha se você
não buscar antes.
GET /v1/accounts/{ACCOUNT_ID}/signers?search=jane%40example.com&page=1&per-page=100
X-Api-Key: {API_KEY}O search é uma correspondência por substring sobre full_name OU email, então ele
alegremente retorna jane@example.com.test para jane@example.com. O signers()->findByEmail()
do SDK faz a varredura paginada, exata e sem diferenciar maiúsculas de que isso precisa; o plugin
usa esse método em vez de reimplementar a busca exata.
Quando não encontra:
POST /v1/accounts/{ACCOUNT_ID}/signers
X-Api-Key: {API_KEY}
Content-Type: application/json
{"full_name":"Jane Doe","email":"jane@example.com"}{
"status": 200, "message": "",
"data": {
"resource": "signer",
"id": "104618b9dc17b2b771659e8b1360",
"full_name": "Jane Doe",
"email": "jane@example.com",
"whatsapp_phone_number": null,
"government_id": null,
"has_accepted_terms": false
}
}Regras de campo, verificadas contra a API real:
- Apenas
full_nameé obrigatório. whatsapp_phone_numberé normalizado para E.164 assumindo+55:"48999990000"volta como"+5548999990000"e"+55 48 99999-0001"como"+5548999990001". Curto demais é um 400"Por favor, insira um número de telefone válido."O SDK é mais rigoroso que a API aqui — ele exige localmente um+inicial e o código do país — então o plugin prefixa+55em números nacionais crus de 10 e 11 dígitos antes de repassá-los.government_idaceita um CPF ou um CNPJ no mesmo campo, com a pontuação removida ("390.533.447-05"→"39053344705"), com validação real de dígito verificador ("11111111111"→ 400"CPF inválido.").
POST /v1/accounts/{ACCOUNT_ID}/documents
X-Api-Key: {API_KEY}
Content-Type: multipart/form-data; boundary=…
--boundary
Content-Disposition: form-data; name="file"; filename="contract.pdf"
Content-Type: application/pdf
%PDF-1.4 …
--boundary--{
"status": 200, "message": "",
"data": {
"resource": "document",
"id": "104618b275d321f5de22240ebfda",
"account_id": "{ACCOUNT_ID}",
"template_id": null,
"name": "contract.pdf",
"status": "uploaded",
"artifacts": {
"original": "https://sandbox.assinafy.com.br/v1/documents/104618b275d321f5de22240ebfda/download/original"
},
"is_closed": false,
"signing_url": "https://app-sandbox.assinafy.com.br/sign/104618b275d321f5de22240ebfda",
"decline_reason": null,
"declined_by": null,
"tags": [],
"created_at": "2026-09-13T18:40:10Z",
"updated_at": "2026-09-13T18:40:10Z",
"pages": []
}
}Detalhes que mudam a implementação:
- HTTP 200, não 201. Sem cabeçalho
Location. - O nome da parte multipart precisa ser exatamente
file. Qualquer outro dá 400"O parâmetro \"file\" não está presente." - Um campo de formulário
name=extra é silenciosamente ignorado — o documento adota o nome do arquivo. - O nome do arquivo é normalizado no servidor: diacríticos reduzidos a ASCII, todo caractere
não suportado substituído por um
-."Contrato Ação & Nº2 (final).pdf"vira"Contrato Acao - N-2 -final-.pdf". Letras, dígitos, espaços,.e-sobrevivem. O plugin guarda odata.nameretornado, não o nome que enviou, e é por isso que o título do post pode diferir do nome do arquivo na biblioteca de mídia.
Não há espera por metadata_ready. O passo 4 roda imediatamente. A reserva local é criada antes
do upload, e o id do documento retornado é armazenado antes do assignment.
POST /v1/documents/104618d0d63884bc446c534e5ff5/assignments
X-Api-Key: {API_KEY}
Content-Type: application/json
{
"method": "virtual",
"signers": [
{"id":"19e6b92e7895332ed9708535d8c","verification_method":"Email","notification_methods":["Email"],"step":1},
{"id":"103033c9cd9426bbbb78eccd2c79","verification_method":"Email","notification_methods":["Email"],"step":1}
],
"message": "Please review and sign the attached agreement.",
"expires_at": "2026-12-31T23:59:59Z"
}{
"status": 200, "message": "",
"data": {
"resource": "assignment",
"id": "1a09c15990f0144256b98ff38aa",
"sender_email": "sender@example.com",
"method": "virtual",
"expires_at": "2026-12-31T23:59:59Z",
"message": "Please review and sign the attached agreement.",
"signers": [
{
"id": "19e6b92e7895332ed9708535d8c",
"full_name": "Jane Doe",
"email": "jane@example.com",
"whatsapp_phone_number": null,
"government_id": null,
"has_accepted_terms": true,
"completed": false,
"notification_history": [
{"event":"signature_request","status":"sent","error_code":null,
"error_message":null,"sent_at":"2026-09-13T18:44:16Z","failed_at":null}
],
"verification_method": "Email",
"notification_methods": ["Email"],
"step": 1,
"notified": true
},
{
"id": "103033c9cd9426bbbb78eccd2c79",
"full_name": "John Roe",
"email": "john@example.com",
"whatsapp_phone_number": null,
"government_id": null,
"has_accepted_terms": false,
"completed": false,
"notification_history": [
{"event":"signature_request","status":"sent","error_code":null,
"error_message":null,"sent_at":"2026-09-13T18:44:17Z","failed_at":null}
],
"verification_method": "Email",
"notification_methods": ["Email"],
"step": 1,
"notified": true
}
],
"copy_receivers": [],
"items": [
{
"id": "104618d7fae557b29898ca4475b7",
"page": null,
"signer": {"id":"19e6b92e7895332ed9708535d8c","full_name":"Jane Doe",
"email":"jane@example.com","whatsapp_phone_number":null,
"government_id":null,"has_accepted_terms":true},
"field": {"id":"102d25a48bc7357b93f9b8e01b24","name":"Virtual","type":"virtual",
"regex":null,"is_pre_defined":true,"is_active":true,"is_required":false,
"is_standard":false,"is_read_only":false,"is_visible":true},
"display_settings": [],
"value": null,
"completed": false
}
],
"summary": {
"signer_count": 2,
"completed_count": 0,
"signers": [
{"id":"19e6b92e7895332ed9708535d8c","full_name":"Jane Doe","email":"jane@example.com",
"whatsapp_phone_number":null,"government_id":null,"has_accepted_terms":true,"completed":false},
{"id":"103033c9cd9426bbbb78eccd2c79","full_name":"John Roe","email":"john@example.com",
"whatsapp_phone_number":null,"government_id":null,"has_accepted_terms":false,"completed":false}
]
},
"signing_urls": [
{"signer_id":"19e6b92e7895332ed9708535d8c",
"url":"https://app-sandbox.assinafy.com.br/sign/104618d0d63884bc446c534e5ff5?email=jane%40example.com"},
{"signer_id":"103033c9cd9426bbbb78eccd2c79",
"url":"https://app-sandbox.assinafy.com.br/sign/104618d0d63884bc446c534e5ff5?email=john%40example.com"}
]
}
}Pontos que vale ter em mente ao ler esse payload em outro lugar:
items[].display_settingsé um array JSON[]para virtual e um objeto para collect. A mesma chave muda de tipo; leia-a com acesso por array e valores padrão com??.items[].pageénullpara virtual.- O campo
virtualé anexado automaticamente e nunca aparece na lista de campos da conta. - O plugin não expõe opção de cópia/CC; seu contrato de envio especifica apenas signatários.
assignment.summaryé a leitura de progresso mais barata:{signer_count, completed_count, signers[]}.stepcontrola a ordem. Etapas iguais assinam em paralelo; uma etapa maior só é notificada depois que todas as etapas menores forem concluídas. As etapas precisam ser contíguas a partir de 1, e ou todos os signatários especificam uma, ou nenhum especifica.
O plugin então mescla o assignment sob a chave assignment do documento — o mesmo formato que
GET /documents/{id} retorna — para que o espelho local seja hidratado a partir de um formato só,
e grava o registro.
signing_urls[] = [{ "signer_id": "…", "url": "…" }]
url = https://app.assinafy.com.br/sign/{documentId}?email={urlencoded signer email}
- O segmento do caminho é o id do documento, não o id do assignment e não um token por signatário.
- O único discriminador por signatário é o parâmetro de query
email. - O host é o app web, não o host da API.
- Leia a URL na resposta. Nunca a construa.
signing_urlscontém apenas signatários que já foram notificados. Com etapas sequenciais, os signatários de etapas posteriores têmnotified: falsee ainda não têm URL, e a meta box do documento mostra "ainda não convidado" em vez de um link morto.
O plugin apenas leva às páginas de assinatura hospedadas pela Assinafy. Ele não implementa assinatura embarcada, autenticação de signatário, recusa de signatário nem preenchimento de campos. Ele lê as URLs de assinatura da API e as expõe pelos fluxos documentados do admin e do WooCommerce.
A reconciliação periódica e as dicas opcionais de webhook compartilham o mesmo caminho autoritativo de releitura.
O cron horário de reconciliação (assinafy_reconcile →
StatusSync::reconcile()) pega até 20 registros cujo
_assinafy_is_closed é falso, do _assinafy_synced_at mais antigo em diante, e relê cada um. Ele
para antes da hora quando o orçamento de requisições registrado está abaixo de 30, para deixar
capacidade às requisições em primeiro plano. Isso é um corte consultivo, não uma cota reservada ou
atômica para toda a conta.
Uma entrega de webhook faz exatamente a mesma coisa — veja a §7. O corpo da entrega nunca é gravado no estado local; ele é uma dica que dispara a mesma releitura.
Ambos passam por uma única chamada:
GET /v1/documents/104618b275d321f5de22240ebfda
X-Api-Key: {API_KEY}
Accept: application/json{
"status": 200, "message": "",
"data": {
"resource": "document",
"id": "104618b275d321f5de22240ebfda",
"account_id": "{ACCOUNT_ID}",
"template_id": null,
"name": "contract.pdf",
"status": "pending_signature",
"artifacts": {
"original": "https://sandbox.assinafy.com.br/v1/documents/104618b2…/download/original",
"thumbnail": "https://sandbox.assinafy.com.br/v1/documents/104618b2…/thumbnail"
},
"is_closed": false,
"signing_url": "https://app-sandbox.assinafy.com.br/sign/104618b275d321f5de22240ebfda",
"decline_reason": null,
"declined_by": null,
"tags": [],
"created_at": "2026-09-13T18:40:10Z",
"updated_at": "2026-09-13T18:40:12Z",
"assignment": {
"id": "1a09c15990f0144256b98ff38aa", "method": "virtual",
"sender_email": "sender@example.com", "expires_at": "2026-12-31T23:59:59Z",
"message": "Please review and sign.", "copy_receivers": [], "items": [],
"signers": [{"id":"19e6b92e7895332ed9708535d8c","full_name":"Jane Doe",
"email":"jane@example.com","completed":false,"step":1,"notified":true}],
"summary": {"signer_count":1,"completed_count":0,"signers":[]},
"signing_urls": [{"signer_id":"19e6b92e7895332ed9708535d8c",
"url":"https://app-sandbox.assinafy.com.br/sign/104618b2…?email=jane%40example.com"}]
},
"pages": [{"id":"104618b2ac6a3b2f99d0fd2c5f43","number":1,"height":1755,"width":1240,
"download_url":"https://sandbox.assinafy.com.br/v1/documents/104618b2…/pages/104618b2…/download"}]
}
}Uma única chamada recupera tudo sobre uma solicitação de assinatura. Não existe endpoint de
detalhe do assignment, e ?expand=assignment não faz nada — assignment e pages sempre vêm
embutidos.
A tela de edição do documento também renderiza o feed de atividades daquele documento, direto da API, e é por isso que o plugin não mantém tabela local de atividades:
GET /v1/documents/104618b275d321f5de22240ebfda/activities
X-Api-Key: {API_KEY}{
"status": 200, "message": "",
"data": [
{ "id": 17172, "event": "signature_requested",
"message": "Solicitação para assinar enviada para Jane Doe <jane@example.com>.",
"payload": { "signer_email": "jane@example.com", "signer_full_name": "Jane Doe",
"notification_method": "email", "signer_whatsapp_phone_number": null },
"origin": null, "created_at": "2026-07-28T19:33:56Z" },
{ "id": 17171, "event": "assignment_created",
"message": "Solicitação de assinatura criada por Acme Test.",
"payload": { "user_name": "Acme Test", "user_email": "owner@example.com", "user_telephone": null },
"origin": { "ip": "203.0.113.20", "user-agent": null },
"created_at": "2026-07-28T19:33:56Z" },
{ "id": 17169, "event": "document_metadata_ready",
"message": "Documento processado.", "payload": [], "origin": null,
"created_at": "2026-07-28T19:33:53Z" },
{ "id": 17168, "event": "document_uploaded",
"message": "Documento criado.", "payload": [],
"origin": { "ip": "203.0.113.20", "user-agent": null },
"created_at": "2026-07-28T19:33:50Z" }
]
}Mais recentes primeiro, sem paginação alguma (page e per-page são ignorados), message é prosa
pt-BR já renderizada, origin é null para eventos do sistema. payload é um array JSON []
quando o evento não tem chaves de payload e um objeto quando tem — o que em PHP é [] versus
['signer_email' => …]. Código tipado como objeto quebra em document_uploaded,
document_metadata_ready e document_prepared; a meta box lê o valor como ['key'] ?? null.
Assim que o documento chega a certificated, seu mapa artifacts ganha certificated,
certificate-page e bundle (mais pades quando um signatário ICP-Brasil participou). A UI do
documento linka os artefatos disponíveis a partir desse mapa, excluindo thumbnail. O proxy
serve apenas original, certificated, certificate-page, pades e bundle.
GET /v1/documents/104618b275d321f5de22240ebfda/download/certificated
X-Api-Key: {API_KEY}HTTP/2 200
content-type: application/pdf
content-length: 1146
content-disposition: attachment; filename="contract.pdf"
cache-control: must-revalidate, post-check=0, pre-check=0
accept-ranges: bytes
%PDF-1.4 …A API devolve os bytes do artefato diretamente. O
DownloadProxy os busca no servidor usando a chave de API da
conta, nesta ordem:
current_user_can( 'assinafy_view', $post_id )check_admin_referer()- o nome do artefato é resolvido contra os nomes armazenados no registro
documents()->download()- cabeçalhos, bytes,
exit
O proxy envia artefatos PDF como application/pdf e bundles como application/zip, com a extensão
correspondente, cabeçalhos de no-cache e X-Content-Type-Options: nosniff.
A capability vem primeiro, deliberadamente: um visitante que não pode ver o registro é barrado antes que a resposta revele se o registro existe.
Sondar a API em busca de artefatos disponíveis não diz nada. Todo nome indisponível e todo nome inventado retornam a resposta idêntica:
{"status":404,"data":null,"message":"Artefato não está disponível."}Não há como distinguir "ainda não gerado" de "não existe esse artefato", e é por isso que a UI de
download é guiada pelo mapa artifacts e o proxy responde com um único 404 para nome errado,
artefato ausente, registro desconhecido e credenciais ausentes, tudo igual — nada sobre o estado do
documento vaza por um link de download.
thumbnail é uma chave no mapa artifacts, mas não é um nome válido na rota de download; ele
tem o próprio endpoint. A lista de permitidos do proxy o exclui.