Assinafy Ruby SDK
Ruby SDK for the Assinafy API v1.
The SDK exposes every Assinafy API v1 operation and the complete supported template lifecycle. The checked-in spec/api_coverage_spec.rb validates that each route maps uniquely to a public SDK method.
- Source: https://github.com/assinafy/ruby-sdk
- Issues: https://github.com/assinafy/ruby-sdk/issues
- API docs: https://api.assinafy.com.br/v1/docs
- Ruby SDK API reference:
docs/API_REFERENCE.md
Requirements
- Ruby 3.2+ (maintained support: 3.3+; 3.2 is legacy/EOL compatibility)
- Bundler
Installation
From RubyGems.org:
gem 'assinafy'
bundle install
From GitHub Packages (mirror):
source 'https://rubygems.pkg.github.com/assinafy' do
gem 'assinafy'
end
You'll need a personal access token with read:packages scope, configured via:
bundle config https://rubygems.pkg.github.com/assinafy USERNAME:TOKEN
Quick Start
require 'assinafy'
client = Assinafy::Client.new(
api_key: ENV.fetch('ASSINAFY_API_KEY'),
account_id: ENV.fetch('ASSINAFY_ACCOUNT_ID')
)
document = client.documents.upload({ file_path: './contract.pdf' })
signer = client.signers.create(full_name: 'Alice Silva', email: 'alice@example.com')
assignment = client.assignments.create(
document['id'],
method: 'virtual',
signers: [{ id: signer['id'] }],
message: 'Please sign the attached contract.'
)
puts assignment['id']
Configuration
client = Assinafy::Client.new(
api_key: 'your-api-key',
token: nil,
account_id: 'your-account-id',
base_url: 'https://api.assinafy.com.br/v1',
webhook_secret: nil,
timeout: 30,
logger: Logger.new($stdout)
)
api_key:sendsX-Api-Key(preferred).token:sendsAuthorization: Bearer ...(legacy session token).- Configure exactly one credential. If both are supplied, the SDK sends only
X-Api-Key. - A client can also be created with no credentials for authentication and public/signer endpoints.
- Account-scoped methods document a per-call account override for multi-workspace tenants.
- Provide a
Logger-compatiblelogger:to observe upload/assignment/webhook lifecycle messages. - Requests send
User-Agent: Assinafy-Ruby-SDK/v1.5.1; the suffix always followsAssinafy::VERSION.
Client.from_config(hash) accepts string- or symbol-keyed hashes (e.g. parsed YAML).
Resources
Assinafy::Client exposes twelve accessors — eleven API resources for the current
OpenAPI operations and sandbox-live template routes, plus the local webhook_verifier helper:
| Accessor | What it covers |
|---|---|
client.auth |
Login, social login, password reset, API keys |
client.accounts |
Account CRUD, theme, KPI stats, brand logo |
client.users |
User profile, notification preferences, cross-account KPIs |
client.documents |
Upload, list, search, rename, download, delete, verify, tags |
client.signers |
Workspace signer CRUD + signer self-service endpoints |
client.signer_documents |
Signer-authenticated multi-document operations + search |
client.assignments |
List/create/sign/decline/resend/estimate assignments |
client.templates |
Template creation (file upload), get, list, update, delete |
client.tags |
Workspace tags |
client.fields |
Field definitions + validation + type catalog |
client.webhooks |
Subscription, event-type catalog, dispatch history, retries |
client.webhook_verifier |
Optional HMAC-SHA256 verifier for signed deliveries |
Authentication
client.auth.login(email: 'user@example.com', password: 'secret')
client.auth.(provider: 'google', token: 'id-token', has_accepted_terms: true)
client.auth.(provider: 'google', token: 'id-token')
client.auth.create_api_key(password: 'secret')
client.auth.get_api_key
client.auth.delete_api_key
client.auth.change_password(email: 'user@example.com', password: 'old', new_password: 'new')
client.auth.request_password_reset(email: 'user@example.com')
client.auth.reset_password(email: 'user@example.com', new_password: 'new', token: 'reset-token')
Accounts
client.accounts.list # accounts the user can access
client.accounts.get # the current account (or pass an id)
client.accounts.create(name: 'Acme Inc.')
client.accounts.update({ name: 'Acme Renamed' })
client.accounts.delete(force: true, account_id_override: 'account-id')
client.accounts.theme # { account_name, primary_color, secondary_color, logo }
client.accounts.stats(granularity: 'monthly', month: '2026-06') # account KPI rows
client.accounts.upload_logo({ file_path: './logo.png' })
client.accounts.download_logo # raw bytes; raises ApiError on HTTP 404 when unset
client.accounts.delete_logo
Users
client.users.me # OpenAPI: AuthUser; some sandboxes: { user:, accounts: }; data is passed through
client.users.stats(granularity: 'monthly') # cross-account KPI rows
client.users.notification_preferences # returns all nine owner-email preferences
client.users.update_notification_preferences(SignerDeclined: false) # partial request; returns all nine
Both stats methods return rows with period, documents_uploaded, documents_sent, signature_requests,
signature_requests_notification_email, signature_requests_notification_whatsapp,
signature_requests_notification_bypass, signature_requests_verification_email,
signature_requests_verification_whatsapp, signature_requests_verification_bypass,
signature_requests_verification_digital_certificate, signature_requests_viewed,
signature_requests_completed, and documents_certified.
Documents
client.documents.statuses # GET /documents/statuses
client.documents.list(page: 1, per_page: 20, status: 'pending_signature')
client.documents.search('contract') # lightweight GET .../documents/search
client.documents.upload({ file_path: './contract.pdf' }, name: 'Contract v1')
client.documents.upload({ buffer: pdf_bytes, file_name: 'contract.pdf' })
client.documents.rename('document-id', 'renamed.pdf') # PATCH /documents/{id}
client.documents.get('document-id') # alias of .details
client.documents.wait_until_ready('document-id', max_wait_seconds: 60)
client.documents.activities('document-id')
client.documents.thumbnail('document-id') # binary PNG/JPEG
client.documents.download('document-id', 'certificated') # binary PDF
client.documents.download('document-id', 'pades') # signed PAdES artifact
client.documents.download_page('document-id', 'page-id')
client.documents.delete('document-id')
client.documents.verify('signature-hash')
client.documents.public_info('document-id')
client.documents.send_token('document-id') # current OpenAPI also permits no body
client.documents.send_token('document-id', email: 'alice@example.com') # current OpenAPI body
# Current sandbox deployment: recipient: 'alice@example.com', channel: 'email'
client.documents.('document-id')
client.documents.('document-id', ['tag-id-1', 'tag-id-2'])
client.documents.('document-id', ['tag-id-3'])
# The deployed sandbox also accepts existing tag names in these arrays.
client.documents.detach_tag('document-id', 'tag-id')
# Template-driven creation
client.documents.create_from_template(
'template-id',
[{ role_id: 'role', id: 'signer-id', verification_method: 'Email', notification_methods: ['Email'] }],
{ name: 'Contract', message: 'Please sign', expires_at: '2026-12-31T23:59:00Z' }
)
client.documents.estimate_cost_from_template(
'template-id',
[{ role_id: 'role', id: 'signer-id', verification_method: 'Whatsapp' }]
)
# Convenience: signing progress derived from the embedded assignment summary
client.documents.fully_signed?('document-id')
client.documents.signing_progress('document-id')
# => { signed: 1, total: 2, pending: 1, percentage: 50.0 }
Signers (workspace CRUD)
client.signers.create(full_name: 'Alice Silva', email: 'alice@example.com')
client.signers.create(full_name: 'Bob Costa', phone: '+5548999990000') # phone -> whatsapp_phone_number
client.signers.get('signer-id')
client.signers.list(search: 'alice', per_page: 50) # returns { data:, meta: }
client.signers.update('signer-id', full_name: 'Alice S.', government_id: '00000000000')
client.signers.delete('signer-id')
# Convenience: case-insensitive lookup; nil means a successful search found no match
client.signers.find_by_email('alice@example.com')
Signers (self-service, signer-access-code)
client.signers.self_data(signer_access_code: 'code') # includes has_signature, has_initial, is_signature_reusable
client.signers.accept_terms(signer_access_code: 'code')
client.signers.verify_email(verification_code: '123456', signer_access_code: 'code')
client.signers.confirm_data('document-id', { full_name: 'Alice Silva', email: 'alice@example.com', government_id: '00000000000' }, signer_access_code: 'code')
client.signers.upload_signature(png_bytes, signer_access_code: 'code', type: 'signature', content_type: 'image/png')
# => nil for the documented no-data envelope; some deployments return []
client.signers.download_signature(signer_access_code: 'code', type: 'signature')
Assignments
# Virtual (no positioned fields)
client.assignments.create(
'document-id',
method: 'virtual',
signers: [{ id: 'signer-1', verification_method: 'Email', notification_methods: ['Email'], step: 1 }],
message: 'Please sign',
expires_at: '2026-12-31T23:59:00Z',
copy_receivers: ['cc-signer-id']
)
# Collect (positioned fields)
client.assignments.create(
'document-id',
method: 'collect',
signers: [{ id: 'signer-1' }],
entries: [{ page_id: 'page-id', fields: [{ signer_id: 'signer-1', field_id: 'field-id',
display_settings: { left: 100, top: 100, width: 240,
height: 48, fontSize: 16 } }] }]
)
client.assignments.list # GET /assignments (scoped to the account)
client.assignments.estimate_cost('document-id', signers: [{ verification_method: 'Whatsapp' }])
client.assignments.reset_expiration('document-id', 'assignment-id', '2026-12-31T23:59:00Z')
client.assignments.reset_expiration('document-id', 'assignment-id', nil) # clears the expiry
client.assignments.resend_notification('document-id', 'assignment-id', 'signer-id')
client.assignments.estimate_resend_cost('document-id', 'assignment-id', 'signer-id')
client.assignments.whatsapp_notifications('document-id', 'assignment-id')
# Signer perspective (signer-access-code authentication)
client.assignments.signer_document(signer_access_code: 'code', has_accepted_terms: true)
client.assignments.sign(
'document-id',
'assignment-id',
[{ item_id: 'i1', field_id: 'f1', page_id: 'p1', value: 'Alice' }],
signer_access_code: 'code'
)
client.assignments.decline('document-id', 'assignment-id', decline_reason: 'Clause 2', signer_access_code: 'code')
The
signrequest body is the API's camelCase body-key exception. This SDK accepts the snake_case keys (item_id,field_id,page_id,value) shown above and maps them toitemId/fieldId/pageId/valueautomatically. CamelCase input is also passed through unchanged. Assignment listing separately uses the live-requiredaccountIdquery parameter.
Signer documents (multi-document workflows)
client.signer_documents.current('signer-id', signer_access_code: 'code')
client.signer_documents.list('signer-id', { status: 'pending_signature' }, signer_access_code: 'code')
client.signer_documents.search('signer-id', 'contract', signer_access_code: 'code')
client.signer_documents.sign_multiple(%w[doc-1 doc-2], signer_access_code: 'code')
client.signer_documents.decline_multiple(%w[doc-1 doc-2], decline_reason: 'No', signer_access_code: 'code')
client.signer_documents.download('signer-id', 'document-id', 'pades') # public: no access code needed
Templates
client.templates.list(search: 'contract', per_page: 25)
client.templates.get('template-id')
client.templates.create({ file_path: './contract.pdf' }) # multipart file upload
client.templates.create({ buffer: pdf_bytes, file_name: 'contract.pdf' })
client.templates.update('template-id', name: 'Renamed template')
client.templates.delete('template-id')
client.templates.download_page('template-id', 'page-id') # binary image bytes
Template endpoints (
get/create/update/delete/download_page) are live-verified against the sandbox but are not part of the current OpenAPI document.createrequires a source file (multipart/form-data); the template name defaults to the uploaded file's name.
Tags
client..list(search: 'contract')
client..create(name: 'Contracts', color: 'ff8800')
client..update('tag-id', name: 'Sales Contracts', color: nil)
client..delete('tag-id') # fails with 409 if the tag is in use
client..delete('tag-id', force: true) # detaches from documents/templates first
Fields
client.fields.types # GET /field-types
client.fields.list(include_inactive: true, include_standard: false)
client.fields.create(type: 'text', name: 'Internal code', regex: '/[A-Z]{3}-[0-9]{4}/')
client.fields.get('field-id')
client.fields.update('field-id', name: 'Renamed')
client.fields.delete('field-id')
# Authenticated as a workspace user
client.fields.validate('field-id', 'ABC-1234')
# Or authenticated via signer-access-code
client.fields.validate('field-id', 'ABC-1234', signer_access_code: 'code')
client.fields.validate_multiple(
[{ field_id: 'a', value: '1' }, { field_id: 'b', value: 'value@example.com' }],
signer_access_code: 'code'
)
Webhooks
client.webhooks.list_event_types # GET /webhooks/event-types
client.webhooks.get # current subscription (nil on 404)
client.webhooks.register(
url: 'https://example.com/webhooks/assinafy',
email: 'ops@example.com',
events: %w[document_ready document_prepared signer_signed_document]
)
client.webhooks.inactivate # stop deliveries, keep the event set
client.webhooks.list_dispatches(delivered: false, per_page: 50)
client.webhooks.retry_dispatch('dispatch-id')
To stop deliveries, use
inactivate(the API has no delete-subscription route).
Webhook signature verification
The Assinafy API does not currently document a body-signing scheme for outbound webhook deliveries. The SDK ships an opt-in HMAC-SHA256 verifier you can use when a gateway in front of your receiver is configured to sign payloads with a shared secret:
verifier = Assinafy::Support::WebhookVerifier.new(ENV.fetch('ASSINAFY_WEBHOOK_SECRET'))
raw_body = request.body.read
# The signature header is one your gateway injects (e.g. Cloudflare / API Gateway).
# Assinafy v1 does not send a signature header itself.
if verifier.verify(raw_body, request.headers['X-Webhook-Signature'])
event = verifier.extract_event(raw_body)
verifier.event_type(event) # => "assignment_created" (the top-level `event`)
verifier.event_payload(event) # => event-specific params, or nil
verifier.event_object(event) # => the entity acted on, e.g. the document
verifier.event_subject(event) # => the actor, e.g. the user
end
If no webhook_secret is configured, verify always returns false — safe-by-default.
Responses
Most JSON successes use a { "status": ..., "message": ..., "data": ... } envelope. The SDK
returns the data payload (a Hash for single resources, an Array for collection bodies). For
documented no-data envelopes containing only status/message, it returns nil; deployed API
versions that add data are passed through. Binary endpoints (download, thumbnail,
download_page, download_signature) return raw bytes as an ASCII-8BIT String, and
delete-style endpoints return nil.
Errors surface the envelope/framework error body through Assinafy::ApiError:
begin
client.documents.details('missing-id')
rescue Assinafy::ApiError => e
e.status_code # => 404
e. # => "Documento não encontrado."
e.error_name # => nil (or "Not Found" for framework errors)
e.error_code # => nil (or an integer code)
e.response_data # => the raw parsed body
end
The SDK API reference maps every API operation to its Ruby method and documents exact authentication, parameters, request bodies, success responses, and every published schema property.
Complete document workflow
Every call below returns the SDK value after response-envelope handling. Use the API operation table for each method's exact HTTP authentication, parameters, request body, and success wire response. Named response objects link to the complete schema catalog.
1. Configure a workspace client
client = Assinafy::Client.new(
api_key: ENV.fetch('ASSINAFY_API_KEY'),
account_id: ENV.fetch('ASSINAFY_ACCOUNT_ID'),
base_url: ENV.fetch('ASSINAFY_BASE_URL', 'https://api.assinafy.com.br/v1')
)
Use one authentication credential per client. Account-scoped methods use the configured account unless an
explicit account_id_override is supplied.
2. Upload a document or select a template
document = client.documents.upload(
{ file_path: './contract.pdf' },
name: 'Customer agreement'
)
# => Document
template = client.templates.get('template-id')
# => Template
For in-memory data, pass { buffer: pdf_bytes, file_name: 'contract.pdf' }. Upload requests are multipart;
template-based document creation is shown after the signer is available.
3. Wait for document processing
document = client.documents.wait_until_ready(
document.fetch('id'),
max_wait_seconds: 60,
poll_interval_seconds: 2
)
# => Document with a ready status and populated pages
The helper raises Assinafy::ValidationError for invalid interval values and Assinafy::Error for a failed
terminal document status or timeout.
4. Create or reuse a signer
signer = client.signers.find_by_email('signer@example.com') ||
client.signers.create(full_name: 'Example Signer', email: 'signer@example.com')
# => Signer
5. Estimate and request signatures
signer_request = {
id: signer.fetch('id'),
verification_method: 'Email',
notification_methods: ['Email'],
step: 1
}
cost = client.assignments.estimate_cost(
document.fetch('id'),
signers: [signer_request]
)
# => CostEstimate
assignment = client.assignments.create(
document.fetch('id'),
method: 'virtual',
signers: [signer_request],
message: 'Please review and sign.',
expires_at: '2026-12-31T23:59:00Z'
)
# => Assignment, including signing_urls
The template alternative binds a signer to a template role and creates the document and assignment together:
role_signer = {
role_id: template.fetch('roles').first.fetch('id'),
id: signer.fetch('id'),
verification_method: 'Email',
notification_methods: ['Email']
}
template_cost = client.documents.estimate_cost_from_template(template.fetch('id'), [role_signer])
# => CostEstimate
template_document = client.documents.create_from_template(
template.fetch('id'),
[role_signer],
name: 'Customer agreement', message: 'Please review and sign.'
)
# => Document with an embedded Assignment
6. Complete the signer flow
Signer-facing calls use the one-time access code delivered by Assinafy. They do not use the workspace API key.
signing = Assinafy::Client.new(base_url: ENV.fetch('ASSINAFY_BASE_URL'))
access_code = ENV.fetch('ASSINAFY_SIGNER_ACCESS_CODE')
verification_code = ENV.fetch('ASSINAFY_VERIFICATION_CODE')
signer_data = signing.signers.self_data(signer_access_code: access_code)
signing.signers.accept_terms(signer_access_code: access_code) unless signer_data['has_accepted_terms']
signing.signers.verify_email(
verification_code: verification_code,
signer_access_code: access_code
)
signing_document = signing.assignments.signer_document(signer_access_code: access_code)
signing.signer_documents.sign_multiple(
[signing_document.fetch('id')],
signer_access_code: access_code
)
The virtual assignment created above uses sign_multiple. For a collect assignment, submit each positioned
item through assignments.sign:
collect_items = signing_document.fetch('assignment').fetch('items').map do |item|
{
item_id: item.fetch('id'),
field_id: item.dig('field', 'id'),
page_id: item.dig('page', 'id'),
value: 'Accepted'
}
end
signing.assignments.sign(
signing_document.fetch('id'),
signing_document.dig('assignment', 'id'),
collect_items,
signer_access_code: access_code
)
The SDK maps the documented snake_case item keys to the API's camelCase request keys.
DigitalCertificate may be supplied as an assignment verification method. Provider documentation refers to
/signers/certificate/start and /signers/certificate/complete, but their authentication and request/response
schemas are not published in the API v1 machine contract, so the SDK does not expose those completion calls.
Contact Assinafy before enabling a digital-certificate signing flow in production.
7. Inspect, tag, download, and verify
document_id = document.fetch('id')
activities = client.documents.activities(document_id) # => Array<DocumentActivity>
= client.documents.(document_id, ['tag-id']) # => Array<Tag>
client.documents.(document_id, .map { |tag| tag.fetch('id') })
original_pdf = client.documents.download(document_id, 'original')
signed_pdf = client.documents.download(document_id, 'pades')
certificate_pdf = client.documents.download(document_id, 'certificated')
verification = client.documents.verify('signature-hash-from-assinafy')
# => DocumentVerification; inspect verification['is_valid']
verify returns Assinafy's verification result. It does not independently validate the PDF signature or its
certificate chain; see Authentication and safety.
8. Handle errors and clean up
begin
client.documents.details(document_id)
rescue Assinafy::ValidationError => e
warn e.errors.inspect
rescue Assinafy::ApiError => e
warn "Assinafy returned #{e.status_code}: #{e.}"
rescue Assinafy::NetworkError => e
warn "Network failure: #{e.}"
end
client.documents.delete(document_id)
client.signers.delete(signer.fetch('id'))
client.templates.delete(template.fetch('id')) if template
Delete resources only after downstream work is complete. Some resources can return 409 while processing or
while still referenced; retry only after resolving the reported state.
Pagination
Most *.list* methods return { data: [...], meta: { ... } } when the API includes pagination
headers. Ruby-style per_page: is transparently converted to the documented per-page query
parameter (values above the API's maximum are clamped server-side; the sandbox caps it at 50).
A few endpoints (e.g. fields.list) do not paginate and return meta: nil.
result = client.documents.list(page: 2, per_page: 25)
result[:data] # => Array<Hash>
result[:meta] # => { current_page: 2, per_page: 25, total: 138, last_page: 6 }
High-level workflow helper
Client#upload_and_request_signatures bundles upload + signer creation + virtual assignment into a single call:
result = client.upload_and_request_signatures(
source: { file_path: './contract.pdf' },
signers: [
{ full_name: 'Alice Silva', email: 'alice@example.com' },
{ full_name: 'Bob Costa', whatsapp_phone_number: '+5548999990000' }
],
message: 'Please sign.',
expires_at: '2026-12-31T23:59:00Z'
)
puts result[:document]['id']
puts result[:assignment]['id']
result[:signer_ids] # => ['<sid-1>', '<sid-2>']
Errors
The SDK raises one of:
Assinafy::ValidationError— caller-side input invalid (missing IDs, bad email, etc.).Assinafy::ApiError— the API returned a non-2xx status. Includesstatus_codeandresponse_data.Assinafy::NetworkError— Faraday connection error or timeout.Assinafy::Error— base class; other unexpected errors get wrapped here with the operation label.
All inherit a #context Hash with debugging metadata.
Tests
bundle exec rake spec # 300+ RSpec examples, including a coverage matrix
bundle exec rubocop # Linting
bundle exec steep check # Static type checking against the published RBS
bundle exec bundler-audit check # Dependency CVEs
ruby scripts/check_api_contract.rb --file path/to/openapi.json # validate a local contract document
The coverage spec validates the committed route-to-method inventory, public wrappers, aliases, and resource mappings without network access.
Ordinary pull-request CI remains network-independent. A weekly scheduled job runs
scripts/check_api_contract.rb against the upstream document. When Assinafy publishes a new contract, update
the SDK implementation, tests, API reference, and expected fixture together.
Live integration tests
The suite in spec/integration/ exercises safe sandbox workflows across
every workspace resource. Signer-code, OTP, password, social-login, API-key mutation, signature upload, and
irreversible sign/decline calls are wire-contract tested in the default suite but are not live-automated without
their one-time credentials or explicit state changes. The live suite is excluded from the default run and only
executes when ASSINAFY_LIVE=1 is set with credentials:
ASSINAFY_LIVE=1 \
ASSINAFY_API_KEY=... \
ASSINAFY_ACCOUNT_ID=... \
ASSINAFY_TEST_EMAIL=recipient1@example.com \
ASSINAFY_TEST_EMAIL2=recipient2@example.com \
ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
bundle exec rspec spec/integration
These tests create and clean up real resources and, for the assignment flow, send real signature-request emails to the addresses in
ASSINAFY_TEST_EMAIL/ASSINAFY_TEST_EMAIL2.
GitHub Actions provides the manual Live Sandbox workflow. Select a protected environment containing
ASSINAFY_API_KEY, ASSINAFY_ACCOUNT_ID, ASSINAFY_TEST_EMAIL, and ASSINAFY_TEST_EMAIL2; environment
branch/tag rules and approvals are applied before GitHub exposes those secrets to the job.
Contributing
Pull requests and issues are welcome at https://github.com/assinafy/ruby-sdk.
License
MIT. See LICENSE.