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.
@@ -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