assinafy 1.5.2 → 1.7.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 +4 -4
- data/CHANGELOG.md +61 -0
- data/README.en.md +815 -0
- data/README.md +608 -480
- data/docs/API_REFERENCE.md +64 -7
- data/lib/assinafy/client.rb +15 -2
- data/lib/assinafy/errors.rb +56 -0
- data/lib/assinafy/oauth.rb +245 -0
- data/lib/assinafy/resources/account_resource.rb +5 -5
- data/lib/assinafy/resources/assignment_resource.rb +68 -9
- data/lib/assinafy/resources/document_resource.rb +9 -9
- data/lib/assinafy/resources/oauth_resource.rb +370 -0
- data/lib/assinafy/resources/user_resource.rb +5 -5
- data/lib/assinafy/version.rb +1 -1
- data/lib/assinafy.rb +2 -0
- data/sig/assinafy.rbs +70 -0
- metadata +7 -18
data/README.en.md
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
# Assinafy Ruby SDK
|
|
2
|
+
|
|
3
|
+
*[Leia em português](README.md) · English*
|
|
4
|
+
|
|
5
|
+
[](https://github.com/assinafy/ruby-sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](https://rubygems.org/gems/assinafy)
|
|
7
|
+
|
|
8
|
+
Ruby SDK for the [Assinafy API v1](https://api.assinafy.com.br/v1/docs).
|
|
9
|
+
|
|
10
|
+
The SDK exposes every Assinafy API v1 operation — including the OAuth 2.1 flow — and the complete supported template lifecycle. The checked-in [`spec/api_coverage_spec.rb`](spec/api_coverage_spec.rb) validates that each route maps uniquely to a public SDK method.
|
|
11
|
+
|
|
12
|
+
- **Source:** <https://github.com/assinafy/ruby-sdk>
|
|
13
|
+
- **Issues:** <https://github.com/assinafy/ruby-sdk/issues>
|
|
14
|
+
- **API docs:** <https://api.assinafy.com.br/v1/docs>
|
|
15
|
+
- **Ruby SDK API reference:** [`docs/API_REFERENCE.md`](docs/API_REFERENCE.md)
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Ruby 3.2+ (maintained support: 3.3+; 3.2 is legacy/EOL compatibility)
|
|
20
|
+
- Bundler
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
From RubyGems.org:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem 'assinafy'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bundle install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
From GitHub Packages (mirror):
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
source 'https://rubygems.pkg.github.com/assinafy' do
|
|
38
|
+
gem 'assinafy'
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
You'll need a personal access token with `read:packages` scope, configured via:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bundle config https://rubygems.pkg.github.com/assinafy USERNAME:TOKEN
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
require 'assinafy'
|
|
52
|
+
|
|
53
|
+
client = Assinafy::Client.new(
|
|
54
|
+
api_key: ENV.fetch('ASSINAFY_API_KEY'),
|
|
55
|
+
account_id: ENV.fetch('ASSINAFY_ACCOUNT_ID')
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
document = client.documents.upload({ file_path: './contract.pdf' })
|
|
59
|
+
signer = client.signers.create(full_name: 'Alice Silva', email: 'alice@example.com')
|
|
60
|
+
|
|
61
|
+
assignment = client.assignments.create(
|
|
62
|
+
document['id'],
|
|
63
|
+
method: 'virtual',
|
|
64
|
+
signers: [{ id: signer['id'] }],
|
|
65
|
+
message: 'Please sign the attached contract.'
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
puts assignment['id']
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
client = Assinafy::Client.new(
|
|
75
|
+
api_key: 'your-api-key',
|
|
76
|
+
token: nil,
|
|
77
|
+
account_id: 'your-account-id',
|
|
78
|
+
base_url: 'https://api.assinafy.com.br/v1',
|
|
79
|
+
webhook_secret: nil,
|
|
80
|
+
timeout: 30,
|
|
81
|
+
logger: Logger.new($stdout)
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- `api_key:` sends `X-Api-Key` (preferred).
|
|
86
|
+
- `token:` sends `Authorization: Bearer ...`. Pass either a login session token or an OAuth 2.1 access token
|
|
87
|
+
(see [OAuth 2.1](#oauth-21)).
|
|
88
|
+
- Configure exactly one credential. If both are supplied, the SDK sends only `X-Api-Key`.
|
|
89
|
+
- A client can also be created with no credentials for authentication, OAuth, and public/signer endpoints.
|
|
90
|
+
- `base_url:` must be an absolute `http`/`https` URL. Anything else — a scheme-less host, a relative path, or
|
|
91
|
+
another scheme — raises `Assinafy::ValidationError` instead of attaching your credentials to it. A trailing
|
|
92
|
+
slash is stripped.
|
|
93
|
+
- Account-scoped methods document a per-call account override for multi-workspace tenants.
|
|
94
|
+
- Provide a `Logger`-compatible `logger:` to observe upload/assignment/webhook lifecycle messages.
|
|
95
|
+
- Requests send `User-Agent: Assinafy-Ruby-SDK/v<Assinafy::VERSION>`; the suffix always follows the gem version.
|
|
96
|
+
|
|
97
|
+
`Client.from_config(hash)` accepts string- or symbol-keyed hashes (e.g. parsed YAML).
|
|
98
|
+
|
|
99
|
+
## Resources
|
|
100
|
+
|
|
101
|
+
`Assinafy::Client` exposes thirteen accessors — twelve API resources for the current
|
|
102
|
+
OpenAPI operations and sandbox-live template routes, plus the local `webhook_verifier` helper:
|
|
103
|
+
|
|
104
|
+
| Accessor | What it covers |
|
|
105
|
+
| --------------------------- | -------------------------------------------------------------- |
|
|
106
|
+
| `client.auth` | Login, social login, password reset, API keys |
|
|
107
|
+
| `client.oauth` | OAuth 2.1 token exchange, refresh, revocation, userinfo, discovery |
|
|
108
|
+
| `client.accounts` | Account CRUD, theme, KPI stats, brand logo |
|
|
109
|
+
| `client.users` | User profile, notification preferences, cross-account KPIs |
|
|
110
|
+
| `client.documents` | Upload, list, search, rename, download, delete, verify, tags |
|
|
111
|
+
| `client.signers` | Workspace signer CRUD + signer self-service endpoints |
|
|
112
|
+
| `client.signer_documents` | Signer-authenticated multi-document operations + search |
|
|
113
|
+
| `client.assignments` | List/create/sign/decline/resend/estimate assignments |
|
|
114
|
+
| `client.templates` | Template creation (file upload), get, list, update, delete |
|
|
115
|
+
| `client.tags` | Workspace tags |
|
|
116
|
+
| `client.fields` | Field definitions + validation + type catalog |
|
|
117
|
+
| `client.webhooks` | Subscription, event-type catalog, dispatch history, retries |
|
|
118
|
+
| `client.webhook_verifier` | Optional HMAC-SHA256 verifier for signed deliveries |
|
|
119
|
+
|
|
120
|
+
### Authentication
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
client.auth.login(email: 'user@example.com', password: 'secret')
|
|
124
|
+
client.auth.social_login(provider: 'google', token: 'id-token', has_accepted_terms: true)
|
|
125
|
+
client.auth.link_social_login(provider: 'google', token: 'id-token')
|
|
126
|
+
client.auth.create_api_key(password: 'secret')
|
|
127
|
+
client.auth.get_api_key
|
|
128
|
+
client.auth.delete_api_key
|
|
129
|
+
client.auth.change_password(email: 'user@example.com', password: 'old', new_password: 'new')
|
|
130
|
+
client.auth.request_password_reset(email: 'user@example.com')
|
|
131
|
+
client.auth.reset_password(email: 'user@example.com', new_password: 'new', token: 'reset-token')
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### OAuth 2.1
|
|
135
|
+
|
|
136
|
+
Use OAuth when an application acts **on behalf of a user**, with that user's permission — marketplace apps,
|
|
137
|
+
third-party integrations, AI assistants. Unlike an API key, the token is scoped to one workspace and carries only
|
|
138
|
+
the scopes the user approved. It never reaches billing, account lifecycle, credential management, or admin
|
|
139
|
+
surfaces, whatever scopes it holds.
|
|
140
|
+
|
|
141
|
+
The flow is authorization-code with **mandatory PKCE**; the authorization server accepts `S256` only.
|
|
142
|
+
|
|
143
|
+
| Scope | Grants |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `documents:read` | Read documents, pages, tags, signers, assignments, activity |
|
|
146
|
+
| `documents:write` | Create, update, delete documents and manage their signers and assignments |
|
|
147
|
+
| `templates:read` | Read templates, pages, roles, fields, tags |
|
|
148
|
+
| `templates:write` | Create, update, delete templates |
|
|
149
|
+
| `account:read` | Read the workspace profile, theme, logo |
|
|
150
|
+
| `openid` | Identify the user (`sub` claim) and enable `/oauth/userinfo` |
|
|
151
|
+
| `profile` | Include the user's name in the claims |
|
|
152
|
+
| `email` | Include the user's email and verification status in the claims |
|
|
153
|
+
| `offline_access` | Be issued a refresh token |
|
|
154
|
+
|
|
155
|
+
**1. Redirect the user.** Store the verifier and state in the session first.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
verifier = Assinafy::OAuth.generate_code_verifier
|
|
159
|
+
state = Assinafy::OAuth.generate_state
|
|
160
|
+
|
|
161
|
+
session[:assinafy_code_verifier] = verifier
|
|
162
|
+
session[:assinafy_state] = state
|
|
163
|
+
|
|
164
|
+
redirect_to Assinafy::OAuth.authorization_url(
|
|
165
|
+
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
|
|
166
|
+
redirect_uri: 'https://app.example.com/oauth/callback',
|
|
167
|
+
code_verifier: verifier,
|
|
168
|
+
scope: %w[documents:read documents:write offline_access],
|
|
169
|
+
state: state
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The `code_challenge` is derived from the verifier; the verifier itself never appears in the URL.
|
|
174
|
+
|
|
175
|
+
**2. Exchange the code.** Compare `state` before exchanging — that is the CSRF check.
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
raise 'state mismatch' unless params[:state] == session.delete(:assinafy_state)
|
|
179
|
+
|
|
180
|
+
tokens = Assinafy::Client.new.oauth.exchange_code(
|
|
181
|
+
code: params.fetch(:code),
|
|
182
|
+
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
|
|
183
|
+
code_verifier: session.delete(:assinafy_code_verifier),
|
|
184
|
+
redirect_uri: 'https://app.example.com/oauth/callback'
|
|
185
|
+
)
|
|
186
|
+
# => { 'access_token' => ..., 'token_type' => 'Bearer', 'expires_in' => 3600,
|
|
187
|
+
# 'refresh_token' => ..., 'scope' => 'documents:read documents:write' }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**3. Act as the user.**
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
user_client = Assinafy::Client.new(
|
|
194
|
+
token: tokens.fetch('access_token'),
|
|
195
|
+
account_id: ENV.fetch('ASSINAFY_ACCOUNT_ID')
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
user_client.documents.list
|
|
199
|
+
user_client.oauth.userinfo # => { 'sub' => ..., 'name' => ..., 'email' => ... }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Refresh and revoke.**
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
client.oauth.refresh(refresh_token: stored_refresh_token, client_id: client_id)
|
|
206
|
+
|
|
207
|
+
client.oauth.revoke(
|
|
208
|
+
token: stored_refresh_token, client_id: client_id, token_type_hint: 'refresh_token'
|
|
209
|
+
)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
A refresh token exists only when `offline_access` was requested *and* consented. Revoking a refresh token also
|
|
213
|
+
invalidates the access tokens issued from it. Every revoke outcome returns `200` — including an unknown or
|
|
214
|
+
already-revoked token — so the endpoint cannot be used to probe whether a token exists.
|
|
215
|
+
|
|
216
|
+
> The SDK does **not** refresh automatically. Persist `expires_in`, refresh before expiry, and treat
|
|
217
|
+
> `invalid_grant` as a signal to restart the authorization flow.
|
|
218
|
+
|
|
219
|
+
**Discovery**, instead of hardcoding endpoints:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
client.oauth.protected_resource_metadata['authorization_servers']
|
|
223
|
+
# => ["https://auth.assinafy.com.br"]
|
|
224
|
+
|
|
225
|
+
client.oauth.authorization_server_metadata['code_challenge_methods_supported']
|
|
226
|
+
# => ["S256"]
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`authorization_server_metadata` reaches a different host, so — like `/oauth/token` and `/oauth/revoke` — the SDK
|
|
230
|
+
sends it with no workspace credentials attached.
|
|
231
|
+
|
|
232
|
+
**Errors.** OAuth routes answer with the flat RFC 6749 object rather than this API's envelope, so the SDK raises
|
|
233
|
+
`Assinafy::OAuthError` (a subclass of `Assinafy::ApiError`):
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
rescue Assinafy::OAuthError => e
|
|
237
|
+
e.error # => "invalid_grant"
|
|
238
|
+
e.error_description # => "The authorization code is invalid or has expired."
|
|
239
|
+
e.context[:www_authenticate] # on a 403, names the missing scope
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Accounts
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
client.accounts.list # accounts the user can access
|
|
246
|
+
client.accounts.get # the current account (or pass an id)
|
|
247
|
+
client.accounts.create(name: 'Acme Inc.')
|
|
248
|
+
client.accounts.update({ name: 'Acme Renamed' })
|
|
249
|
+
client.accounts.delete(force: true, account_id_override: 'account-id')
|
|
250
|
+
client.accounts.theme # { account_name, primary_color, secondary_color, logo }
|
|
251
|
+
client.accounts.stats(granularity: 'monthly', month: '2026-06') # account KPI rows
|
|
252
|
+
client.accounts.upload_logo({ file_path: './logo.png' })
|
|
253
|
+
client.accounts.download_logo # raw bytes; raises ApiError on HTTP 404 when unset
|
|
254
|
+
client.accounts.delete_logo
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Users
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
client.users.me # OpenAPI: AuthUser; some sandboxes: { user:, accounts: }; data is passed through
|
|
261
|
+
client.users.stats(granularity: 'monthly') # cross-account KPI rows
|
|
262
|
+
client.users.notification_preferences # returns all nine owner-email preferences
|
|
263
|
+
client.users.update_notification_preferences(SignerDeclined: false) # partial request; returns all nine
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Both stats methods return rows with `period`, `documents_uploaded`, `documents_sent`, `signature_requests`,
|
|
267
|
+
`signature_requests_notification_email`, `signature_requests_notification_whatsapp`,
|
|
268
|
+
`signature_requests_notification_bypass`, `signature_requests_verification_email`,
|
|
269
|
+
`signature_requests_verification_whatsapp`, `signature_requests_verification_bypass`,
|
|
270
|
+
`signature_requests_verification_digital_certificate`, `signature_requests_viewed`,
|
|
271
|
+
`signature_requests_completed`, and `documents_certified`.
|
|
272
|
+
|
|
273
|
+
### Documents
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
client.documents.statuses # GET /documents/statuses
|
|
277
|
+
client.documents.list(page: 1, per_page: 20, status: 'pending_signature')
|
|
278
|
+
client.documents.search('contract') # lightweight GET .../documents/search
|
|
279
|
+
client.documents.upload({ file_path: './contract.pdf' }, name: 'Contract v1')
|
|
280
|
+
client.documents.upload({ buffer: pdf_bytes, file_name: 'contract.pdf' })
|
|
281
|
+
client.documents.rename('document-id', 'renamed.pdf') # PATCH /documents/{id}
|
|
282
|
+
client.documents.get('document-id') # alias of .details
|
|
283
|
+
client.documents.wait_until_ready('document-id', max_wait_seconds: 60)
|
|
284
|
+
client.documents.activities('document-id')
|
|
285
|
+
client.documents.thumbnail('document-id') # binary PNG/JPEG
|
|
286
|
+
client.documents.download('document-id', 'certificated') # binary PDF
|
|
287
|
+
client.documents.download('document-id', 'pades') # signed PAdES artifact
|
|
288
|
+
client.documents.download_page('document-id', 'page-id')
|
|
289
|
+
client.documents.delete('document-id')
|
|
290
|
+
client.documents.verify('signature-hash')
|
|
291
|
+
client.documents.public_info('document-id')
|
|
292
|
+
client.documents.send_token('document-id') # current OpenAPI also permits no body
|
|
293
|
+
client.documents.send_token('document-id', email: 'alice@example.com') # current OpenAPI body
|
|
294
|
+
# Current sandbox deployment: recipient: 'alice@example.com', channel: 'email'
|
|
295
|
+
client.documents.list_tags('document-id')
|
|
296
|
+
client.documents.replace_tags('document-id', ['tag-id-1', 'tag-id-2'])
|
|
297
|
+
client.documents.append_tags('document-id', ['tag-id-3'])
|
|
298
|
+
# The deployed sandbox also accepts existing tag names in these arrays.
|
|
299
|
+
client.documents.detach_tag('document-id', 'tag-id')
|
|
300
|
+
|
|
301
|
+
# Template-driven creation
|
|
302
|
+
client.documents.create_from_template(
|
|
303
|
+
'template-id',
|
|
304
|
+
[{ role_id: 'role', id: 'signer-id', verification_method: 'Email', notification_methods: ['Email'] }],
|
|
305
|
+
{ name: 'Contract', message: 'Please sign', expires_at: '2026-12-31T23:59:00Z' }
|
|
306
|
+
)
|
|
307
|
+
client.documents.estimate_cost_from_template(
|
|
308
|
+
'template-id',
|
|
309
|
+
[{ role_id: 'role', id: 'signer-id', verification_method: 'Whatsapp' }]
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Convenience: signing progress derived from the embedded assignment summary
|
|
313
|
+
client.documents.fully_signed?('document-id')
|
|
314
|
+
client.documents.signing_progress('document-id')
|
|
315
|
+
# => { signed: 1, total: 2, pending: 1, percentage: 50.0 }
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Signers (workspace CRUD)
|
|
319
|
+
|
|
320
|
+
```ruby
|
|
321
|
+
client.signers.create(full_name: 'Alice Silva', email: 'alice@example.com')
|
|
322
|
+
client.signers.create(full_name: 'Bob Costa', phone: '+5548999990000') # phone -> whatsapp_phone_number
|
|
323
|
+
client.signers.get('signer-id')
|
|
324
|
+
client.signers.list(search: 'alice', per_page: 50) # returns { data:, meta: }
|
|
325
|
+
client.signers.update('signer-id', full_name: 'Alice S.', government_id: '00000000000')
|
|
326
|
+
client.signers.delete('signer-id')
|
|
327
|
+
|
|
328
|
+
# Convenience: case-insensitive lookup; nil means a successful search found no match
|
|
329
|
+
client.signers.find_by_email('alice@example.com')
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Signers (self-service, signer-access-code)
|
|
333
|
+
|
|
334
|
+
```ruby
|
|
335
|
+
client.signers.self_data(signer_access_code: 'code') # includes has_signature, has_initial, is_signature_reusable
|
|
336
|
+
client.signers.accept_terms(signer_access_code: 'code')
|
|
337
|
+
client.signers.verify_email(verification_code: '123456', signer_access_code: 'code')
|
|
338
|
+
client.signers.confirm_data('document-id', { full_name: 'Alice Silva', email: 'alice@example.com', government_id: '00000000000' }, signer_access_code: 'code')
|
|
339
|
+
client.signers.upload_signature(png_bytes, signer_access_code: 'code', type: 'signature', content_type: 'image/png')
|
|
340
|
+
# => nil for the documented no-data envelope; some deployments return []
|
|
341
|
+
client.signers.download_signature(signer_access_code: 'code', type: 'signature')
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Assignments
|
|
345
|
+
|
|
346
|
+
Per-signer verification and notification values are published as
|
|
347
|
+
`AssignmentResource::VERIFICATION_METHODS` (`Email`, `Whatsapp`, `DigitalCertificate`) and
|
|
348
|
+
`::NOTIFICATION_METHODS` (`Email`, `Whatsapp`). `build_payload` validates both — and `step` — locally, because
|
|
349
|
+
the API's enums are closed and it reports an unknown value as a `422` only after the request has been sent, and
|
|
350
|
+
after any signers created for that assignment already exist.
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
# Virtual (no positioned fields)
|
|
354
|
+
client.assignments.create(
|
|
355
|
+
'document-id',
|
|
356
|
+
method: 'virtual',
|
|
357
|
+
signers: [{ id: 'signer-1', verification_method: 'Email', notification_methods: ['Email'], step: 1 }],
|
|
358
|
+
message: 'Please sign',
|
|
359
|
+
expires_at: '2026-12-31T23:59:00Z',
|
|
360
|
+
copy_receivers: ['cc-signer-id']
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Collect (positioned fields)
|
|
364
|
+
client.assignments.create(
|
|
365
|
+
'document-id',
|
|
366
|
+
method: 'collect',
|
|
367
|
+
signers: [{ id: 'signer-1' }],
|
|
368
|
+
entries: [{ page_id: 'page-id', fields: [{ signer_id: 'signer-1', field_id: 'field-id',
|
|
369
|
+
display_settings: { left: 100, top: 100, width: 240,
|
|
370
|
+
height: 48, fontSize: 16 } }] }]
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
client.assignments.list # GET /assignments (scoped to the account)
|
|
374
|
+
client.assignments.estimate_cost('document-id', signers: [{ verification_method: 'Whatsapp' }])
|
|
375
|
+
client.assignments.reset_expiration('document-id', 'assignment-id', '2026-12-31T23:59:00Z')
|
|
376
|
+
client.assignments.reset_expiration('document-id', 'assignment-id', nil) # clears the expiry
|
|
377
|
+
client.assignments.resend_notification('document-id', 'assignment-id', 'signer-id')
|
|
378
|
+
client.assignments.estimate_resend_cost('document-id', 'assignment-id', 'signer-id')
|
|
379
|
+
client.assignments.whatsapp_notifications('document-id', 'assignment-id')
|
|
380
|
+
|
|
381
|
+
# Signer perspective (signer-access-code authentication)
|
|
382
|
+
client.assignments.signer_document(signer_access_code: 'code', has_accepted_terms: true)
|
|
383
|
+
client.assignments.sign(
|
|
384
|
+
'document-id',
|
|
385
|
+
'assignment-id',
|
|
386
|
+
[{ item_id: 'i1', field_id: 'f1', page_id: 'p1', value: 'Alice' }],
|
|
387
|
+
signer_access_code: 'code'
|
|
388
|
+
)
|
|
389
|
+
client.assignments.decline('document-id', 'assignment-id', decline_reason: 'Clause 2', signer_access_code: 'code')
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
> 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.
|
|
393
|
+
|
|
394
|
+
### Signer documents (multi-document workflows)
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
client.signer_documents.current('signer-id', signer_access_code: 'code')
|
|
398
|
+
client.signer_documents.list('signer-id', { status: 'pending_signature' }, signer_access_code: 'code')
|
|
399
|
+
client.signer_documents.search('signer-id', 'contract', signer_access_code: 'code')
|
|
400
|
+
client.signer_documents.sign_multiple(%w[doc-1 doc-2], signer_access_code: 'code')
|
|
401
|
+
client.signer_documents.decline_multiple(%w[doc-1 doc-2], decline_reason: 'No', signer_access_code: 'code')
|
|
402
|
+
client.signer_documents.download('signer-id', 'document-id', 'pades') # public: no access code needed
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Templates
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
client.templates.list(search: 'contract', per_page: 25)
|
|
409
|
+
client.templates.get('template-id')
|
|
410
|
+
client.templates.create({ file_path: './contract.pdf' }) # multipart file upload
|
|
411
|
+
client.templates.create({ buffer: pdf_bytes, file_name: 'contract.pdf' })
|
|
412
|
+
client.templates.update('template-id', name: 'Renamed template')
|
|
413
|
+
client.templates.delete('template-id')
|
|
414
|
+
client.templates.download_page('template-id', 'page-id') # binary image bytes
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
> 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.
|
|
418
|
+
|
|
419
|
+
### Tags
|
|
420
|
+
|
|
421
|
+
```ruby
|
|
422
|
+
client.tags.list(search: 'contract')
|
|
423
|
+
client.tags.create(name: 'Contracts', color: 'ff8800')
|
|
424
|
+
client.tags.update('tag-id', name: 'Sales Contracts', color: nil)
|
|
425
|
+
client.tags.delete('tag-id') # fails with 409 if the tag is in use
|
|
426
|
+
client.tags.delete('tag-id', force: true) # detaches from documents/templates first
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Fields
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
client.fields.types # GET /field-types
|
|
433
|
+
client.fields.list(include_inactive: true, include_standard: false)
|
|
434
|
+
client.fields.create(type: 'text', name: 'Internal code', regex: '/[A-Z]{3}-[0-9]{4}/')
|
|
435
|
+
client.fields.get('field-id')
|
|
436
|
+
client.fields.update('field-id', name: 'Renamed')
|
|
437
|
+
client.fields.delete('field-id')
|
|
438
|
+
|
|
439
|
+
# Authenticated as a workspace user
|
|
440
|
+
client.fields.validate('field-id', 'ABC-1234')
|
|
441
|
+
# Or authenticated via signer-access-code
|
|
442
|
+
client.fields.validate('field-id', 'ABC-1234', signer_access_code: 'code')
|
|
443
|
+
client.fields.validate_multiple(
|
|
444
|
+
[{ field_id: 'a', value: '1' }, { field_id: 'b', value: 'value@example.com' }],
|
|
445
|
+
signer_access_code: 'code'
|
|
446
|
+
)
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Webhooks
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
client.webhooks.list_event_types # GET /webhooks/event-types
|
|
453
|
+
client.webhooks.get # current subscription (nil on 404)
|
|
454
|
+
client.webhooks.register(
|
|
455
|
+
url: 'https://example.com/webhooks/assinafy',
|
|
456
|
+
email: 'ops@example.com',
|
|
457
|
+
events: %w[document_ready document_prepared signer_signed_document]
|
|
458
|
+
)
|
|
459
|
+
client.webhooks.inactivate # stop deliveries, keep the event set
|
|
460
|
+
|
|
461
|
+
client.webhooks.list_dispatches(delivered: false, per_page: 50)
|
|
462
|
+
client.webhooks.retry_dispatch('dispatch-id')
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
> To stop deliveries, use `inactivate` (the API has no delete-subscription route).
|
|
466
|
+
|
|
467
|
+
#### Webhook signature verification
|
|
468
|
+
|
|
469
|
+
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:
|
|
470
|
+
|
|
471
|
+
```ruby
|
|
472
|
+
verifier = Assinafy::Support::WebhookVerifier.new(ENV.fetch('ASSINAFY_WEBHOOK_SECRET'))
|
|
473
|
+
raw_body = request.body.read
|
|
474
|
+
|
|
475
|
+
# The signature header is one your gateway injects (e.g. Cloudflare / API Gateway).
|
|
476
|
+
# Assinafy v1 does not send a signature header itself.
|
|
477
|
+
if verifier.verify(raw_body, request.headers['X-Webhook-Signature'])
|
|
478
|
+
event = verifier.extract_event(raw_body)
|
|
479
|
+
verifier.event_type(event) # => "assignment_created" (the top-level `event`)
|
|
480
|
+
verifier.event_payload(event) # => event-specific params, or nil
|
|
481
|
+
verifier.event_object(event) # => the entity acted on, e.g. the document
|
|
482
|
+
verifier.event_subject(event) # => the actor, e.g. the user
|
|
483
|
+
end
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
If no `webhook_secret` is configured, `verify` always returns `false` — safe-by-default.
|
|
487
|
+
|
|
488
|
+
## Responses
|
|
489
|
+
|
|
490
|
+
Most JSON successes use a `{ "status": ..., "message": ..., "data": ... }` envelope. The SDK
|
|
491
|
+
returns the `data` payload (a Hash for single resources, an Array for collection bodies). For
|
|
492
|
+
documented no-data envelopes containing only `status`/`message`, it returns `nil`; deployed API
|
|
493
|
+
versions that add `data` are passed through. Binary endpoints (`download`, `thumbnail`,
|
|
494
|
+
`download_page`, `download_signature`) return raw bytes as an ASCII-8BIT `String`, and
|
|
495
|
+
delete-style endpoints return `nil`.
|
|
496
|
+
|
|
497
|
+
OAuth endpoints are the exception: `/oauth/token`, `/oauth/revoke`, `/oauth/userinfo`, and the `.well-known`
|
|
498
|
+
metadata documents answer with flat RFC 6749 / OIDC / RFC 8615 objects rather than the envelope, and the SDK
|
|
499
|
+
returns those bodies unchanged.
|
|
500
|
+
|
|
501
|
+
Errors surface the envelope/framework error body through `Assinafy::ApiError`:
|
|
502
|
+
|
|
503
|
+
```ruby
|
|
504
|
+
begin
|
|
505
|
+
client.documents.details('missing-id')
|
|
506
|
+
rescue Assinafy::ApiError => e
|
|
507
|
+
e.status_code # => 404
|
|
508
|
+
e.message # => "Documento não encontrado."
|
|
509
|
+
e.error_name # => nil (or "Not Found" for framework errors)
|
|
510
|
+
e.error_code # => nil (or an integer code)
|
|
511
|
+
e.response_data # => the raw parsed body
|
|
512
|
+
end
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
The [SDK API reference](docs/API_REFERENCE.md) maps every API operation to its Ruby method and documents exact
|
|
516
|
+
authentication, parameters, request bodies, success responses, and every published schema property.
|
|
517
|
+
|
|
518
|
+
## Complete document workflow
|
|
519
|
+
|
|
520
|
+
Every call below returns the SDK value after response-envelope handling. Use the
|
|
521
|
+
[API operation table](docs/API_REFERENCE.md#api-operations) for each method's exact HTTP authentication,
|
|
522
|
+
parameters, request body, and success wire response. Named response objects link to the complete
|
|
523
|
+
[schema catalog](docs/API_REFERENCE.md#schema-catalog).
|
|
524
|
+
|
|
525
|
+
### 1. Configure a workspace client
|
|
526
|
+
|
|
527
|
+
```ruby
|
|
528
|
+
client = Assinafy::Client.new(
|
|
529
|
+
api_key: ENV.fetch('ASSINAFY_API_KEY'),
|
|
530
|
+
account_id: ENV.fetch('ASSINAFY_ACCOUNT_ID'),
|
|
531
|
+
base_url: ENV.fetch('ASSINAFY_BASE_URL', 'https://api.assinafy.com.br/v1')
|
|
532
|
+
)
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Use one authentication credential per client. Account-scoped methods use the configured account unless an
|
|
536
|
+
explicit `account_id_override` is supplied.
|
|
537
|
+
|
|
538
|
+
### 2. Upload a document or select a template
|
|
539
|
+
|
|
540
|
+
```ruby
|
|
541
|
+
document = client.documents.upload(
|
|
542
|
+
{ file_path: './contract.pdf' },
|
|
543
|
+
name: 'Customer agreement'
|
|
544
|
+
)
|
|
545
|
+
# => Document
|
|
546
|
+
|
|
547
|
+
template = client.templates.get('template-id')
|
|
548
|
+
# => Template
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
For in-memory data, pass `{ buffer: pdf_bytes, file_name: 'contract.pdf' }`. Upload requests are multipart;
|
|
552
|
+
template-based document creation is shown after the signer is available.
|
|
553
|
+
|
|
554
|
+
### 3. Wait for document processing
|
|
555
|
+
|
|
556
|
+
```ruby
|
|
557
|
+
document = client.documents.wait_until_ready(
|
|
558
|
+
document.fetch('id'),
|
|
559
|
+
max_wait_seconds: 60,
|
|
560
|
+
poll_interval_seconds: 2
|
|
561
|
+
)
|
|
562
|
+
# => Document with a ready status and populated pages
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
The helper raises `Assinafy::ValidationError` for invalid interval values and `Assinafy::Error` for a failed
|
|
566
|
+
terminal document status or timeout.
|
|
567
|
+
|
|
568
|
+
### 4. Create or reuse a signer
|
|
569
|
+
|
|
570
|
+
```ruby
|
|
571
|
+
signer = client.signers.find_by_email('signer@example.com') ||
|
|
572
|
+
client.signers.create(full_name: 'Example Signer', email: 'signer@example.com')
|
|
573
|
+
# => Signer
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### 5. Estimate and request signatures
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
signer_request = {
|
|
580
|
+
id: signer.fetch('id'),
|
|
581
|
+
verification_method: 'Email',
|
|
582
|
+
notification_methods: ['Email'],
|
|
583
|
+
step: 1
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
cost = client.assignments.estimate_cost(
|
|
587
|
+
document.fetch('id'),
|
|
588
|
+
signers: [signer_request]
|
|
589
|
+
)
|
|
590
|
+
# => CostEstimate
|
|
591
|
+
|
|
592
|
+
assignment = client.assignments.create(
|
|
593
|
+
document.fetch('id'),
|
|
594
|
+
method: 'virtual',
|
|
595
|
+
signers: [signer_request],
|
|
596
|
+
message: 'Please review and sign.',
|
|
597
|
+
expires_at: '2026-12-31T23:59:00Z'
|
|
598
|
+
)
|
|
599
|
+
# => Assignment, including signing_urls
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
The template alternative binds a signer to a template role and creates the document and assignment together:
|
|
603
|
+
|
|
604
|
+
```ruby
|
|
605
|
+
role_signer = {
|
|
606
|
+
role_id: template.fetch('roles').first.fetch('id'),
|
|
607
|
+
id: signer.fetch('id'),
|
|
608
|
+
verification_method: 'Email',
|
|
609
|
+
notification_methods: ['Email']
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
template_cost = client.documents.estimate_cost_from_template(template.fetch('id'), [role_signer])
|
|
613
|
+
# => CostEstimate
|
|
614
|
+
|
|
615
|
+
template_document = client.documents.create_from_template(
|
|
616
|
+
template.fetch('id'),
|
|
617
|
+
[role_signer],
|
|
618
|
+
name: 'Customer agreement', message: 'Please review and sign.'
|
|
619
|
+
)
|
|
620
|
+
# => Document with an embedded Assignment
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
### 6. Complete the signer flow
|
|
624
|
+
|
|
625
|
+
Signer-facing calls use the one-time access code delivered by Assinafy. They do not use the workspace API key.
|
|
626
|
+
|
|
627
|
+
```ruby
|
|
628
|
+
signing = Assinafy::Client.new(base_url: ENV.fetch('ASSINAFY_BASE_URL'))
|
|
629
|
+
access_code = ENV.fetch('ASSINAFY_SIGNER_ACCESS_CODE')
|
|
630
|
+
verification_code = ENV.fetch('ASSINAFY_VERIFICATION_CODE')
|
|
631
|
+
|
|
632
|
+
signer_data = signing.signers.self_data(signer_access_code: access_code)
|
|
633
|
+
signing.signers.accept_terms(signer_access_code: access_code) unless signer_data['has_accepted_terms']
|
|
634
|
+
signing.signers.verify_email(
|
|
635
|
+
verification_code: verification_code,
|
|
636
|
+
signer_access_code: access_code
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
signing_document = signing.assignments.signer_document(signer_access_code: access_code)
|
|
640
|
+
signing.signer_documents.sign_multiple(
|
|
641
|
+
[signing_document.fetch('id')],
|
|
642
|
+
signer_access_code: access_code
|
|
643
|
+
)
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
The virtual assignment created above uses `sign_multiple`. For a collect assignment, submit each positioned
|
|
647
|
+
item through `assignments.sign`:
|
|
648
|
+
|
|
649
|
+
```ruby
|
|
650
|
+
collect_items = signing_document.fetch('assignment').fetch('items').map do |item|
|
|
651
|
+
{
|
|
652
|
+
item_id: item.fetch('id'),
|
|
653
|
+
field_id: item.dig('field', 'id'),
|
|
654
|
+
page_id: item.dig('page', 'id'),
|
|
655
|
+
value: 'Accepted'
|
|
656
|
+
}
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
signing.assignments.sign(
|
|
660
|
+
signing_document.fetch('id'),
|
|
661
|
+
signing_document.dig('assignment', 'id'),
|
|
662
|
+
collect_items,
|
|
663
|
+
signer_access_code: access_code
|
|
664
|
+
)
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
The SDK maps the documented snake_case item keys to the API's camelCase request keys.
|
|
668
|
+
|
|
669
|
+
`DigitalCertificate` is one of the three verification methods in `AssignmentResource::VERIFICATION_METHODS`,
|
|
670
|
+
alongside the `Email` and `Whatsapp` one-time codes. It has the signer sign with their own ICP-Brasil certificate
|
|
671
|
+
(A1 or A3) via the Web PKI browser extension, producing a qualified PAdES signature. It needs the Digital
|
|
672
|
+
Certificate account feature, a CPF or CNPJ in the signer's `government_id`, and the signer alone in its signing
|
|
673
|
+
step; it costs 2 credits per signer. `build_payload` validates the enum locally, so a typo fails before any
|
|
674
|
+
signer is created.
|
|
675
|
+
|
|
676
|
+
Requesting the method is fully supported. Completing the signature is not: the two-step handshake
|
|
677
|
+
(`/signers/certificate/start`, `/signers/certificate/complete`) is absent from the API v1 machine contract — its
|
|
678
|
+
authentication and request/response schemas are not published — so the SDK does not expose those completion
|
|
679
|
+
calls rather than guess at their payloads. Contact Assinafy before enabling a digital-certificate signing flow.
|
|
680
|
+
Once it completes, the `pades` artifact returns the qualified signature.
|
|
681
|
+
|
|
682
|
+
### 7. Inspect, tag, download, and verify
|
|
683
|
+
|
|
684
|
+
```ruby
|
|
685
|
+
document_id = document.fetch('id')
|
|
686
|
+
|
|
687
|
+
activities = client.documents.activities(document_id) # => Array<DocumentActivity>
|
|
688
|
+
tags = client.documents.append_tags(document_id, ['tag-id']) # => Array<Tag>
|
|
689
|
+
client.documents.replace_tags(document_id, tags.map { |tag| tag.fetch('id') })
|
|
690
|
+
|
|
691
|
+
original_pdf = client.documents.download(document_id, 'original')
|
|
692
|
+
signed_pdf = client.documents.download(document_id, 'pades')
|
|
693
|
+
certificate_pdf = client.documents.download(document_id, 'certificated')
|
|
694
|
+
|
|
695
|
+
verification = client.documents.verify('signature-hash-from-assinafy')
|
|
696
|
+
# => DocumentVerification; inspect verification['is_valid']
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
`verify` returns Assinafy's verification result. It does not independently validate the PDF signature or its
|
|
700
|
+
certificate chain; see [Authentication and safety](docs/API_REFERENCE.md#authentication-and-safety).
|
|
701
|
+
|
|
702
|
+
### 8. Handle errors and clean up
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
begin
|
|
706
|
+
client.documents.details(document_id)
|
|
707
|
+
rescue Assinafy::ValidationError => e
|
|
708
|
+
warn e.errors.inspect
|
|
709
|
+
rescue Assinafy::ApiError => e
|
|
710
|
+
warn "Assinafy returned #{e.status_code}: #{e.message}"
|
|
711
|
+
rescue Assinafy::NetworkError => e
|
|
712
|
+
warn "Network failure: #{e.message}"
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
client.documents.delete(document_id)
|
|
716
|
+
client.signers.delete(signer.fetch('id'))
|
|
717
|
+
client.templates.delete(template.fetch('id')) if template
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
Delete resources only after downstream work is complete. Some resources can return `409` while processing or
|
|
721
|
+
while still referenced; retry only after resolving the reported state.
|
|
722
|
+
|
|
723
|
+
## Pagination
|
|
724
|
+
|
|
725
|
+
Most `*.list*` methods return `{ data: [...], meta: { ... } }` when the API includes pagination
|
|
726
|
+
headers. Ruby-style `per_page:` is transparently converted to the documented `per-page` query
|
|
727
|
+
parameter (values above the API's maximum are clamped server-side; the sandbox caps it at 50).
|
|
728
|
+
A few endpoints (e.g. `fields.list`) do not paginate and return `meta: nil`.
|
|
729
|
+
|
|
730
|
+
```ruby
|
|
731
|
+
result = client.documents.list(page: 2, per_page: 25)
|
|
732
|
+
result[:data] # => Array<Hash>
|
|
733
|
+
result[:meta] # => { current_page: 2, per_page: 25, total: 138, last_page: 6 }
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
## High-level workflow helper
|
|
737
|
+
|
|
738
|
+
`Client#upload_and_request_signatures` bundles upload + signer creation + virtual assignment into a single call:
|
|
739
|
+
|
|
740
|
+
```ruby
|
|
741
|
+
result = client.upload_and_request_signatures(
|
|
742
|
+
source: { file_path: './contract.pdf' },
|
|
743
|
+
signers: [
|
|
744
|
+
{ full_name: 'Alice Silva', email: 'alice@example.com' },
|
|
745
|
+
{ full_name: 'Bob Costa', whatsapp_phone_number: '+5548999990000' }
|
|
746
|
+
],
|
|
747
|
+
message: 'Please sign.',
|
|
748
|
+
expires_at: '2026-12-31T23:59:00Z'
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
puts result[:document]['id']
|
|
752
|
+
puts result[:assignment]['id']
|
|
753
|
+
result[:signer_ids] # => ['<sid-1>', '<sid-2>']
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
## Errors
|
|
757
|
+
|
|
758
|
+
The SDK raises one of:
|
|
759
|
+
|
|
760
|
+
- `Assinafy::ValidationError` — caller-side input invalid (missing IDs, bad email, etc.).
|
|
761
|
+
- `Assinafy::ApiError` — the API returned a non-2xx status. Includes `status_code` and `response_data`.
|
|
762
|
+
- `Assinafy::OAuthError` — an OAuth endpoint failed. Subclasses `ApiError`, and adds `error` and
|
|
763
|
+
`error_description` from the flat RFC 6749 body; on a `403`, `context[:www_authenticate]` names the missing
|
|
764
|
+
scope.
|
|
765
|
+
- `Assinafy::NetworkError` — Faraday connection error or timeout.
|
|
766
|
+
- `Assinafy::Error` — base class; other unexpected errors get wrapped here with the operation label.
|
|
767
|
+
|
|
768
|
+
All inherit a `#context` Hash with debugging metadata.
|
|
769
|
+
|
|
770
|
+
## Tests
|
|
771
|
+
|
|
772
|
+
```bash
|
|
773
|
+
bundle exec rake spec # 400+ RSpec examples, including a coverage matrix
|
|
774
|
+
bundle exec rubocop # Linting
|
|
775
|
+
bundle exec bundler-audit check # Dependency CVEs
|
|
776
|
+
ruby scripts/check_api_contract.rb --file path/to/openapi.json # validate a local contract document
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
The coverage spec validates the committed route-to-method inventory, public wrappers, aliases, and resource mappings without network access.
|
|
780
|
+
|
|
781
|
+
Ordinary pull-request CI remains network-independent. A weekly scheduled job runs
|
|
782
|
+
`scripts/check_api_contract.rb` against the upstream document. When Assinafy publishes a new contract, update
|
|
783
|
+
the SDK implementation, tests, API reference, and expected fixture together.
|
|
784
|
+
|
|
785
|
+
### Live integration tests
|
|
786
|
+
|
|
787
|
+
The suite in [`spec/integration/`](spec/integration/live_sandbox_spec.rb) exercises safe sandbox workflows across
|
|
788
|
+
every workspace resource. Signer-code, OTP, password, social-login, API-key mutation, signature upload, and
|
|
789
|
+
irreversible sign/decline calls are wire-contract tested in the default suite but are not live-automated without
|
|
790
|
+
their one-time credentials or explicit state changes. The live suite is excluded from the default run and only
|
|
791
|
+
executes when `ASSINAFY_LIVE=1` is set with credentials:
|
|
792
|
+
|
|
793
|
+
```bash
|
|
794
|
+
ASSINAFY_LIVE=1 \
|
|
795
|
+
ASSINAFY_API_KEY=... \
|
|
796
|
+
ASSINAFY_ACCOUNT_ID=... \
|
|
797
|
+
ASSINAFY_TEST_EMAIL=recipient1@example.com \
|
|
798
|
+
ASSINAFY_TEST_EMAIL2=recipient2@example.com \
|
|
799
|
+
ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
|
|
800
|
+
bundle exec rspec spec/integration
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
> 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`.
|
|
804
|
+
|
|
805
|
+
GitHub Actions provides the manual **Live Sandbox** workflow. Select a protected environment containing
|
|
806
|
+
`ASSINAFY_API_KEY`, `ASSINAFY_ACCOUNT_ID`, `ASSINAFY_TEST_EMAIL`, and `ASSINAFY_TEST_EMAIL2`; environment
|
|
807
|
+
branch/tag rules and approvals are applied before GitHub exposes those secrets to the job.
|
|
808
|
+
|
|
809
|
+
## Contributing
|
|
810
|
+
|
|
811
|
+
Pull requests and issues are welcome at <https://github.com/assinafy/ruby-sdk>.
|
|
812
|
+
|
|
813
|
+
## License
|
|
814
|
+
|
|
815
|
+
MIT. See [LICENSE](LICENSE).
|