Português · Read in English
Acesso nativo em Swift e Objective-C à API Assinafy v1 — plataforma brasileira de assinatura eletrônica: documentos, signatários, assignments, campos, tags, templates, workspaces, webhooks e autenticação.
O SDK cobre todas as operações do documento OpenAPI v1 publicado. As requisições são
async/await primeiro, com wrappers de completion handler para Objective-C. As respostas
decodificam em modelos tipados, e as falhas aparecem como quatro tipos distintos de erro Swift que
fazem bridge limpo para NSError.
Sem dependências de terceiros: apenas Foundation e CryptoKit.
- Requisitos
- Instalação
- Início rápido
- Configurando o cliente
- As quatro formas de autenticar
- OAuth 2.1 com PKCE
- Como as credenciais são enviadas
- O ciclo de vida da assinatura
- Métodos de verificação do signatário
- O lado do signatário
- Templates
- Tags, campos e JSON não tipado
- Workspaces e webhooks
- Paginação
- Tratamento de erros
- Transporte e segurança de rede
- Objective-C
- Mapa de recursos
- Ambientes
- Testes
- Versionamento
| iOS | 16.0+ |
| macOS | 12.0+ |
| Swift | 6.3+ (modo de linguagem 6) |
| Xcode | 26.6+ |
| TLS | 1.2+ (o SDK recusa TLS 1.0 e 1.1) |
Swift e Xcode não têm canal de release LTS. Estas são as versões estáveis atuais da toolchain contra as quais este pacote é compilado e testado.
Adicione o pacote e o produto ao Package.swift:
dependencies: [
.package(
url: "https://github.com/assinafy/mobile-ios-sdk.git",
from: "1.8.0"
),
],
targets: [
.target(
name: "SeuApp",
dependencies: [
.product(name: "Assinafy", package: "mobile-ios-sdk"),
]
),
]No Xcode, escolha File → Add Package Dependencies… e informe a URL do repositório.
Enviar um PDF para um signatário:
import Assinafy
let client = AssinafyClient(token: bearerToken, defaultAccountId: accountId)
let enviado = try await client.documents.upload(pdfData)
let documento = try await client.documents.waitUntilReady(documentId: enviado.id)
let signatario = try await client.signers.create(
CreateSignerPayload(fullName: "Ana Souza", email: "ana@exemplo.invalid")
)
let assignment = try await client.assignments.create(
documentId: documento.id,
payload: .withSignerIds([signatario.id], method: .virtual, message: "Por favor, revise e assine.")
)Crie um cliente e mantenha uma referência forte a ele. AssinafyClient é thread-safe e seus
objetos de recurso são visões sem estado sobre um transporte compartilhado.
import Assinafy
let configuration = AssinafyClientConfiguration(
token: bearerToken, // ou apiKey:, nunca os dois
baseURL: AssinafyClientConfiguration.productionBaseURL,
defaultAccountId: accountId,
timeout: 30,
logger: NoopLogger()
)
try configuration.validate()
let client = AssinafyClient(configuration: configuration)defaultAccountId é aplicado a toda chamada com escopo de conta. Qualquer método que receba um
argumento accountId o sobrescreve naquela chamada.
A configuração falha fechada. validate() rejeita credenciais conflitantes ou malformadas,
timeout não positivo ou não finito, identificador inseguro, e qualquer URL base que não seja uma
URL HTTPS absoluta livre de informação de usuário, query e fragmento. As mesmas checagens rodam em
toda requisição, então um cliente construído sem chamar validate() lança ValidationError antes
de fazer I/O de rede, em vez de enviar uma requisição malformada.
Algumas operações não precisam de credencial alguma. Deixe as duas de fora para login, redefinição de senha, as rotas públicas de documento e a troca de tokens OAuth:
let publicClient = AssinafyClient(configuration: AssinafyClientConfiguration())
let session = try await publicClient.auth.login(
LoginPayload(email: email, password: password)
)| Forma | Quem é autenticado | Onde usar |
|---|---|---|
Chave de API (X-Api-Key) |
O workspace, permanentemente | Back-end confiável |
Token bearer (Authorization) |
O usuário que fez login | App móvel distribuído |
OAuth 2.1 (Authorization) |
Um app agindo em nome de um usuário, com escopos que ele aprovou | Integrações de terceiros, assistentes de IA |
| Código de acesso do signatário | Um signatário, para um documento | O lado de quem assina |
Escolha a credencial que combina com onde o código roda. Uma chave de API permanente concede acesso permanente a todo o workspace e pertence a um back-end confiável — nunca a um bundle de app, de onde pode ser extraída. Tokens bearer e tokens OAuth pertencem a apps móveis.
OAuth é a escolha certa quando o código não é o dono do workspace: o usuário aprova um conjunto nomeado de permissões numa tela de consentimento da Assinafy, e o token resultante fica limitado a elas e a um único workspace. Independentemente dos escopos, um token OAuth nunca alcança cobrança, ciclo de vida da conta, gestão de credenciais ou superfícies administrativas.
Fluxo de authorization code com PKCE S256 obrigatório. Um app móvel é um cliente público: autentica-se por PKCE e nunca recebe um client secret — um segredo embarcado no bundle pode ser extraído dele.
Disponível apenas em produção. O host de sandbox ainda não expõe os endpoints OAuth.
| Escopo | Concede |
|---|---|
documents:read |
Ler documentos, páginas, tags, signatários, assignments e atividades |
documents:write |
Criar, atualizar e excluir documentos e gerenciar seus signatários e assignments |
templates:read |
Ler templates, suas páginas, papéis, campos e tags |
templates:write |
Criar, atualizar e excluir templates e seu conteúdo |
account:read |
Ler o perfil, o tema e o logo do workspace |
webhooks:write |
Configurar e desativar a assinatura de webhooks do workspace |
openid |
Identificar o usuário autenticado e habilitar /oauth/userinfo |
profile |
Incluir o nome do usuário nas claims |
email |
Incluir o e-mail e seu status de verificação nas claims |
offline_access |
Receber um refresh token, para continuar funcionando sem novo consentimento |
Passe OAuthScope.webhooksWrite pelo inicializador scopeStrings de OAuthAuthorizationRequest ao solicitar acesso a webhooks.
O usuário aprova tudo o que foi pedido ou nada: peça o mínimo e leia scope na resposta do token
para saber o que foi concedido. Uma chamada sem o escopo que exige responde 403 com
WWW-Authenticate: Bearer error="insufficient_scope", scope="…", e APIError.insufficientScope
devolve esse escopo: peça ao usuário para conectar de novo incluindo-o, porque repetir a chamada não
resolve. Um 403 sem esse desafio indica outro workspace, o papel do próprio usuário ou uma área que
tokens OAuth nunca alcançam.
let recurso = try await client.oauth.protectedResourceMetadata()
let servidor = try await client.oauth.authorizationServerMetadata(
issuer: recurso.authorizationServers[0]
)A descoberta é opcional — OAuthResource.defaultAuthorizationEndpoint e defaultIssuer trazem os
valores de produção — mas os metadados publicados são a fonte autoritativa, e ambas as rotas são
públicas e não carregam credencial alguma.
let pedido = OAuthAuthorizationRequest(
clientId: clientId,
redirectURI: "https://meuapp.exemplo.invalid/oauth/callback",
scopes: [.documentsRead, .documentsWrite, .offlineAccess],
resource: recurso.resource
)
let url = pedido.authorizationURL(endpoint: servidor.authorizationEndpoint)!OAuthAuthorizationRequest já gera o par PKCE e um state imprevisível. Guarde pedido em
memória até a troca do código: ele carrega o codeVerifier, que nunca vai para a URL, para o
disco ou para um log.
O redirectURI precisa ser um endereço https:// cadastrado na aplicação, idêntico caractere a
caractere: a Assinafy não aceita esquema próprio (meuapp://) nem http://localhost. Abra url
numa ASWebAuthenticationSession; no iOS 17.4+ e no macOS 14.4+, callback: .https(host:path:)
recebe o retorno num host dos domínios associados do app. Em versões anteriores, a página https://
cadastrada pode repassar o retorno inteiro para o esquema próprio do app.
let codigo = try OAuthCallback(callbackURL: callbackURL)!
.validate(against: pedido, issuer: servidor.issuer)validate(against:issuer:) confere antes de tudo o state, em tempo constante, e o parâmetro iss
(RFC 9207) — tanto na aprovação quanto num retorno de erro. Um state ou iss divergente significa
que o redirect não veio do fluxo que este app iniciou, e nada nele é usado. Só então o método lança o
erro relatado pelo servidor (access_denied, invalid_scope, …) ou devolve o código. Sem issuer:,
o iss é comparado com OAuthResource.defaultIssuer.
let token = try await client.oauth.exchangeAuthorizationCode(
.authorizationCode(
code: codigo,
redirectURI: pedido.redirectURI,
codeVerifier: pedido.pkce.codeVerifier,
clientId: clientId,
resource: recurso.resource
)
)
var clienteDoUsuario = AssinafyClient(token: token.accessToken)
let workspaceId = try await clienteDoUsuario.workspaces.list().data[0].idO código vale uma única vez e expira 60 segundos depois da aprovação: troque-o na hora e, se a troca
falhar, comece uma nova autorização — o SDK nunca repete a requisição. Com um token OAuth,
workspaces.list() devolve exatamente o workspace que o usuário escolheu. Guarde workspaceId junto
dos tokens e use-o como defaultAccountId ou accountId:: uma conexão vale para um único
workspace, e qualquer outro responde 403, mesmo que o usuário pertença a ele.
// Grave os tokens assim que chegarem, junto de `workspaceId`.
try gravarTokens(token)
// Quando o access token expirar, ou uma chamada responder 401: envie o último refresh token gravado.
let enviado = try refreshTokenGravado()
let novo = try await client.oauth.refreshAccessToken(
.refreshToken(enviado, clientId: clientId)
)
try gravarTokens(novo) // antes de tudo: `enviado` já está aposentado
clienteDoUsuario = AssinafyClient(token: novo.accessToken, defaultAccountId: workspaceId)
// Quando o usuário desconectar: revogue o último refresh token gravado e apague os tokens.
let ultimo = try refreshTokenGravado()
try await client.oauth.revoke(OAuthRevokePayload(token: ultimo, clientId: clientId))
try apagarTokens()gravarTokens, refreshTokenGravado e apagarTokens representam o armazenamento do próprio app
no Keychain. Um cliente guarda o token com que foi criado, então crie um novo a cada access token
renovado.
refreshToken só existe quando offline_access foi pedido e consentido; sem ele, conduza o
usuário pelo fluxo de autorização novamente quando o access token expirar, e revogue o access token
quando ele desconectar.
Cada renovação devolve um novo refreshToken e aposenta o enviado, e reutilizar um refresh
token aposentado encerra a conexão inteira: todos os tokens param de funcionar e o usuário precisa
conectar de novo. Por isso, grave os novos tokens antes de fazer qualquer outra coisa com a
resposta, envie sempre o último refresh token gravado e faça uma renovação por vez em cada conexão.
Uma resposta de sucesso sem um novo refresh token lança AssinafySDKError. O SDK envia cada
requisição de token uma única vez e não segue redirects: um 3xx chega como APIError.
Quando uma renovação falha sem um erro OAuth — timeout, conexão perdida, cancelamento, 3xx ou
5xx, resposta sem novo refresh token —, o servidor pode já ter aposentado o token enviado, porque
uma resposta perdida é indistinguível de uma requisição que nunca chegou. Releia o refresh token
gravado e só continue se um token diferente tiver sido gravado desde então. Se ainda for o token
enviado, nunca o envie de novo: peça ao usuário para conectar de novo. Só podem ser repetidas as
falhas que comprovadamente aconteceram antes de a requisição sair: um NetworkError cujo
underlyingError é um URLError com código .cannotFindHost, .dnsLookupFailed,
.cannotConnectToHost, .secureConnectionFailed ou um dos códigos .serverCertificate….
Um refresh token vale 30 dias, e cada renovação devolve um novo com mais 30 dias: a conexão só
expira se o app passar 30 dias sem renovar. Um 401 da API significa token expirado ou revogado:
renove uma vez e, se falhar, peça ao usuário para conectar de novo. invalid_grant na renovação
significa que a conexão acabou (token já usado ou expirado, acesso revogado, ou nova aprovação com
outras permissões) — peça para conectar de novo em vez de repetir.
Ao desconectar, revogue o último refresh token gravado (ou o access token, quando não houver um):
todos os anteriores já estão aposentados. A revogação sempre responde 200 para qualquer desfecho
de token — inclusive um token inexistente, já revogado ou malformado — de modo que o endpoint não
possa ser usado para descobrir se um token existe.
let claims = try await clienteDoUsuario.oauth.userInfo()
print(claims.sub, claims.name as Any, claims.email as Any)Exige openid; name exige profile e email exige email.
Erros OAuth chegam como APIError com um oauthError tipado:
do {
_ = try await client.oauth.exchangeAuthorizationCode(payload)
} catch let error as APIError {
switch error.oauthError?.code {
case "invalid_grant": break // código expirado, reutilizado, ou verifier errado
case "invalid_client": break // client_id desconhecido ou desabilitado
case "invalid_target": break // `resource` divergente do autorizado
default: break
}
}A API v1 coloca cada operação em uma de três classes de autenticação, e o SDK impõe essa separação em vez de anexar credenciais indiscriminadamente.
| Classe | Como a API autentica | O que o SDK envia |
|---|---|---|
| Conta | Authorization: Bearer {token} ou X-Api-Key: {chave} |
O header da credencial configurada |
| Signatário | O parâmetro de query exato signer-access-code={código} |
Apenas o parâmetro de query |
| Público | Nada | Nada |
Um cliente configurado com token bearer ou chave de API não o transmite a uma operação de
Signatário ou Pública. Isso significa que um mesmo cliente pode servir os dois lados de um fluxo: a
mesma instância pode gerenciar documentos com uma credencial de conta e conduzir um signatário por
assignments.sign(…) sem que aquela credencial saia das rotas que a aceitam.
A regra vale também por origem: uma requisição cuja URL não compartilha a origem da URL base — como a descoberta OAuth no servidor de autorização — perde os headers de credencial no transporte, qualquer que seja a configuração do cliente.
Códigos de acesso são segredos. Trate um signerAccessCode exatamente como trataria uma senha:
nunca registre em log, nunca persista além da sessão de assinatura, e nunca o coloque em uma URL
que você compartilhe.
Um pedido de assinatura passa por cinco etapas. Cada uma tem uma chamada dedicada.
enviar ──▶ aguardar processamento ──▶ criar signatários ──▶ estimar ──▶ criar assignment
let enviado = try await client.documents.upload(
pdfData,
options: DocumentUploadOptions(accountId: accountId)
)O SDK confere os magic bytes do PDF e o limite de 25 MB da plataforma localmente, então um arquivo
inválido falha imediatamente com ValidationError em vez de gastar uma ida ao servidor.
Um documento recém-enviado está em metadata_processing e ainda não pode receber assignment nem
ser excluído:
let documento = try await client.documents.waitUntilReady(
documentId: enviado.id,
options: WaitUntilReadyOptions(maxWaitSeconds: 60, pollIntervalSeconds: 2)
)waitUntilReady retorna assim que o documento alcança metadataReady, pendingSignature,
certificating ou certificated. Lança AssinafySDKError num status terminal (failed,
expired, rejectedBySigner, rejectedByUser) ou quando o prazo expira, e honra cancelamento de
tarefa do início ao fim.
let signatario = try await client.signers.create(
CreateSignerPayload(fullName: "Ana Souza", email: "ana@exemplo.invalid")
)A criação é idempotente por e-mail: o SDK procura um signatário existente com o mesmo endereço e o
devolve em vez de criar um duplicado, e se recupera da mesma forma de um 409. Signatários também
podem ser criados apenas com o nome completo.
Pedidos de assinatura consomem documentos e créditos. Confira antes de se comprometer:
let pedido = CreateAssignmentPayload.withSignerIds(
[signatario.id],
method: .virtual,
message: "Por favor, revise e assine."
)
let estimativa = try await client.assignments.estimateCost(
documentId: documento.id,
payload: pedido
)
guard estimativa.hasSufficientResources else {
throw AssinafySDKError(estimativa.blockingReason ?? "Recursos insuficientes na conta")
}CostEstimate traz o breakdown tipado, os saldos documentBalance e creditBalance, e um
blockingReason entre PendingPayment, InsufficientDocuments e InsufficientCredits.
let assignment = try await client.assignments.create(
documentId: documento.id,
payload: pedido
)Use .virtual para assinatura remota por notificação, e .collect para coleta presencial de
campos, que exige também as posições dos campos nas páginas. Para assinatura ordenada, construa os
signatários com SignerReference.signer(id:verification:notifications:step:) e dê a cada um o seu
step.
let progresso = try await client.documents.getSigningProgress(documentId: documento.id)
let concluido = try await client.documents.isFullySigned(documentId: documento.id)
let historico = try await client.documents.activities(documentId: documento.id)
let pdfAssinado = try await client.documents.downloadArtifact(
documentId: documento.id,
artifact: .certificated
)As atividades devolvem todos os eventos registrados, cada um com um snapshot do payload do evento
e a origin da requisição (ip, user-agent).
| 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 |
Miniaturas e páginas isoladas têm métodos próprios. Quem tiver o hash da assinatura confere um documento finalizado sem autenticação alguma:
let verificacao = try await client.documents.verifyDetails(signatureHash: hash)uploadAndRequestSignatures executa a mesma orquestração do lado do dono:
let opcoes = AssinafyClient.UploadOptions(signers: [
AssinafyClient.SignerInput(name: "Ana Souza", email: "ana@exemplo.invalid"),
])
opcoes.message = "Por favor, revise e assine."
let (documento, assignment) = try await client.uploadAndRequestSignatures(
documentData: pdfData,
options: opcoes
)Valida cada signatário — nomes não vazios, endereços bem formados, nenhum e-mail duplicado ignorando maiúsculas — antes de enviar o arquivo, de modo que entrada inválida não deixe um documento órfão para trás. As etapas remotas não são transacionais: se uma etapa posterior falhar, o documento e os signatários já criados continuam disponíveis para nova tentativa ou limpeza explícita.
Definidos por signatário ao criar o assignment. O método de verificação e o de notificação são
acoplados: envie um, os dois ou nenhum — o lado que faltar é inferido. Sem nenhum dos dois,
ambos assumem Email.
| Método | Como funciona | Custo por signatário |
|---|---|---|
.email (padrão) |
Código de uso único (OTP) por e-mail, exigido antes de assinar | Gratuito |
.whatsapp |
Código de uso único (OTP) por WhatsApp | Verificação gratuita; notificação 0,45 crédito, só em planos pagos |
.digitalCertificate |
O signatário assina com o próprio certificado ICP-Brasil (A1/A3), pela extensão de navegador Web PKI, gerando uma assinatura PAdES qualificada | 2 créditos |
let pedido = CreateAssignmentPayload(
method: .virtual,
signers: [
.signer(id: primeiro.id, verification: .email, step: 1),
.signer(id: segundo.id, verification: .digitalCertificate, step: 2),
]
)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.
A1 e A3 são os dois formatos de certificado ICP-Brasil: A1 é um arquivo de software guardado na máquina do signatário, A3 vive num cartão inteligente ou token USB. Ambos são apresentados pela mesma extensão Web PKI, então a escolha entre eles é do signatário e não exige nada da integração.
O certificado digital exige o recurso habilitado na conta (planos Standard e Pro), CPF ou CNPJ em
government_id, e que o signatário esteja sozinho no seu passo. Um CPF exige o certificado
daquela pessoa — um e-CPF, ou um e-CNPJ que a nomeie como representante legal; um CNPJ exige um
e-CNPJ daquela empresa, de qualquer um de seus representantes.
Como a assinatura por certificado acontece por um handshake com a extensão de navegador Web PKI, ela não é concluída pelo fluxo de assinatura nativo deste SDK — encaminhe o signatário à página web de assinatura.
Ao ler um signatário de volta, signatario.verification e signatario.notifications devolvem os
valores tipados, e nil ou uma lista reduzida quando o servidor manda algo que esta versão do SDK
não conhece.
Um signatário chega com um código de acesso vindo do convite. A sequência completa é: ler o documento, aceitar os termos, confirmar os dados de identidade e então assinar.
func assinarAssignmentVirtual(
client: AssinafyClient,
documentId: String,
assignmentId: String,
accessCode: String,
nomeCompleto: String,
email: String
) async throws {
_ = try await client.signers.getSelf(signerAccessCode: accessCode)
try await client.signers.acceptTermsWithoutResponse(signerAccessCode: accessCode)
_ = try await client.documents.confirmSignerDataAndReturnSigner(
documentId: documentId,
signerAccessCode: accessCode,
payload: ConfirmSignerDataPayload(
fullName: nomeCompleto,
email: email,
hasAcceptedTerms: true
)
)
try await client.assignments.sign(
documentId: documentId,
assignmentId: assignmentId,
signerAccessCode: accessCode
)
}Confirmar os dados do signatário é obrigatório em assignments virtuais; assinar sem isso devolve
400. O signatário pode, em vez disso, recusar:
try await client.assignments.decline(
documentId: documentId,
assignmentId: assignmentId,
signerAccessCode: accessCode,
reason: "Destinatário incorreto"
)Signatários com vários documentos podem agir sobre todos de uma vez com
signers.signMultipleDocuments(…) e signers.declineMultipleDocuments(…), e listar os próprios
documentos com signers.listSignerDocuments(…) e signers.searchSignerDocuments(…).
Imagens de assinatura e rubrica são PNG:
try await client.signers.uploadSignature(
signerAccessCode: accessCode,
type: .signature,
imageData: pngData,
reuse: true
)Quando o link de assinatura carrega apenas um código de acesso e nenhum ID de signatário, use
signers.getSigningDocument(signerAccessCode:).
Antes de o signatário verificar seu código, um app pode mostrar um resumo público e enviar o token de seis dígitos:
let info = try await client.documents.getPublicInfo(documentId: documentId)
try await client.documents.sendPublicSignToken(
documentId: documentId,
email: "ana@exemplo.invalid"
)Crie documentos a partir de um template reutilizável mapeando papéis para signatários:
let templates = try await client.templates.list(
params: TemplateListParams(search: "Contratação")
)
let documento = try await client.documents.createFromTemplate(
templateId: templateId,
signers: [TemplateSigner(roleId: roleId, id: signatario.id)],
options: CreateDocumentFromTemplateOptions(name: "Carta proposta")
)documents.estimateCostFromTemplate(…) precifica o mesmo pedido antes.
TemplateSigner também aceita métodos tipados:
TemplateSigner(roleId: roleId, id: signatario.id, verification: .whatsapp)A listagem de templates e a criação de documentos a partir deles são operações v1. A gestão da
definição de templates — templates.create(name:pdfData:), get, update e delete — existe na
API ao vivo mas não consta do documento OpenAPI publicado; confirme a disponibilidade no seu
ambiente antes de depender dela.
let tag = try await client.tags.create(
CreateTagPayload(name: "Contratos", color: "ff8800")
)
_ = try await client.tags.appendDocumentTags(documentId: documentId, tagIds: [tag.id])tags.appendDocumentTags acrescenta às tags de um documento; tags.replaceDocumentTags define
exatamente o conjunto, e passar um array vazio limpa todas.
Definições de campo descrevem valores coletados dos signatários. A validação aceita uma String
simples, ou um JSONValue quando o campo recebe outro tipo JSON:
let resultado = try await client.fields.validate(
fieldId: fieldId,
value: JSONValue(.integer(42))
)
let lote = try await client.fields.validateMultiple(items: [
FieldValidateMultipleItem(fieldId: fieldId, value: "2026-08-27"),
])Campos de resposta cujo schema aceita qualquer tipo JSON expõem um JSONValue sem perdas ao lado
da visão legada em string:
DocumentActivity.originJSONepayloadJSONAssignmentItem.valueJSONTemplateFieldPlacement.displaySettingsJSONWebhookDispatch.payloadJSON
client.workspaces mapeia os endpoints de conta: criar, ler, atualizar, excluir, tema de marca,
estatísticas do funil de documentos e o logo da conta.
let tema = try await client.workspaces.theme()
try await client.workspaces.uploadLogo(pngData)theme() é a fonte canônica das cores de marca de uma conta.
Webhooks entregam eventos do ciclo de vida do documento:
let assinatura = try await client.webhooks.register(
WebhookRegisterPayload(
url: "https://exemplo.invalid/hooks/assinafy",
email: "ops@exemplo.invalid",
events: ["document.completed"]
)
)
let tipos = try await client.webhooks.listEventTypes()
let tentativas = try await client.webhooks.listDispatches()
try await client.webhooks.retryDispatch(dispatchId: tentativas.data[0].id)A API não tem exclusão destrutiva de assinaturas. Interrompa a entrega com webhooks.inactivate();
webhooks.delete() está descontinuado e encaminha para ele.
Operações de listagem devolvem PaginatedResult<T>. data traz a página; meta é montado a
partir dos headers X-Pagination-* e é nil quando o servidor os omite.
var pagina = 1
var todos: [DocumentListItem] = []
repeat {
let resultado = try await client.documents.list(params: ListParams(page: pagina, perPage: 100))
todos += resultado.data
guard let meta = resultado.meta, pagina < meta.lastPage else { break }
pagina += 1
} while trueO SDK lança exatamente quatro tipos, mais CancellationError:
| Tipo | Significado | Detalhe principal |
|---|---|---|
APIError |
A API devolveu um status fora do 2xx | statusCode, message, responseData, oauthError |
ValidationError |
Validação local falhou antes de qualquer requisição | mapa errors por campo |
NetworkError |
Falha de transporte — DNS, TLS, timeout | underlyingError como URLError |
AssinafySDKError |
Violação de contrato do SDK | context, underlyingError |
do {
_ = try await client.documents.get(documentId: documentId)
} catch let error as APIError {
print(error.statusCode, error.message, error.responseData as Any)
for restricao in error.workspaceDeletionRestrictions {
print(restricao.code, restricao.accountIds)
}
} catch let error as ValidationError {
print(error.message, error.errors)
} catch let error as NetworkError {
print(error.message, error.underlyingError as Any)
} catch is CancellationError {
// O cancelamento de tarefa é preservado, não convertido.
}As requisições passam por uma URLSession efêmera com cookies e cache de URL desligados, de modo
que o SDK não grava credencial nem resposta em disco.
Redirects são restritos. POST /oauth/token e POST /oauth/revoke não seguem nenhum: o corpo
carrega um código ou token, então um 3xx chega como APIError em vez de enviá-lo de novo. Os
demais redirects de mesma origem são seguidos sem alteração. Um redirect para outra origem só é
aceito quando é HTTPS, o método é GET ou HEAD e não há corpo — o que cobre downloads de
artefatos servidos por outro host. Nesse redirect sobrevivem apenas Accept,
Accept-Encoding, Accept-Language, Range, If-Range e User-Agent; Authorization,
X-Api-Key, Cookie e todo header desconhecido são removidos. Downgrades para HTTP, URLs de
destino com informação de usuário e redirects para outra origem carregando corpo são recusados de
saída.
Os métodos compatíveis com Objective-C recebem completion handlers, sempre entregues na main queue. O header gerado é a fonte da verdade dos seletores; nem todo auxiliar de concorrência Swift tem um wrapper de completion.
ASFAssinafyClient *client = [[ASFAssinafyClient alloc]
initWithToken:bearerToken
defaultAccountId:accountId];
[client.signers getSignerWithId:@"signer-id"
accountId:nil
completion:^(Signer *signer, NSError *error) {
if (error != nil) {
NSLog(@"A requisição falhou: %@", error.localizedDescription);
return;
}
NSLog(@"ID do signatário: %@", signer.id);
}];Erros Swift fazem bridge para NSError sob os domínios de ASFErrorDomain. code carrega o
status HTTP em APIError, 422 em ValidationError, o código de URLError em NetworkError e
-1 em AssinafySDKError; os detalhes chegam em userInfo sob responseData, errors e
NSUnderlyingErrorKey. Um APIError também traz o desafio WWW-Authenticate bruto sob
wwwAuthenticate e, num 403 insufficient_scope, o escopo que falta sob insufficientScope:
NSString *escopo = error.userInfo[@"insufficientScope"];
if (escopo != nil) {
// Peça ao usuário para conectar de novo, solicitando também `escopo`.
}| Recurso | Cobertura |
|---|---|
client.auth |
Login, login social e vínculo, operações de senha, gestão de chave de API, usuário atual, preferências de notificação, estatísticas |
client.oauth |
Descoberta, URL de autorização com PKCE, troca de código, renovação, revogação, userinfo |
client.workspaces |
CRUD de conta, tema, estatísticas, upload/download/exclusão de logo |
client.documents |
Envio, listar/buscar/obter/renomear/excluir, status de processamento, páginas, miniaturas, atividades, artefatos, verificação, criação a partir de template, fluxo público de token |
client.signers |
CRUD de signatários do workspace, autoatendimento, termos, verificação, imagens de assinatura, documentos do signatário, assinatura e recusa em lote |
client.assignments |
Listar, criar, estimar, assinar, recusar, reenviar, expiração, notificações WhatsApp |
client.fields |
Definições, tipos de campo, validação simples e em lote |
client.tags |
Tags do workspace e vínculos com documentos |
client.templates |
Listagem de templates e gestão de definições |
client.webhooks |
Assinaturas, tipos de evento, histórico de entrega, reenvio |
docs/API_REFERENCE.md documenta, para cada método, a classe de
autenticação, o caminho HTTP exato, o payload de requisição, o payload de resposta, o
comportamento de compatibilidade e o modelo de erro.
| Produção | AssinafyClientConfiguration.productionBaseURL |
| Sandbox | https://sandbox.assinafy.com.br/v1 |
Os endpoints OAuth existem apenas em produção.
Rode a suíte com concorrência Swift 6 e warnings como erro, depois um build de release:
swift test -Xswiftc -strict-concurrency=complete -Xswiftc -warnings-as-errors
swift build -c release -Xswiftc -strict-concurrency=complete -Xswiftc -warnings-as-errorsA CI roda ainda o XCTest num simulador iOS 26.5 / iPhone 17 Pro sob Xcode 26.6, compila a DocC com
warnings como erro, verifica compatibilidade da API pública contra a tag anterior mais próxima, e
confere que a tag de release, sdkVersion, o changelog e este arquivo concordam.
Os testes ao vivo leem credenciais apenas do ambiente e recusam qualquer host que não seja o sandbox:
ASSINAFY_API_KEY="..." \
ASSINAFY_ACCOUNT_ID="..." \
ASSINAFY_BASE_URL="https://sandbox.assinafy.com.br/v1" \
swift test --filter AssinafyTests.AssinafyLiveTestsTestes que criam recursos no sandbox ou enviam notificações exigem opt-in explícito e dois destinatários distintos informados em tempo de execução:
ASSINAFY_RUN_LIVE_MUTATIONS=1 \
ASSINAFY_TEST_EMAIL_A="destinatario-a@exemplo.invalid" \
ASSINAFY_TEST_EMAIL_B="destinatario-b@exemplo.invalid" \
swift test --filter AssinafyTests.AssinafyLiveTestsNunca versione credenciais, códigos de acesso, senhas ou endereços de destinatários.
O pacote segue versionamento semântico. Versões liberadas são marcadas como vMAJOR.MINOR.PATCH, e
cada uma é registrada em CHANGELOG.md.
Distribuído sob a licença MIT.