assinafy 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f8b8c2701ec7f853e14b297d675829d16fef94b35f87d46cf372ef24d2d55482
4
+ data.tar.gz: d6cd90c3ad7304648f397544a118b9cd57dfd80db2a6a84ddea116878b515da8
5
+ SHA512:
6
+ metadata.gz: 823b760bc2d019f2e6f77de43b6e199067b1030e5e1f2a75fde65ad87fa7cec51ae8dba59f357e388a27db945a814cc6ff0dd2be4be92e4d9c41a6b5cf5c272c
7
+ data.tar.gz: 5007b5ec7928af7dd1cc736cba63f9fc8efb72aa1478aedfad08bcaa28af2eec0c248e7903732b6e4b4c76fd73cd17f47a64535a266e33370e8d56b2b424e615
data/CHANGELOG.md ADDED
@@ -0,0 +1,168 @@
1
+ # Changelog
2
+
3
+ All notable changes to the `assinafy` Ruby gem are documented here.
4
+
5
+ ## 1.5.0
6
+
7
+ ### Added
8
+
9
+ - **`Resources::AccountResource`** (`client.accounts`): `list`, `create`, `get`, `update`,
10
+ `delete` (with `force:`), `theme`, `stats`, and brand-logo `upload_logo`/`download_logo`/`delete_logo`.
11
+ - **`Resources::UserResource`** (`client.users`): `me`, `stats`, and notification-preference
12
+ `GET`/`PUT` wrappers.
13
+ - `DocumentResource#search` (`GET /accounts/{account_id}/documents/search`) and
14
+ `#rename` (`PATCH /documents/{document_id}`).
15
+ - `AssignmentResource#list` (`GET /assignments`; the account context is sent as the
16
+ camelCase `accountId` query parameter, verified live).
17
+ - `SignerDocumentResource#search` (`GET /signers/{signer_id}/documents/search`).
18
+ - `AuthResource#link_social_login` (`POST /auth/link-social-login`).
19
+ - `SignerResource#upload_signature` accepts an optional `reuse:` flag.
20
+ - An opt-in live integration suite (`spec/integration/`, gated by `ASSINAFY_LIVE=1`)
21
+ that drives representative workflows against the sandbox and self-cleans.
22
+ - A `.github/dependabot.yml` (bundler + github-actions) so dependency advisories are
23
+ caught automatically.
24
+
25
+ ### Fixed
26
+
27
+ - `SignerResource#accept_terms` and `#verify_email` now send `signer-access-code` as
28
+ the documented query parameter (the `signerAccessCode` security scheme is `in: query`),
29
+ consistent with every other signer-authenticated endpoint. Previously it was sent
30
+ only in the request body.
31
+ - `TemplateResource#create` now performs the required `multipart/form-data` file
32
+ upload. The previous JSON body was rejected by the API (HTTP 400) and never worked.
33
+ Its signature is now `create(source, options = {}, account_id_override = nil)`.
34
+ - `SignerDocumentResource#download` makes `signer_access_code:` optional — the endpoint
35
+ is public (`security: []`), so only the document/artifact IDs are required.
36
+ - Document search wrappers send the documented `search` query key; downloads accept the
37
+ documented `pades` artifact; signer updates preserve `government_id`.
38
+ - `faraday` dependency floor raised to `>= 2.14.3` (CVE-2026-54297, DoS via deeply
39
+ nested query params).
40
+
41
+ ### Removed
42
+
43
+ - `WebhookResource#delete`. There is no `DELETE /webhooks/subscriptions` route — it
44
+ returns HTTP 404 (verified live). Use `#inactivate` to stop deliveries.
45
+
46
+ ### Documentation
47
+
48
+ - Corrected YARD payloads verified against live responses: `confirm_data` (body is
49
+ `full_name`/`email`/`government_id`; returns the signer object), `upload_signature`
50
+ (handles documented no-data responses), `FieldResource#update` (accepts only `name`/`regex`/`is_active`),
51
+ webhook dispatch shape (ISO-8601 `created_at` + `updated_at`), and template `create`.
52
+ - README updated for the new resources and behaviors, plus live-test instructions.
53
+
54
+ ## 1.4.0
55
+
56
+ This release is the result of a full audit of the SDK against the live Assinafy
57
+ v1 API (verified end-to-end against the sandbox) and the published documentation
58
+ at <https://api.assinafy.com.br/v1/docs>.
59
+
60
+ ### Added
61
+
62
+ - `Resources::TemplateResource#delete` (`DELETE /accounts/{account_id}/templates/{template_id}`)
63
+ and `#download_page` (`GET /accounts/{account_id}/templates/{template_id}/pages/{page_id}/download`)
64
+ — both verified against the live API.
65
+ - `Support::WebhookVerifier#event_payload`, `#event_object`, and `#event_subject`,
66
+ matching the real delivery envelope (top-level `payload`/`object`/`subject` keys).
67
+ - `ApiError#error_name` and `ApiError#error_code` expose the API's `name`/`code`
68
+ error fields; `ApiError.from_response` now also falls back to `name` for the message.
69
+ - YARD `@example` blocks with full request **and** response payloads on every public
70
+ method, sourced from the live API and the docs.
71
+ - Behavioral coverage: `spec/api_coverage_spec.rb` now also fails CI when a public
72
+ endpoint wrapper is missing a matrix row, and asserts documented aliases still
73
+ resolve to their canonical methods. Behavioral WebMock specs were added for the
74
+ previously untested methods and for `Client#upload_and_request_signatures`.
75
+
76
+ ### Fixed
77
+
78
+ - `AssignmentResource#reset_expiration` now sends `expires_at` verbatim, so an
79
+ explicit `nil` is serialized as JSON `null` ("no expiration") instead of being
80
+ dropped from the body (the field is required by the API). Verified live.
81
+ - `Support::WebhookVerifier#event_data`/`#event_type` corrected to the real envelope:
82
+ `event_type` reads `event` (the fabricated `type` fallback was removed), and
83
+ `event_data` is deprecated in favor of `#event_payload`/`#event_object`.
84
+ - `DocumentResource#send_token` validates `recipient`/`channel` before the request.
85
+ - `SignerResource#find_by_email` now paginates through all result pages instead of
86
+ relying on a single oversized page (the API clamps `per-page` to its own maximum —
87
+ observed as 50 on the sandbox).
88
+ - `Client#upload_and_request_signatures` raises a clear `ApiError` if a created
89
+ signer comes back without an ID, rather than building an assignment with `nil` IDs.
90
+ - `TagResource#update` rejects an empty payload or a blank name before issuing a no-op PUT.
91
+
92
+ ### Changed
93
+
94
+ - **Minimum Ruby is now 3.2** (3.0 and 3.1 are end-of-life). CI tests 3.2, 3.3, 3.4,
95
+ 4.0, and head. `.ruby-version` is committed (3.4.8) and drives the lint/audit/release jobs.
96
+ - `SignerDocumentResource#list` accepts an optional `signer_access_code:` (the endpoint
97
+ also supports workspace `X-Api-Key` auth); the class auth documentation was corrected.
98
+ - CI gained a `concurrency` group to cancel superseded runs.
99
+
100
+ ## 1.3.1
101
+
102
+ ### Added
103
+
104
+ - `Client#tags` and `Resources::TagResource` for the documented workspace tag
105
+ endpoints.
106
+ - Document tag helpers on `Resources::DocumentResource`: `list_tags`,
107
+ `replace_tags`, `append_tags`, and `detach_tag`.
108
+
109
+ ### Fixed
110
+
111
+ - Assignment signer payloads now preserve the documented `step` field for
112
+ sequential signing.
113
+
114
+ ## 1.3.0
115
+
116
+ ### Added
117
+
118
+ - YARD documentation for every public method on `Client`, `Configuration`,
119
+ every `Resources::*` class, `Support::WebhookVerifier`, and the SDK's
120
+ error hierarchy.
121
+ - `spec/api_coverage_spec.rb` — an explicit, version-controlled matrix that
122
+ asserts every documented endpoint at
123
+ https://api.assinafy.com.br/v1/docs has a corresponding SDK method.
124
+
125
+ ### Fixed
126
+
127
+ - `AssignmentResource#sign` now translates snake_case keys (`item_id`,
128
+ `field_id`, `page_id`, `value`) to the camelCase keys (`itemId`, `fieldId`,
129
+ `pageId`, `value`) that the `POST /documents/{documentId}/assignments/{assignmentId}`
130
+ endpoint expects — the only place the API deviates from snake_case.
131
+ Already-camelCase input is passed through unchanged.
132
+
133
+ ### Changed
134
+
135
+ - README expanded with one runnable example per resource matching the
136
+ documented Assinafy v1 surface area.
137
+
138
+ ## 1.2.0
139
+
140
+ ### Changed
141
+
142
+ - Consolidated HTTP error handling in `BaseResource` behind a single private
143
+ `request` helper used by `call`, `call_void`, `call_binary`, `call_list`,
144
+ and `call_optional`.
145
+ - Pagination metadata extraction in `BaseResource` is now driven by a
146
+ declarative header → key mapping.
147
+ - `Client.from_config` now delegates to `Configuration.from_hash`. A new
148
+ `Client.from_hash` alias is exposed for symmetry with `Configuration`.
149
+ - `Client#upload_and_request_signatures` no longer duplicates the signer
150
+ payload normalization logic — it relies on `SignerResource#create`.
151
+ - `NullLogger` now responds to the full Ruby `Logger` severity surface.
152
+ - Minimum Ruby is now 3.0 (matching the CI matrix).
153
+
154
+ ### Removed
155
+
156
+ - Undocumented `expiration` field from `AssignmentResource.build_payload`
157
+ — only `expires_at` is documented in the public API.
158
+
159
+ ### Fixed
160
+
161
+ - Documented `signer-access-code` and pagination handling are now consistent
162
+ across every resource.
163
+
164
+ ### Release
165
+
166
+ - Added a tag-triggered release workflow that publishes to both GitHub
167
+ Packages (`rubygems.pkg.github.com/assinafy`) and RubyGems.org. The verify
168
+ job blocks publishing when the git tag does not match `Assinafy::VERSION`.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Assinafy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,463 @@
1
+ # Assinafy Ruby SDK
2
+
3
+ [![CI](https://github.com/assinafy/ruby-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/assinafy/ruby-sdk/actions/workflows/ci.yml)
4
+ [![Gem Version](https://img.shields.io/gem/v/assinafy.svg)](https://rubygems.org/gems/assinafy)
5
+
6
+ Ruby SDK for the [Assinafy API v1](https://api.assinafy.com.br/v1/docs).
7
+
8
+ The SDK includes a public wrapper for every operation in the current Assinafy v1 [OpenAPI specification](https://api.assinafy.com.br/v1/docs/openapi.json), plus sandbox-live template routes that are not yet listed there. The checked-in [`spec/api_coverage_spec.rb`](spec/api_coverage_spec.rb) verifies that its static route-to-method inventory is unique and points to public SDK methods.
9
+
10
+ - **Source:** <https://github.com/assinafy/ruby-sdk>
11
+ - **Issues:** <https://github.com/assinafy/ruby-sdk/issues>
12
+ - **API docs:** <https://api.assinafy.com.br/v1/docs>
13
+
14
+ ## Requirements
15
+
16
+ - Ruby 3.2+ (maintained support: 3.3+; 3.2 is legacy/EOL compatibility)
17
+ - Bundler
18
+
19
+ ## Installation
20
+
21
+ From RubyGems.org:
22
+
23
+ ```ruby
24
+ gem 'assinafy'
25
+ ```
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ From GitHub Packages (mirror):
32
+
33
+ ```ruby
34
+ source 'https://rubygems.pkg.github.com/assinafy' do
35
+ gem 'assinafy'
36
+ end
37
+ ```
38
+
39
+ You'll need a personal access token with `read:packages` scope, configured via:
40
+
41
+ ```bash
42
+ bundle config https://rubygems.pkg.github.com/assinafy USERNAME:TOKEN
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```ruby
48
+ require 'assinafy'
49
+
50
+ client = Assinafy::Client.new(
51
+ api_key: ENV.fetch('ASSINAFY_API_KEY'),
52
+ account_id: ENV.fetch('ASSINAFY_ACCOUNT_ID')
53
+ )
54
+
55
+ document = client.documents.upload({ file_path: './contract.pdf' })
56
+ signer = client.signers.create(full_name: 'Alice Silva', email: 'alice@example.com')
57
+
58
+ assignment = client.assignments.create(
59
+ document['id'],
60
+ method: 'virtual',
61
+ signers: [{ id: signer['id'] }],
62
+ message: 'Please sign the attached contract.'
63
+ )
64
+
65
+ puts assignment['id']
66
+ ```
67
+
68
+ ## Configuration
69
+
70
+ ```ruby
71
+ client = Assinafy::Client.new(
72
+ api_key: 'your-api-key',
73
+ token: nil,
74
+ account_id: 'your-account-id',
75
+ base_url: 'https://api.assinafy.com.br/v1',
76
+ webhook_secret: nil,
77
+ timeout: 30,
78
+ logger: Logger.new($stdout)
79
+ )
80
+ ```
81
+
82
+ - `api_key:` sends `X-Api-Key` (preferred).
83
+ - `token:` sends `Authorization: Bearer ...` (legacy session token).
84
+ - A client can also be created with no credentials for authentication and public/signer endpoints.
85
+ - Account-scoped methods document a per-call account override for multi-workspace tenants.
86
+ - Provide a `Logger`-compatible `logger:` to observe upload/assignment/webhook lifecycle messages.
87
+
88
+ `Client.from_config(hash)` accepts string- or symbol-keyed hashes (e.g. parsed YAML).
89
+
90
+ ## Resources
91
+
92
+ `Assinafy::Client` exposes twelve accessors — eleven API resources for the current
93
+ OpenAPI operations and sandbox-live template routes, plus the local `webhook_verifier` helper:
94
+
95
+ | Accessor | What it covers |
96
+ | --------------------------- | -------------------------------------------------------------- |
97
+ | `client.auth` | Login, social login, password reset, API keys |
98
+ | `client.accounts` | Account CRUD, theme, KPI stats, brand logo |
99
+ | `client.users` | User profile, notification preferences, cross-account KPIs |
100
+ | `client.documents` | Upload, list, search, rename, download, delete, verify, tags |
101
+ | `client.signers` | Workspace signer CRUD + signer self-service endpoints |
102
+ | `client.signer_documents` | Signer-authenticated multi-document operations + search |
103
+ | `client.assignments` | List/create/sign/decline/resend/estimate assignments |
104
+ | `client.templates` | Template creation (file upload), get, list, update, delete |
105
+ | `client.tags` | Workspace tags |
106
+ | `client.fields` | Field definitions + validation + type catalog |
107
+ | `client.webhooks` | Subscription, event-type catalog, dispatch history, retries |
108
+ | `client.webhook_verifier` | Optional HMAC-SHA256 verifier for signed deliveries |
109
+
110
+ ### Authentication
111
+
112
+ ```ruby
113
+ client.auth.login(email: 'user@example.com', password: 'secret')
114
+ client.auth.social_login(provider: 'google', token: 'id-token', has_accepted_terms: true)
115
+ client.auth.link_social_login(provider: 'google', token: 'id-token')
116
+ client.auth.create_api_key(password: 'secret')
117
+ client.auth.get_api_key
118
+ client.auth.delete_api_key
119
+ client.auth.change_password(email: 'user@example.com', password: 'old', new_password: 'new')
120
+ client.auth.request_password_reset(email: 'user@example.com')
121
+ client.auth.reset_password(email: 'user@example.com', new_password: 'new', token: 'reset-token')
122
+ ```
123
+
124
+ ### Accounts
125
+
126
+ ```ruby
127
+ client.accounts.list # accounts the user can access
128
+ client.accounts.get # the current account (or pass an id)
129
+ client.accounts.create(name: 'Acme Inc.')
130
+ client.accounts.update({ name: 'Acme Renamed' })
131
+ client.accounts.delete(force: true, account_id_override: 'account-id')
132
+ client.accounts.theme # { account_name, primary_color, secondary_color, logo }
133
+ client.accounts.stats(granularity: 'monthly', month: '2026-06') # account KPI rows
134
+ client.accounts.upload_logo({ file_path: './logo.png' })
135
+ client.accounts.download_logo # raw bytes; raises ApiError on HTTP 404 when unset
136
+ client.accounts.delete_logo
137
+ ```
138
+
139
+ ### Users
140
+
141
+ ```ruby
142
+ client.users.me # OpenAPI: AuthUser; some sandboxes: { user:, accounts: }; data is passed through
143
+ client.users.stats(granularity: 'monthly') # cross-account KPI rows
144
+ client.users.notification_preferences # returns all nine owner-email preferences
145
+ client.users.update_notification_preferences(SignerDeclined: false) # partial request; returns all nine
146
+ ```
147
+
148
+ Both stats methods return rows with `period`, `documents_uploaded`, `documents_sent`,
149
+ `signature_requests`, `signature_requests_email`, `signature_requests_whatsapp`,
150
+ `signature_requests_viewed`, `signature_requests_completed`, and `documents_certified`.
151
+
152
+ ### Documents
153
+
154
+ ```ruby
155
+ client.documents.statuses # GET /documents/statuses
156
+ client.documents.list(page: 1, per_page: 20, status: 'pending_signature')
157
+ client.documents.search('contract') # lightweight GET .../documents/search
158
+ client.documents.upload({ file_path: './contract.pdf' }, name: 'Contract v1')
159
+ client.documents.upload({ buffer: pdf_bytes, file_name: 'contract.pdf' })
160
+ client.documents.rename('document-id', 'renamed.pdf') # PATCH /documents/{id}
161
+ client.documents.get('document-id') # alias of .details
162
+ client.documents.wait_until_ready('document-id', max_wait_seconds: 60)
163
+ client.documents.activities('document-id')
164
+ client.documents.thumbnail('document-id') # binary PNG/JPEG
165
+ client.documents.download('document-id', 'certificated') # binary PDF
166
+ client.documents.download('document-id', 'pades') # signed PAdES artifact
167
+ client.documents.download_page('document-id', 'page-id')
168
+ client.documents.delete('document-id')
169
+ client.documents.verify('signature-hash')
170
+ client.documents.public_info('document-id')
171
+ client.documents.send_token('document-id') # current OpenAPI also permits no body
172
+ client.documents.send_token('document-id', email: 'alice@example.com') # current OpenAPI body
173
+ # Current sandbox deployment: recipient: 'alice@example.com', channel: 'email'
174
+ client.documents.list_tags('document-id')
175
+ client.documents.replace_tags('document-id', ['tag-id-1', 'tag-id-2'])
176
+ client.documents.append_tags('document-id', ['tag-id-3'])
177
+ # The current sandbox also accepts existing tag names in these arrays.
178
+ client.documents.detach_tag('document-id', 'tag-id')
179
+
180
+ # Template-driven creation
181
+ client.documents.create_from_template(
182
+ 'template-id',
183
+ [{ role_id: 'role', id: 'signer-id', verification_method: 'Email', notification_methods: ['Email'] }],
184
+ { name: 'Contract', message: 'Please sign', expires_at: '2026-12-31T23:59:00Z' }
185
+ )
186
+ client.documents.estimate_cost_from_template(
187
+ 'template-id',
188
+ [{ role_id: 'role', id: 'signer-id', verification_method: 'Whatsapp' }]
189
+ )
190
+
191
+ # Convenience: signing progress derived from the embedded assignment summary
192
+ client.documents.fully_signed?('document-id')
193
+ client.documents.signing_progress('document-id')
194
+ # => { signed: 1, total: 2, pending: 1, percentage: 50.0 }
195
+ ```
196
+
197
+ ### Signers (workspace CRUD)
198
+
199
+ ```ruby
200
+ client.signers.create(full_name: 'Alice Silva', email: 'alice@example.com')
201
+ client.signers.create(full_name: 'Bob Costa', phone: '+5548999990000') # phone -> whatsapp_phone_number
202
+ client.signers.get('signer-id')
203
+ client.signers.list(search: 'alice', per_page: 50) # returns { data:, meta: }
204
+ client.signers.update('signer-id', full_name: 'Alice S.', government_id: '00000000000')
205
+ client.signers.delete('signer-id')
206
+
207
+ # Convenience: case-insensitive lookup with built-in 404 handling
208
+ client.signers.find_by_email('alice@example.com')
209
+ ```
210
+
211
+ ### Signers (self-service, signer-access-code)
212
+
213
+ ```ruby
214
+ client.signers.self_data(signer_access_code: 'code') # includes has_signature, has_initial, is_signature_reusable
215
+ client.signers.accept_terms(signer_access_code: 'code')
216
+ client.signers.verify_email(verification_code: '123456', signer_access_code: 'code')
217
+ client.signers.confirm_data('document-id', { full_name: 'Alice Silva', email: 'alice@example.com', government_id: '00000000000' }, signer_access_code: 'code')
218
+ client.signers.upload_signature(png_bytes, signer_access_code: 'code', type: 'signature', content_type: 'image/png')
219
+ # => nil for the documented no-data envelope; some deployments return []
220
+ client.signers.download_signature(signer_access_code: 'code', type: 'signature')
221
+ ```
222
+
223
+ ### Assignments
224
+
225
+ ```ruby
226
+ # Virtual (no positioned fields)
227
+ client.assignments.create(
228
+ 'document-id',
229
+ method: 'virtual',
230
+ signers: [{ id: 'signer-1', verification_method: 'Email', notification_methods: ['Email'], step: 1 }],
231
+ message: 'Please sign',
232
+ expires_at: '2026-12-31T23:59:00Z',
233
+ copy_receivers: ['cc-signer-id']
234
+ )
235
+
236
+ # Collect (positioned fields)
237
+ client.assignments.create(
238
+ 'document-id',
239
+ method: 'collect',
240
+ signers: [{ id: 'signer-1' }],
241
+ entries: [{ page_id: 'page-id', fields: [{ signer_id: 'signer-1', field_id: 'field-id',
242
+ display_settings: { left: 100, top: 100, width: 240,
243
+ height: 48, fontSize: 16 } }] }]
244
+ )
245
+
246
+ client.assignments.list # GET /assignments (scoped to the account)
247
+ client.assignments.estimate_cost('document-id', signers: [{ verification_method: 'Whatsapp' }])
248
+ client.assignments.reset_expiration('document-id', 'assignment-id', '2026-12-31T23:59:00Z')
249
+ client.assignments.reset_expiration('document-id', 'assignment-id', nil) # clears the expiry
250
+ client.assignments.resend_notification('document-id', 'assignment-id', 'signer-id')
251
+ client.assignments.estimate_resend_cost('document-id', 'assignment-id', 'signer-id')
252
+ client.assignments.whatsapp_notifications('document-id', 'assignment-id')
253
+
254
+ # Signer perspective (signer-access-code authentication)
255
+ client.assignments.signer_document(signer_access_code: 'code', has_accepted_terms: true)
256
+ client.assignments.sign(
257
+ 'document-id',
258
+ 'assignment-id',
259
+ [{ item_id: 'i1', field_id: 'f1', page_id: 'p1', value: 'Alice' }],
260
+ signer_access_code: 'code'
261
+ )
262
+ client.assignments.decline('document-id', 'assignment-id', decline_reason: 'Clause 2', signer_access_code: 'code')
263
+ ```
264
+
265
+ > The `sign` request 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 to `itemId/fieldId/pageId/value` automatically. CamelCase input is also passed through unchanged. Assignment listing separately uses the live-required `accountId` query parameter.
266
+
267
+ ### Signer documents (multi-document workflows)
268
+
269
+ ```ruby
270
+ client.signer_documents.current('signer-id', signer_access_code: 'code')
271
+ client.signer_documents.list('signer-id', { status: 'pending_signature' }, signer_access_code: 'code')
272
+ client.signer_documents.search('signer-id', 'contract', signer_access_code: 'code')
273
+ client.signer_documents.sign_multiple(%w[doc-1 doc-2], signer_access_code: 'code')
274
+ client.signer_documents.decline_multiple(%w[doc-1 doc-2], decline_reason: 'No', signer_access_code: 'code')
275
+ client.signer_documents.download('signer-id', 'document-id', 'pades') # public: no access code needed
276
+ ```
277
+
278
+ ### Templates
279
+
280
+ ```ruby
281
+ client.templates.list(search: 'contract', per_page: 25)
282
+ client.templates.get('template-id')
283
+ client.templates.create({ file_path: './contract.pdf' }) # multipart file upload
284
+ client.templates.create({ buffer: pdf_bytes, file_name: 'contract.pdf' })
285
+ client.templates.update('template-id', name: 'Renamed template')
286
+ client.templates.delete('template-id')
287
+ client.templates.download_page('template-id', 'page-id') # binary image bytes
288
+ ```
289
+
290
+ > Template endpoints (`get`/`create`/`update`/`delete`/`download_page`) are live-verified against the sandbox but are not part of the current OpenAPI document. `create` requires a source file (`multipart/form-data`); the template name defaults to the uploaded file's name.
291
+
292
+ ### Tags
293
+
294
+ ```ruby
295
+ client.tags.list(search: 'contract')
296
+ client.tags.create(name: 'Contracts', color: 'ff8800')
297
+ client.tags.update('tag-id', name: 'Sales Contracts', color: nil)
298
+ client.tags.delete('tag-id') # fails with 409 if the tag is in use
299
+ client.tags.delete('tag-id', force: true) # detaches from documents/templates first
300
+ ```
301
+
302
+ ### Fields
303
+
304
+ ```ruby
305
+ client.fields.types # GET /field-types
306
+ client.fields.list(include_inactive: true, include_standard: false)
307
+ client.fields.create(type: 'text', name: 'Internal code', regex: '/[A-Z]{3}-[0-9]{4}/')
308
+ client.fields.get('field-id')
309
+ client.fields.update('field-id', name: 'Renamed')
310
+ client.fields.delete('field-id')
311
+
312
+ # Authenticated as a workspace user
313
+ client.fields.validate('field-id', 'ABC-1234')
314
+ # Or authenticated via signer-access-code
315
+ client.fields.validate('field-id', 'ABC-1234', signer_access_code: 'code')
316
+ client.fields.validate_multiple(
317
+ [{ field_id: 'a', value: '1' }, { field_id: 'b', value: 'value@example.com' }],
318
+ signer_access_code: 'code'
319
+ )
320
+ ```
321
+
322
+ ### Webhooks
323
+
324
+ ```ruby
325
+ client.webhooks.list_event_types # GET /webhooks/event-types
326
+ client.webhooks.get # current subscription (nil on 404)
327
+ client.webhooks.register(
328
+ url: 'https://example.com/webhooks/assinafy',
329
+ email: 'ops@example.com',
330
+ events: %w[document_ready document_prepared signer_signed_document]
331
+ )
332
+ client.webhooks.inactivate # stop deliveries, keep the event set
333
+
334
+ client.webhooks.list_dispatches(delivered: false, per_page: 50)
335
+ client.webhooks.retry_dispatch('dispatch-id')
336
+ ```
337
+
338
+ > To stop deliveries, use `inactivate` (the API has no delete-subscription route).
339
+
340
+ #### Webhook signature verification
341
+
342
+ 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:
343
+
344
+ ```ruby
345
+ verifier = Assinafy::Support::WebhookVerifier.new(ENV.fetch('ASSINAFY_WEBHOOK_SECRET'))
346
+ raw_body = request.body.read
347
+
348
+ # The signature header is one your gateway injects (e.g. Cloudflare / API Gateway).
349
+ # Assinafy v1 does not send a signature header itself.
350
+ if verifier.verify(raw_body, request.headers['X-Webhook-Signature'])
351
+ event = verifier.extract_event(raw_body)
352
+ verifier.event_type(event) # => "assignment_created" (the top-level `event`)
353
+ verifier.event_payload(event) # => event-specific params, or nil
354
+ verifier.event_object(event) # => the entity acted on, e.g. the document
355
+ verifier.event_subject(event) # => the actor, e.g. the user
356
+ end
357
+ ```
358
+
359
+ If no `webhook_secret` is configured, `verify` always returns `false` — safe-by-default.
360
+
361
+ ## Responses
362
+
363
+ Most JSON successes use a `{ "status": ..., "message": ..., "data": ... }` envelope. The SDK
364
+ returns the `data` payload (a Hash for single resources, an Array for collection bodies). For
365
+ documented no-data envelopes containing only `status`/`message`, it returns `nil`; deployed API
366
+ versions that add `data` are passed through. Binary endpoints (`download`, `thumbnail`,
367
+ `download_page`, `download_signature`) return raw bytes as an ASCII-8BIT `String`, and
368
+ delete-style endpoints return `nil`.
369
+
370
+ Errors surface the envelope/framework error body through `Assinafy::ApiError`:
371
+
372
+ ```ruby
373
+ begin
374
+ client.documents.details('missing-id')
375
+ rescue Assinafy::ApiError => e
376
+ e.status_code # => 404
377
+ e.message # => "Documento não encontrado."
378
+ e.error_name # => nil (or "Not Found" for framework errors)
379
+ e.error_code # => nil (or an integer code)
380
+ e.response_data # => the raw parsed body
381
+ end
382
+ ```
383
+
384
+ Resource YARD documentation includes request/response examples and calls out known differences
385
+ between the current OpenAPI document and the deployed sandbox.
386
+
387
+ ## Pagination
388
+
389
+ Most `*.list*` methods return `{ data: [...], meta: { ... } }` when the API includes pagination
390
+ headers. Ruby-style `per_page:` is transparently converted to the documented `per-page` query
391
+ parameter (values above the API's maximum are clamped server-side; the sandbox caps it at 50).
392
+ A few endpoints (e.g. `fields.list`) do not paginate and return `meta: nil`.
393
+
394
+ ```ruby
395
+ result = client.documents.list(page: 2, per_page: 25)
396
+ result[:data] # => Array<Hash>
397
+ result[:meta] # => { current_page: 2, per_page: 25, total: 138, last_page: 6 }
398
+ ```
399
+
400
+ ## High-level workflow helper
401
+
402
+ `Client#upload_and_request_signatures` bundles upload + signer creation + virtual assignment into a single call:
403
+
404
+ ```ruby
405
+ result = client.upload_and_request_signatures(
406
+ source: { file_path: './contract.pdf' },
407
+ signers: [
408
+ { full_name: 'Alice Silva', email: 'alice@example.com' },
409
+ { full_name: 'Bob Costa', whatsapp_phone_number: '+5548999990000' }
410
+ ],
411
+ message: 'Please sign.',
412
+ expires_at: '2026-12-31T23:59:00Z'
413
+ )
414
+
415
+ puts result[:document]['id']
416
+ puts result[:assignment]['id']
417
+ result[:signer_ids] # => ['<sid-1>', '<sid-2>']
418
+ ```
419
+
420
+ ## Errors
421
+
422
+ The SDK raises one of:
423
+
424
+ - `Assinafy::ValidationError` — caller-side input invalid (missing IDs, bad email, etc.).
425
+ - `Assinafy::ApiError` — the API returned a non-2xx status. Includes `status_code` and `response_data`.
426
+ - `Assinafy::NetworkError` — Faraday connection error or timeout.
427
+ - `Assinafy::Error` — base class; other unexpected errors get wrapped here with the operation label.
428
+
429
+ All inherit a `#context` Hash with debugging metadata.
430
+
431
+ ## Tests
432
+
433
+ ```bash
434
+ bundle exec rake spec # 200+ RSpec examples, including a coverage matrix
435
+ bundle exec rubocop # Linting
436
+ bundle exec bundler-audit check # Dependency CVEs
437
+ ```
438
+
439
+ The coverage spec checks the committed route-to-method matrix for duplicate operations or wrappers, missing public methods, aliases that drift, and unmapped resource methods. It does not download or compare the [OpenAPI document](https://api.assinafy.com.br/v1/docs/openapi.json) during the test run; update the matrix deliberately when the remote contract changes.
440
+
441
+ ### Live integration tests
442
+
443
+ The suite in [`spec/integration/`](spec/integration/live_sandbox_spec.rb) exercises representative workflows across every resource against the real sandbox. It is not an exhaustive operation-by-operation contract check: OTP- or feature-gated routes may be skipped, and sandbox rollout can lag the current OpenAPI document. The suite is excluded from the default run and only executes when `ASSINAFY_LIVE=1` is set with credentials:
444
+
445
+ ```bash
446
+ ASSINAFY_LIVE=1 \
447
+ ASSINAFY_API_KEY=... \
448
+ ASSINAFY_ACCOUNT_ID=... \
449
+ ASSINAFY_TEST_EMAIL=recipient1@example.com \
450
+ ASSINAFY_TEST_EMAIL2=recipient2@example.com \
451
+ ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
452
+ bundle exec rspec spec/integration
453
+ ```
454
+
455
+ > 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`.
456
+
457
+ ## Contributing
458
+
459
+ Pull requests and issues are welcome at <https://github.com/assinafy/ruby-sdk>.
460
+
461
+ ## License
462
+
463
+ MIT. See [LICENSE](LICENSE).