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 +7 -0
- data/CHANGELOG.md +168 -0
- data/LICENSE +19 -0
- data/README.md +463 -0
- data/lib/assinafy/client.rb +245 -0
- data/lib/assinafy/configuration.rb +157 -0
- data/lib/assinafy/errors.rb +83 -0
- data/lib/assinafy/null_logger.rb +9 -0
- data/lib/assinafy/resources/account_resource.rb +267 -0
- data/lib/assinafy/resources/assignment_resource.rb +476 -0
- data/lib/assinafy/resources/auth_resource.rb +251 -0
- data/lib/assinafy/resources/base_resource.rb +250 -0
- data/lib/assinafy/resources/document_resource.rb +912 -0
- data/lib/assinafy/resources/field_resource.rb +274 -0
- data/lib/assinafy/resources/signer_document_resource.rb +222 -0
- data/lib/assinafy/resources/signer_resource.rb +422 -0
- data/lib/assinafy/resources/tag_resource.rb +183 -0
- data/lib/assinafy/resources/template_resource.rb +215 -0
- data/lib/assinafy/resources/user_resource.rb +182 -0
- data/lib/assinafy/resources/webhook_resource.rb +222 -0
- data/lib/assinafy/support/webhook_verifier.rb +129 -0
- data/lib/assinafy/utils.rb +122 -0
- data/lib/assinafy/version.rb +5 -0
- data/lib/assinafy.rb +25 -0
- metadata +195 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Assinafy
|
|
4
|
+
module Resources
|
|
5
|
+
# Templates — reusable document blueprints with roles and field placements.
|
|
6
|
+
#
|
|
7
|
+
# See https://api.assinafy.com.br/v1/docs#template for the documentation
|
|
8
|
+
# of the Template Object and its related endpoints.
|
|
9
|
+
class TemplateResource < BaseResource
|
|
10
|
+
# List templates with pagination metadata.
|
|
11
|
+
#
|
|
12
|
+
# @param params [Hash] documented `search`, `page`, and `per_page` query parameters
|
|
13
|
+
# @param account_id_override [String, nil]
|
|
14
|
+
# @return [Hash{Symbol=>Array,Hash}] `{ data: [Template, ...], meta: { current_page:, per_page:, ... } }`
|
|
15
|
+
# @see GET /accounts/{account_id}/templates
|
|
16
|
+
#
|
|
17
|
+
# @example Search templates and read pagination metadata
|
|
18
|
+
# client.templates.list(search: 'contract', per_page: 3)
|
|
19
|
+
# # Returns the unwrapped { data:, meta: } payload:
|
|
20
|
+
# # {
|
|
21
|
+
# # data: [
|
|
22
|
+
# # {
|
|
23
|
+
# # "id" => "fa7f3e524f3a2cc00a5ea4325e2",
|
|
24
|
+
# # "name" => "sample-contract-one-page.pdf",
|
|
25
|
+
# # "document_name" => "sample-contract-one-page.pdf",
|
|
26
|
+
# # "message" => nil,
|
|
27
|
+
# # "status" => "Ready",
|
|
28
|
+
# # "pages" => [{ "id" => "fa7f3e528d77f2b3ed786df2ce0", "number" => 1, "fields" => [] }],
|
|
29
|
+
# # "roles" => [{ "id" => "fa7f3e525bfefc71df3701eac6f", "name" => "TemplateEditor" }],
|
|
30
|
+
# # "tags" => [{ "id" => "fa8c09f3e709a8a1c82d69b1454", "name" => "HR" }],
|
|
31
|
+
# # "created_at" => "2024-07-19T15:23:03Z",
|
|
32
|
+
# # "updated_at" => "2024-07-19T15:23:03Z"
|
|
33
|
+
# # }
|
|
34
|
+
# # # ... (each entry is a Template Object; see docs for full shape)
|
|
35
|
+
# # ],
|
|
36
|
+
# # meta: { current_page: 1, per_page: 3, total: 0, last_page: 0 }
|
|
37
|
+
# # }
|
|
38
|
+
def list(params = {}, account_id_override = nil)
|
|
39
|
+
acc_id = account_id(account_id_override)
|
|
40
|
+
|
|
41
|
+
call_list('Failed to list templates') do
|
|
42
|
+
http_get("accounts/#{acc_id}/templates", params)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Fetch a template by ID.
|
|
47
|
+
#
|
|
48
|
+
# @param template_id [String]
|
|
49
|
+
# @param account_id_override [String, nil]
|
|
50
|
+
# @return [Hash] the unwrapped Template Object
|
|
51
|
+
# @see GET /accounts/{account_id}/templates/{template_id}
|
|
52
|
+
#
|
|
53
|
+
# @example Fetch a single template (includes default_document_tags, omitted from #list)
|
|
54
|
+
# client.templates.get('fa7f3e524f3a2cc00a5ea4325e2')
|
|
55
|
+
# # Returns the unwrapped Template Object:
|
|
56
|
+
# # {
|
|
57
|
+
# # "resource" => "template",
|
|
58
|
+
# # "id" => "fa7f3e524f3a2cc00a5ea4325e2",
|
|
59
|
+
# # "name" => "sample-contract-one-page.pdf",
|
|
60
|
+
# # "document_name" => "sample-contract-one-page.pdf",
|
|
61
|
+
# # "message" => nil,
|
|
62
|
+
# # "status" => "Ready",
|
|
63
|
+
# # "pages" => [
|
|
64
|
+
# # {
|
|
65
|
+
# # "id" => "fa7f3e528d77f2b3ed786df2ce0", "number" => 1, "height" => 2100, "width" => 1275,
|
|
66
|
+
# # "download_url" => "https://api.assinafy.com.br/v1/accounts/1a/templates/.../pages/.../download",
|
|
67
|
+
# # "fields" => []
|
|
68
|
+
# # }
|
|
69
|
+
# # ],
|
|
70
|
+
# # "roles" => [{ "id" => "fa7f3e525bfe", "name" => "TemplateEditor", "assignment_type" => "Editor" }],
|
|
71
|
+
# # "tags" => [{ "id" => "fa8c09f3e709a8a1c82d69b1454", "name" => "HR" }],
|
|
72
|
+
# # "default_document_tags" => [],
|
|
73
|
+
# # "created_at" => "2024-07-19T15:23:03Z",
|
|
74
|
+
# # "updated_at" => "2024-07-19T15:23:03Z"
|
|
75
|
+
# # }
|
|
76
|
+
def get(template_id, account_id_override = nil)
|
|
77
|
+
acc_id = account_id(account_id_override)
|
|
78
|
+
tmpl_id = require_id(template_id, 'Template ID')
|
|
79
|
+
|
|
80
|
+
call('Failed to fetch template') do
|
|
81
|
+
http_get("accounts/#{acc_id}/templates/#{tmpl_id}")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Create a template by uploading a source document. The endpoint requires a
|
|
86
|
+
# `multipart/form-data` file upload (verified live) — the template name
|
|
87
|
+
# defaults to the uploaded file's name and an `Editor` role is created
|
|
88
|
+
# automatically.
|
|
89
|
+
#
|
|
90
|
+
# @param source [String, Hash] a path to a PDF, or a Hash with `:file_path`
|
|
91
|
+
# (path) **or** `:buffer` + `:file_name` (raw bytes).
|
|
92
|
+
# @param options [Hash] additional multipart form fields (e.g. `:message`)
|
|
93
|
+
# @param account_id_override [String, nil]
|
|
94
|
+
# @return [Hash] the unwrapped Template Object for the created template
|
|
95
|
+
# @see POST /accounts/{account_id}/templates
|
|
96
|
+
#
|
|
97
|
+
# @example Create a template from a PDF on disk
|
|
98
|
+
# client.templates.create('/path/to/contract.pdf')
|
|
99
|
+
# # Request: POST /accounts/{account_id}/templates (multipart/form-data)
|
|
100
|
+
# # Body: file=<binary application/pdf>
|
|
101
|
+
# # Returns the unwrapped Template Object:
|
|
102
|
+
# # {
|
|
103
|
+
# # "resource" => "template",
|
|
104
|
+
# # "id" => "103b0275c2bb53a437c761ec3462",
|
|
105
|
+
# # "name" => "contract.pdf",
|
|
106
|
+
# # "document_name" => "contract.pdf",
|
|
107
|
+
# # "message" => nil,
|
|
108
|
+
# # "status" => "Uploaded",
|
|
109
|
+
# # "pages" => [],
|
|
110
|
+
# # "roles" => [{ "id" => "103b0275db76f02f0531db15b62a", "name" => "TemplateEditor",
|
|
111
|
+
# # "assignment_type" => "Editor", "created_at" => "2026-07-20T15:57:19Z",
|
|
112
|
+
# # "updated_at" => "2026-07-20T15:57:19Z" }],
|
|
113
|
+
# # "tags" => [],
|
|
114
|
+
# # "created_at" => "2026-07-20T15:57:18Z",
|
|
115
|
+
# # "updated_at" => "2026-07-20T15:57:19Z"
|
|
116
|
+
# # }
|
|
117
|
+
def create(source, options = {}, account_id_override = nil)
|
|
118
|
+
acc_id = account_id(account_id_override)
|
|
119
|
+
buffer, file_name = read_source(source)
|
|
120
|
+
|
|
121
|
+
payload = { file: file_part(buffer, file_name) }
|
|
122
|
+
options.each { |key, value| payload[key.to_s] = value unless value.nil? }
|
|
123
|
+
|
|
124
|
+
call('Failed to create template') do
|
|
125
|
+
http_post("accounts/#{acc_id}/templates", payload)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Update a template.
|
|
130
|
+
#
|
|
131
|
+
# @param template_id [String]
|
|
132
|
+
# @param payload [Hash]
|
|
133
|
+
# @param account_id_override [String, nil]
|
|
134
|
+
# @return [Hash] the unwrapped Template Object for the updated template
|
|
135
|
+
# @see PUT /accounts/{account_id}/templates/{template_id}
|
|
136
|
+
#
|
|
137
|
+
# @example Rename a template and set its default invitation message
|
|
138
|
+
# client.templates.update('fa7f3e524f3a2cc00a5ea4325e2', name: 'Renamed', message: 'Please sign')
|
|
139
|
+
# # JSON body sent: { "name": "Renamed", "message": "Please sign" }
|
|
140
|
+
# # Returns the unwrapped Template Object:
|
|
141
|
+
# # {
|
|
142
|
+
# # "resource" => "template",
|
|
143
|
+
# # "id" => "fa7f3e524f3a2cc00a5ea4325e2",
|
|
144
|
+
# # "name" => "Renamed",
|
|
145
|
+
# # "document_name" => "sample-contract-one-page.pdf",
|
|
146
|
+
# # "message" => "Please sign",
|
|
147
|
+
# # "status" => "Ready",
|
|
148
|
+
# # "created_at" => "2024-07-19T15:23:03Z",
|
|
149
|
+
# # "updated_at" => "2024-07-19T16:00:00Z"
|
|
150
|
+
# # # ... (full Template Object: pages, roles, tags; see docs for full shape)
|
|
151
|
+
# # }
|
|
152
|
+
def update(template_id, payload, account_id_override = nil)
|
|
153
|
+
acc_id = account_id(account_id_override)
|
|
154
|
+
tmpl_id = require_id(template_id, 'Template ID')
|
|
155
|
+
body = body_params(require_payload(payload, 'Template payload'))
|
|
156
|
+
|
|
157
|
+
call('Failed to update template') do
|
|
158
|
+
http_put("accounts/#{acc_id}/templates/#{tmpl_id}", body)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Delete a template.
|
|
163
|
+
#
|
|
164
|
+
# @param template_id [String]
|
|
165
|
+
# @param account_id_override [String, nil]
|
|
166
|
+
# @return [nil] always nil on success (the SDK discards the envelope body)
|
|
167
|
+
# @raise [Assinafy::ApiError] 404 "Template não encontrado." when the template ID does not exist
|
|
168
|
+
# @see DELETE /accounts/{account_id}/templates/{template_id}
|
|
169
|
+
#
|
|
170
|
+
# @example Delete a template
|
|
171
|
+
# client.templates.delete('fa7f3e524f3a2cc00a5ea4325e2')
|
|
172
|
+
# # => nil
|
|
173
|
+
#
|
|
174
|
+
# @example Deleting an unknown template raises
|
|
175
|
+
# client.templates.delete('does-not-exist')
|
|
176
|
+
# # raises Assinafy::ApiError (status 404, message "Template não encontrado.")
|
|
177
|
+
def delete(template_id, account_id_override = nil)
|
|
178
|
+
acc_id = account_id(account_id_override)
|
|
179
|
+
tmpl_id = require_id(template_id, 'Template ID')
|
|
180
|
+
|
|
181
|
+
call_void('Failed to delete template') do
|
|
182
|
+
http_delete("accounts/#{acc_id}/templates/#{tmpl_id}")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Download a rendered template page image as raw bytes. The page IDs come
|
|
187
|
+
# from the Template Page Objects on {#get} (each carries a `download_url`).
|
|
188
|
+
#
|
|
189
|
+
# @param template_id [String]
|
|
190
|
+
# @param page_id [String]
|
|
191
|
+
# @param account_id_override [String, nil]
|
|
192
|
+
# @return [String] the raw image bytes (ASCII-8BIT/binary), not the JSON envelope
|
|
193
|
+
# @raise [Assinafy::ApiError] 404 'Template "{id}" não encontrado.' when the template or page is unknown
|
|
194
|
+
# @see GET /accounts/{account_id}/templates/{template_id}/pages/{page_id}/download
|
|
195
|
+
#
|
|
196
|
+
# @example Download a page image and write it to disk
|
|
197
|
+
# bytes = client.templates.download_page('fa7f3e524f3a2cc00a5ea4325e2', 'fa7f3e528d77f2b3ed786df2ce0')
|
|
198
|
+
# # => "\x89PNG\r\n\x1A\n..." (raw binary String, encoding ASCII-8BIT)
|
|
199
|
+
# File.binwrite('page-1.png', bytes)
|
|
200
|
+
#
|
|
201
|
+
# @example Downloading an unknown page raises
|
|
202
|
+
# client.templates.download_page('bad-id', 'bad-page')
|
|
203
|
+
# # raises Assinafy::ApiError (status 404, message 'Template "{id}" não encontrado.')
|
|
204
|
+
def download_page(template_id, page_id, account_id_override = nil)
|
|
205
|
+
acc_id = account_id(account_id_override)
|
|
206
|
+
tmpl_id = require_id(template_id, 'Template ID')
|
|
207
|
+
pid = require_id(page_id, 'Page ID')
|
|
208
|
+
|
|
209
|
+
call_binary('Failed to download template page') do
|
|
210
|
+
http_get("accounts/#{acc_id}/templates/#{tmpl_id}/pages/#{pid}/download")
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Assinafy
|
|
4
|
+
module Resources
|
|
5
|
+
# The authenticated user's own profile and cross-account KPIs.
|
|
6
|
+
#
|
|
7
|
+
# See https://api.assinafy.com.br/v1/docs for the User Object.
|
|
8
|
+
class UserResource < BaseResource
|
|
9
|
+
NOTIFICATION_PREFERENCE_CODES = %w[
|
|
10
|
+
DocumentCompleted
|
|
11
|
+
SignerDeclined
|
|
12
|
+
DocumentCancelled
|
|
13
|
+
DocumentAboutToExpire
|
|
14
|
+
DocumentExpired
|
|
15
|
+
DocumentExpirationReset
|
|
16
|
+
DocumentProcessingFailed
|
|
17
|
+
TemplateProcessingFailed
|
|
18
|
+
SignerWhatsappFailed
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
# Fetch the authenticated user's profile. The current OpenAPI response is
|
|
22
|
+
# an `AuthUser` directly, while some sandbox deployments return the login-like
|
|
23
|
+
# `{ 'user' => AuthUser, 'accounts' => [...] }` shape. The SDK does not reshape
|
|
24
|
+
# either form; it returns the envelope's `data` value unchanged.
|
|
25
|
+
#
|
|
26
|
+
# @return [Hash{String=>Object}] an AuthUser Hash, or the sandbox
|
|
27
|
+
# `{ 'user' => {..}, 'accounts' => [{..}] }` form
|
|
28
|
+
# @see GET /users/self
|
|
29
|
+
# @example Fetch the current user
|
|
30
|
+
# # Request: GET /users/self
|
|
31
|
+
# client.users.me
|
|
32
|
+
#
|
|
33
|
+
# # Current OpenAPI response (unwrapped data payload):
|
|
34
|
+
# {
|
|
35
|
+
# 'id' => 'user-id',
|
|
36
|
+
# 'name' => 'Example User',
|
|
37
|
+
# 'email' => 'user@example.com',
|
|
38
|
+
# 'telephone' => nil,
|
|
39
|
+
# 'government_id' => '',
|
|
40
|
+
# 'is_email_verified' => true,
|
|
41
|
+
# 'has_accepted_terms' => true,
|
|
42
|
+
# 'is_password_set' => true,
|
|
43
|
+
# 'created_at' => '2026-05-12T18:05:11Z',
|
|
44
|
+
# 'to_be_deleted_at' => nil
|
|
45
|
+
# }
|
|
46
|
+
#
|
|
47
|
+
# # Shape returned by some sandbox deployments (also passed through unchanged):
|
|
48
|
+
# {
|
|
49
|
+
# 'user' => {
|
|
50
|
+
# 'id' => 'user-id',
|
|
51
|
+
# 'name' => 'Example User',
|
|
52
|
+
# 'email' => 'user@example.com',
|
|
53
|
+
# 'telephone' => nil,
|
|
54
|
+
# 'government_id' => '',
|
|
55
|
+
# 'is_email_verified' => true,
|
|
56
|
+
# 'has_accepted_terms' => true,
|
|
57
|
+
# 'is_password_set' => true,
|
|
58
|
+
# 'created_at' => '2026-05-12T18:05:11Z',
|
|
59
|
+
# 'to_be_deleted_at' => nil
|
|
60
|
+
# },
|
|
61
|
+
# 'accounts' => [
|
|
62
|
+
# {
|
|
63
|
+
# 'id' => 'account-id',
|
|
64
|
+
# 'name' => 'Example Workspace',
|
|
65
|
+
# 'roles' => ['owner'],
|
|
66
|
+
# 'is_delete_allowed' => true,
|
|
67
|
+
# 'created_at' => '2026-05-12T18:05:11Z'
|
|
68
|
+
# }
|
|
69
|
+
# ]
|
|
70
|
+
# }
|
|
71
|
+
def me
|
|
72
|
+
call('Failed to fetch current user') do
|
|
73
|
+
http_get('users/self')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Fetch the authenticated user's cross-account document KPIs.
|
|
78
|
+
#
|
|
79
|
+
# @note Documented in the API reference but not enabled on every
|
|
80
|
+
# environment — the sandbox currently returns 404 for this route.
|
|
81
|
+
# @param granularity [String, nil] `"monthly"` or `"daily"`
|
|
82
|
+
# @param month [String, nil] e.g. `"2026-06"`
|
|
83
|
+
# @return [Array<Hash>] one KPI entry per period
|
|
84
|
+
# @see GET /users/self/stats
|
|
85
|
+
# @example Fetch monthly cross-account KPIs
|
|
86
|
+
# # Request: GET /users/self/stats?granularity=monthly
|
|
87
|
+
# client.users.stats(granularity: 'monthly')
|
|
88
|
+
#
|
|
89
|
+
# # Response (unwrapped data payload):
|
|
90
|
+
# [
|
|
91
|
+
# {
|
|
92
|
+
# 'period' => '2026-06',
|
|
93
|
+
# 'documents_uploaded' => 42,
|
|
94
|
+
# 'documents_sent' => 37,
|
|
95
|
+
# 'signature_requests' => 61,
|
|
96
|
+
# 'signature_requests_email' => 45,
|
|
97
|
+
# 'signature_requests_whatsapp' => 16,
|
|
98
|
+
# 'signature_requests_viewed' => 58,
|
|
99
|
+
# 'signature_requests_completed' => 52,
|
|
100
|
+
# 'documents_certified' => 34
|
|
101
|
+
# }
|
|
102
|
+
# ]
|
|
103
|
+
def stats(granularity: nil, month: nil)
|
|
104
|
+
call('Failed to fetch user stats') do
|
|
105
|
+
http_get('users/self/stats', query_params(granularity: granularity, month: month))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Fetch all owner-facing document email preferences. All nine keys are
|
|
110
|
+
# returned and default to true. Account and security email is not
|
|
111
|
+
# configurable through this endpoint.
|
|
112
|
+
#
|
|
113
|
+
# @return [Hash{String=>Boolean}] all nine documented preference codes
|
|
114
|
+
# @see GET /users/self/notification-preferences
|
|
115
|
+
# @example Fetch the current preferences
|
|
116
|
+
# # Request: GET /users/self/notification-preferences
|
|
117
|
+
#
|
|
118
|
+
# # Response (unwrapped data payload):
|
|
119
|
+
# {
|
|
120
|
+
# 'DocumentCompleted' => true,
|
|
121
|
+
# 'SignerDeclined' => true,
|
|
122
|
+
# 'DocumentCancelled' => true,
|
|
123
|
+
# 'DocumentAboutToExpire' => true,
|
|
124
|
+
# 'DocumentExpired' => true,
|
|
125
|
+
# 'DocumentExpirationReset' => true,
|
|
126
|
+
# 'DocumentProcessingFailed' => true,
|
|
127
|
+
# 'TemplateProcessingFailed' => true,
|
|
128
|
+
# 'SignerWhatsappFailed' => true
|
|
129
|
+
# }
|
|
130
|
+
def notification_preferences
|
|
131
|
+
call('Failed to fetch notification preferences') do
|
|
132
|
+
http_get('users/self/notification-preferences')
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Merge selected owner-facing document email preferences.
|
|
137
|
+
#
|
|
138
|
+
# Omitted keys keep their current values. The API returns the full
|
|
139
|
+
# nine-key map shown by {#notification_preferences}.
|
|
140
|
+
#
|
|
141
|
+
# @param preferences [Hash{String,Symbol=>Boolean}] non-empty partial map
|
|
142
|
+
# @return [Hash{String=>Boolean}] the full updated map
|
|
143
|
+
# @see PUT /users/self/notification-preferences
|
|
144
|
+
# @example Disable one notification
|
|
145
|
+
# client.users.update_notification_preferences(SignerDeclined: false)
|
|
146
|
+
#
|
|
147
|
+
# # Request: PUT /users/self/notification-preferences
|
|
148
|
+
# # Body: { "SignerDeclined": false }
|
|
149
|
+
#
|
|
150
|
+
# # Response (the full unwrapped preference map):
|
|
151
|
+
# {
|
|
152
|
+
# 'DocumentCompleted' => true,
|
|
153
|
+
# 'SignerDeclined' => false,
|
|
154
|
+
# 'DocumentCancelled' => true,
|
|
155
|
+
# 'DocumentAboutToExpire' => true,
|
|
156
|
+
# 'DocumentExpired' => true,
|
|
157
|
+
# 'DocumentExpirationReset' => true,
|
|
158
|
+
# 'DocumentProcessingFailed' => true,
|
|
159
|
+
# 'TemplateProcessingFailed' => true,
|
|
160
|
+
# 'SignerWhatsappFailed' => true
|
|
161
|
+
# }
|
|
162
|
+
def update_notification_preferences(preferences)
|
|
163
|
+
preferences = require_payload(preferences, 'Notification preferences')
|
|
164
|
+
raise ValidationError.new('At least one notification preference is required') if preferences.empty?
|
|
165
|
+
|
|
166
|
+
preferences.each do |code, enabled|
|
|
167
|
+
code = code.to_s
|
|
168
|
+
unless NOTIFICATION_PREFERENCE_CODES.include?(code)
|
|
169
|
+
raise ValidationError.new("Unknown notification preference: #{code}")
|
|
170
|
+
end
|
|
171
|
+
unless [true, false].include?(enabled)
|
|
172
|
+
raise ValidationError.new("Notification preference #{code} must be boolean")
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
call('Failed to update notification preferences') do
|
|
177
|
+
http_put('users/self/notification-preferences', body_params(preferences))
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Assinafy
|
|
4
|
+
module Resources
|
|
5
|
+
# Webhook subscription, event-type catalog, delivery history, and retries.
|
|
6
|
+
#
|
|
7
|
+
# See https://api.assinafy.com.br/v1/docs#webhooks for the full
|
|
8
|
+
# documentation of these endpoints.
|
|
9
|
+
class WebhookResource < BaseResource
|
|
10
|
+
# Create or replace the account's webhook subscription. The API uses
|
|
11
|
+
# `PUT subscriptions` for both create and update semantics, hence the
|
|
12
|
+
# name `register` (with an `update` alias).
|
|
13
|
+
#
|
|
14
|
+
# @param payload [Hash]
|
|
15
|
+
# @option payload [String] :url endpoint that will receive events
|
|
16
|
+
# @option payload [String] :email contact email for delivery health
|
|
17
|
+
# @option payload [Array<String>] :events event-type IDs (see {#list_event_types})
|
|
18
|
+
# @option payload [Boolean] :is_active default `true` when omitted
|
|
19
|
+
# @param account_id_override [String, nil]
|
|
20
|
+
# @return [Hash] the subscription object: { events:, is_active:, url:, email:, updated_at: }
|
|
21
|
+
# @see PUT /accounts/{account_id}/webhooks/subscriptions
|
|
22
|
+
# @example Register (or replace) the subscription
|
|
23
|
+
# client.webhooks.register(
|
|
24
|
+
# url: 'https://example.com/webhook',
|
|
25
|
+
# email: 'ops@example.com',
|
|
26
|
+
# events: %w[document_ready document_prepared]
|
|
27
|
+
# )
|
|
28
|
+
# # PUT /accounts/{account_id}/webhooks/subscriptions
|
|
29
|
+
# # request body sent by the SDK:
|
|
30
|
+
# # {
|
|
31
|
+
# # "url": "https://example.com/webhook",
|
|
32
|
+
# # "email": "ops@example.com",
|
|
33
|
+
# # "events": ["document_ready", "document_prepared"],
|
|
34
|
+
# # "is_active": true
|
|
35
|
+
# # }
|
|
36
|
+
# # => unwrapped data payload returned:
|
|
37
|
+
# # {
|
|
38
|
+
# # events: ["document_ready", "document_prepared"],
|
|
39
|
+
# # is_active: true,
|
|
40
|
+
# # url: "https://example.com/webhook",
|
|
41
|
+
# # email: "ops@example.com",
|
|
42
|
+
# # updated_at: "2026-06-05T21:13:24Z"
|
|
43
|
+
# # }
|
|
44
|
+
def register(payload, account_id_override = nil)
|
|
45
|
+
p = require_payload(payload, 'Webhook payload').transform_keys(&:to_sym)
|
|
46
|
+
|
|
47
|
+
raise ValidationError.new('Webhook URL is required') if p[:url].to_s.strip.empty?
|
|
48
|
+
raise ValidationError.new('Webhook email is required') if p[:email].to_s.strip.empty?
|
|
49
|
+
|
|
50
|
+
events = require_array(p[:events], 'Webhook events')
|
|
51
|
+
unless events.all? { |event| event.is_a?(String) && !event.strip.empty? }
|
|
52
|
+
raise ValidationError.new('Webhook events must be non-empty Strings')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
acc_id = account_id(account_id_override)
|
|
56
|
+
|
|
57
|
+
body = {
|
|
58
|
+
url: p[:url],
|
|
59
|
+
email: p[:email],
|
|
60
|
+
events: events,
|
|
61
|
+
is_active: p.key?(:is_active) ? require_boolean(p[:is_active], 'is_active') : true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@logger.info('Registering webhook subscription')
|
|
65
|
+
|
|
66
|
+
call('Failed to register webhook') do
|
|
67
|
+
http_put("accounts/#{acc_id}/webhooks/subscriptions", body_params(body))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
alias update register
|
|
72
|
+
|
|
73
|
+
# Fetch the current webhook subscription. Returns `nil` on 404
|
|
74
|
+
# (no subscription configured yet).
|
|
75
|
+
#
|
|
76
|
+
# @param account_id_override [String, nil]
|
|
77
|
+
# @return [Hash, nil] subscription object, or `nil` when none is configured (404)
|
|
78
|
+
# @see GET /accounts/{account_id}/webhooks/subscriptions
|
|
79
|
+
# @example Fetch the current subscription
|
|
80
|
+
# client.webhooks.get
|
|
81
|
+
# # GET /accounts/{account_id}/webhooks/subscriptions
|
|
82
|
+
# # => unwrapped data payload returned (nil if no subscription exists):
|
|
83
|
+
# # {
|
|
84
|
+
# # events: ["document_ready", "signer_signed_document"],
|
|
85
|
+
# # is_active: false,
|
|
86
|
+
# # url: "https://example.com/sdk-smoke-webhook",
|
|
87
|
+
# # email: "webhook@example.com",
|
|
88
|
+
# # updated_at: "2026-06-05T21:13:24Z"
|
|
89
|
+
# # }
|
|
90
|
+
def get(account_id_override = nil)
|
|
91
|
+
acc_id = account_id(account_id_override)
|
|
92
|
+
|
|
93
|
+
call_optional('Failed to fetch webhook subscription') do
|
|
94
|
+
http_get("accounts/#{acc_id}/webhooks/subscriptions")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Inactivate (but keep) the account's webhook subscription. Stops
|
|
99
|
+
# deliveries without losing the configured event set.
|
|
100
|
+
#
|
|
101
|
+
# @param account_id_override [String, nil]
|
|
102
|
+
# @return [Hash] the subscription object with `is_active: false`; the event set is preserved
|
|
103
|
+
# @see PUT /accounts/{account_id}/webhooks/inactivate
|
|
104
|
+
# @example Inactivate without losing the configured events
|
|
105
|
+
# client.webhooks.inactivate
|
|
106
|
+
# # PUT /accounts/{account_id}/webhooks/inactivate (no request body)
|
|
107
|
+
# # => unwrapped data payload returned:
|
|
108
|
+
# # {
|
|
109
|
+
# # events: ["document_ready", "document_prepared"],
|
|
110
|
+
# # is_active: false,
|
|
111
|
+
# # url: "https://example.com/webhook",
|
|
112
|
+
# # email: "ops@example.com",
|
|
113
|
+
# # updated_at: "2026-06-05T21:13:24Z"
|
|
114
|
+
# # }
|
|
115
|
+
def inactivate(account_id_override = nil)
|
|
116
|
+
acc_id = account_id(account_id_override)
|
|
117
|
+
|
|
118
|
+
@logger.info('Inactivating webhook subscription')
|
|
119
|
+
|
|
120
|
+
call('Failed to inactivate webhook subscription') do
|
|
121
|
+
http_put("accounts/#{acc_id}/webhooks/inactivate")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Catalogue of supported event-type identifiers.
|
|
126
|
+
#
|
|
127
|
+
# @return [Array<Hash>] each entry is { id:, description: } (18 event types available)
|
|
128
|
+
# @see GET /webhooks/event-types
|
|
129
|
+
# @example List subscribable event types
|
|
130
|
+
# client.webhooks.list_event_types
|
|
131
|
+
# # GET /webhooks/event-types
|
|
132
|
+
# # => unwrapped data payload returned (18 entries):
|
|
133
|
+
# # [
|
|
134
|
+
# # { id: "document_uploaded", description: "Triggered when the User has uploaded a Document" },
|
|
135
|
+
# # { id: "document_metadata_ready", description: "Triggered when the document is ready to be prepared..." },
|
|
136
|
+
# # { id: "document_prepared", description: "Triggered when the User prepares a Document." },
|
|
137
|
+
# # { id: "assignment_created", description: "Triggered when the User created an assignment..." },
|
|
138
|
+
# # { id: "signature_requested", description: "Triggered when the User requested signature..." },
|
|
139
|
+
# # { id: "document_ready", description: "Triggered when the last Signer signs the Document..." },
|
|
140
|
+
# # { id: "signer_created", description: "Triggered when the User created a Signer" },
|
|
141
|
+
# # { id: "signer_email_verified", description: "Triggered when Signer's email has been verified..." }
|
|
142
|
+
# # # ... (see docs for the full 18-event catalogue)
|
|
143
|
+
# # ]
|
|
144
|
+
def list_event_types
|
|
145
|
+
call('Failed to list webhook event types') do
|
|
146
|
+
http_get('webhooks/event-types')
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# List webhook delivery attempts (dispatches) with pagination metadata.
|
|
151
|
+
#
|
|
152
|
+
# @param params [Hash] `event`, `delivered`, `from`, `to`, `page`, `per-page`
|
|
153
|
+
# @param account_id_override [String, nil]
|
|
154
|
+
# @return [Hash{Symbol=>Array,Hash}] `{ data: [dispatch, ...], meta: { current_page:, per_page:, total:,
|
|
155
|
+
# last_page: } }`
|
|
156
|
+
# @see GET /accounts/{account_id}/webhooks
|
|
157
|
+
# @example List delivery attempts, filtered to undelivered
|
|
158
|
+
# client.webhooks.list_dispatches(delivered: false, 'per-page': 20)
|
|
159
|
+
# # GET /accounts/{account_id}/webhooks?delivered=false&per-page=20
|
|
160
|
+
# # => unwrapped data payload returned (pagination from x-pagination-* headers):
|
|
161
|
+
# # {
|
|
162
|
+
# # data: [
|
|
163
|
+
# # {
|
|
164
|
+
# # id: "dispatch-id",
|
|
165
|
+
# # event: "signature_requested",
|
|
166
|
+
# # activity_id: 15431,
|
|
167
|
+
# # endpoint: "https://example.com/webhook",
|
|
168
|
+
# # payload: { id: 15431, event: "signature_requested", object: {}, subject: {}, payload: {} },
|
|
169
|
+
# # delivered: false,
|
|
170
|
+
# # http_status: 404,
|
|
171
|
+
# # response_body: "{\"success\":false,...}",
|
|
172
|
+
# # error: "Client error: `POST https://example.com/webhook` resulted in a 404 ...",
|
|
173
|
+
# # created_at: "2026-07-20T15:57:38Z",
|
|
174
|
+
# # updated_at: "2026-07-20T15:57:38Z"
|
|
175
|
+
# # }
|
|
176
|
+
# # # ... (see docs for full dispatch shape)
|
|
177
|
+
# # ],
|
|
178
|
+
# # meta: { current_page: 1, per_page: 20, total: 2, last_page: 1 }
|
|
179
|
+
# # }
|
|
180
|
+
def list_dispatches(params = {}, account_id_override = nil)
|
|
181
|
+
acc_id = account_id(account_id_override)
|
|
182
|
+
|
|
183
|
+
call_list('Failed to list webhook dispatches') do
|
|
184
|
+
http_get("accounts/#{acc_id}/webhooks", params)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Force a single dispatch to be re-attempted.
|
|
189
|
+
#
|
|
190
|
+
# @param dispatch_id [String]
|
|
191
|
+
# @param account_id_override [String, nil]
|
|
192
|
+
# @return [Hash] the freshly created dispatch entry (same shape as {#list_dispatches}, plus `resource`)
|
|
193
|
+
# @see POST /accounts/{account_id}/webhooks/{dispatch_id}/retry
|
|
194
|
+
# @example Force a single dispatch to be re-attempted
|
|
195
|
+
# client.webhooks.retry_dispatch('dispatch-id')
|
|
196
|
+
# # POST /accounts/{account_id}/webhooks/dispatch-id/retry (no request body)
|
|
197
|
+
# # => unwrapped data payload returned:
|
|
198
|
+
# # {
|
|
199
|
+
# # resource: "activity_dispatching_history",
|
|
200
|
+
# # id: "dispatch-id",
|
|
201
|
+
# # event: "signature_requested",
|
|
202
|
+
# # activity_id: 15431,
|
|
203
|
+
# # endpoint: "https://example.com/webhook",
|
|
204
|
+
# # payload: { id: 15431, event: "signature_requested", object: {}, subject: {} },
|
|
205
|
+
# # delivered: true,
|
|
206
|
+
# # http_status: 200,
|
|
207
|
+
# # response_body: "OK",
|
|
208
|
+
# # error: nil,
|
|
209
|
+
# # created_at: "2026-07-20T15:57:38Z",
|
|
210
|
+
# # updated_at: "2026-07-20T15:57:39Z"
|
|
211
|
+
# # }
|
|
212
|
+
def retry_dispatch(dispatch_id, account_id_override = nil)
|
|
213
|
+
acc_id = account_id(account_id_override)
|
|
214
|
+
did = require_id(dispatch_id, 'Dispatch ID')
|
|
215
|
+
|
|
216
|
+
call('Failed to retry webhook dispatch') do
|
|
217
|
+
http_post("accounts/#{acc_id}/webhooks/#{did}/retry")
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|