zai_payment 2.9.0 → 2.9.1

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.
@@ -1,244 +0,0 @@
1
- # Webhook Signature Verification Implementation
2
-
3
- ## Overview
4
-
5
- This document summarizes the implementation of webhook signature verification for the Zai Payment Ruby gem, following Zai's security specifications.
6
-
7
- ## What Was Implemented
8
-
9
- ### 1. Core Functionality
10
-
11
- #### `create_secret_key(secret_key:)`
12
- - Creates and registers a secret key with Zai for webhook signature generation
13
- - **Validation**:
14
- - Minimum 32 bytes
15
- - ASCII characters only
16
- - **Endpoint**: `POST /webhooks/secret_key`
17
- - **Returns**: Response object with registered secret key
18
-
19
- #### `verify_signature(payload:, signature_header:, secret_key:, tolerance: 300)`
20
- - Verifies that a webhook request came from Zai
21
- - **Features**:
22
- - HMAC SHA256 signature verification
23
- - Timestamp validation (prevents replay attacks)
24
- - Constant-time comparison (prevents timing attacks)
25
- - Support for multiple signatures in header
26
- - Configurable tolerance window (default: 5 minutes)
27
- - **Returns**: `true` if valid, `false` if invalid
28
- - **Raises**: `ValidationError` for malformed headers or expired timestamps
29
-
30
- #### `generate_signature(payload, secret_key, timestamp = Time.now.to_i)`
31
- - Utility method to generate signatures
32
- - Useful for testing and webhook simulation
33
- - Uses HMAC SHA256 with base64url encoding (no padding)
34
- - **Returns**: Base64url-encoded signature string
35
-
36
- ### 2. Security Best Practices
37
-
38
- ✅ **HMAC SHA256**: Industry-standard cryptographic hashing
39
- ✅ **Constant-time comparison**: Prevents timing attacks
40
- ✅ **Timestamp validation**: Prevents replay attacks
41
- ✅ **Base64 URL-safe encoding**: Compatible with HTTP headers
42
- ✅ **Configurable tolerance**: Flexible for network latency
43
- ✅ **Multi-signature support**: Handles key rotation scenarios
44
-
45
- ### 3. Test Coverage
46
-
47
- **Total Tests**: 95 (all passing ✅)
48
- **New Tests**: 56 test cases for webhook signature verification
49
-
50
- Test categories:
51
- - ✅ Secret key creation (valid, invalid, missing)
52
- - ✅ Signature generation (known values, default timestamp)
53
- - ✅ Signature verification (valid, invalid, expired)
54
- - ✅ Header parsing (malformed, missing components)
55
- - ✅ Multiple signatures handling
56
- - ✅ Edge cases and error scenarios
57
-
58
- **RSpec Compliance**: All test blocks follow the requirement of max 2 examples per `it` block.
59
-
60
- ### 4. Documentation
61
-
62
- #### Updated Files:
63
- 1. **`docs/webhooks.md`** (170+ lines added)
64
- - Complete security section
65
- - Step-by-step setup guide
66
- - Rails controller examples
67
- - Troubleshooting guide
68
- - Security best practices
69
- - References to official Zai documentation
70
-
71
- 2. **`examples/webhooks.md`** (400+ lines added)
72
- - Complete workflow from setup to testing
73
- - Rails controller implementation
74
- - RSpec test examples
75
- - Sinatra example
76
- - Rack middleware example
77
- - Background job processing pattern
78
- - Idempotency pattern
79
-
80
- 3. **`readme.md`** (40+ lines added)
81
- - Quick start guide
82
- - Security features highlights
83
- - Simple usage example
84
-
85
- ### 5. Implementation Details
86
-
87
- #### File Structure:
88
- ```
89
- lib/zai_payment/resources/webhook.rb
90
- ├── Public Methods
91
- │ ├── create_secret_key(secret_key:)
92
- │ ├── verify_signature(payload:, signature_header:, secret_key:, tolerance:)
93
- │ └── generate_signature(payload, secret_key, timestamp)
94
- └── Private Methods
95
- ├── validate_secret_key!(secret_key)
96
- ├── parse_signature_header(header)
97
- ├── verify_timestamp!(timestamp, tolerance)
98
- └── secure_compare(a, b)
99
- ```
100
-
101
- #### Algorithm Implementation:
102
-
103
- 1. **Signature Generation**:
104
- ```ruby
105
- signed_payload = "#{timestamp}.#{payload}"
106
- digest = OpenSSL::Digest.new('sha256')
107
- hash = OpenSSL::HMAC.digest(digest, secret_key, signed_payload)
108
- signature = Base64.urlsafe_encode64(hash, padding: false)
109
- ```
110
-
111
- 2. **Verification Process**:
112
- - Parse header: `t=timestamp,v=signature`
113
- - Validate timestamp is within tolerance
114
- - Generate expected signature
115
- - Compare using constant-time comparison
116
- - Return true if any signature matches
117
-
118
- ## Usage Examples
119
-
120
- ### Basic Setup
121
-
122
- ```ruby
123
- require 'securerandom'
124
-
125
- # 1. Generate secret key
126
- secret_key = SecureRandom.alphanumeric(32)
127
-
128
- # 2. Register with Zai
129
- ZaiPayment.webhooks.create_secret_key(secret_key: secret_key)
130
-
131
- # 3. Store securely
132
- ENV['ZAI_WEBHOOK_SECRET'] = secret_key
133
- ```
134
-
135
- ### Rails Controller
136
-
137
- ```ruby
138
- class WebhooksController < ApplicationController
139
- skip_before_action :verify_authenticity_token
140
-
141
- def zai_webhook
142
- payload = request.body.read
143
- signature_header = request.headers['Webhooks-signature']
144
-
145
- if ZaiPayment.webhooks.verify_signature(
146
- payload: payload,
147
- signature_header: signature_header,
148
- secret_key: ENV['ZAI_WEBHOOK_SECRET']
149
- )
150
- process_webhook(JSON.parse(payload))
151
- render json: { status: 'success' }
152
- else
153
- render json: { error: 'Invalid signature' }, status: :unauthorized
154
- end
155
- end
156
- end
157
- ```
158
-
159
- ### Testing
160
-
161
- ```ruby
162
- RSpec.describe WebhooksController do
163
- let(:secret_key) { SecureRandom.alphanumeric(32) }
164
- let(:payload) { { event: 'transaction.updated' }.to_json }
165
- let(:timestamp) { Time.now.to_i }
166
-
167
- it 'accepts valid webhooks' do
168
- signature = ZaiPayment::Resources::Webhook.new.generate_signature(
169
- payload, secret_key, timestamp
170
- )
171
-
172
- request.headers['Webhooks-signature'] = "t=#{timestamp},v=#{signature}"
173
- post :zai_webhook, body: payload
174
-
175
- expect(response).to have_http_status(:ok)
176
- end
177
- end
178
- ```
179
-
180
- ## API Reference
181
-
182
- ### Method Signatures
183
-
184
- ```ruby
185
- # Create secret key
186
- ZaiPayment.webhooks.create_secret_key(
187
- secret_key: String # Required, min 32 bytes, ASCII only
188
- ) # => Response
189
-
190
- # Verify signature
191
- ZaiPayment.webhooks.verify_signature(
192
- payload: String, # Required, raw request body
193
- signature_header: String, # Required, 'Webhooks-signature' header
194
- secret_key: String, # Required, your secret key
195
- tolerance: Integer # Optional, default: 300 seconds
196
- ) # => Boolean
197
-
198
- # Generate signature
199
- ZaiPayment.webhooks.generate_signature(
200
- payload, # String, request body
201
- secret_key, # String, your secret key
202
- timestamp = Time.now.to_i # Integer, Unix timestamp
203
- ) # => String (base64url-encoded signature)
204
- ```
205
-
206
- ## Standards Compliance
207
-
208
- ✅ **Zai API Specification**: Follows [official documentation](https://developer.hellozai.com/docs/verify-webhook-signatures)
209
- ✅ **RFC 2104**: HMAC implementation
210
- ✅ **RFC 4648**: Base64url encoding
211
- ✅ **OWASP Best Practices**: Timing attack prevention
212
-
213
- ## Testing
214
-
215
- Run all tests:
216
- ```bash
217
- bundle exec rspec
218
- ```
219
-
220
- Run webhook tests only:
221
- ```bash
222
- bundle exec rspec spec/zai_payment/resources/webhook_spec.rb
223
- ```
224
-
225
- ## References
226
-
227
- - [Zai Webhook Signature Documentation](https://developer.hellozai.com/docs/verify-webhook-signatures)
228
- - [Create Secret Key API](https://developer.hellozai.com/reference/createsecretkey)
229
- - [Ruby OpenSSL Documentation](https://ruby-doc.org/stdlib-3.0.0/libdoc/openssl/rdoc/OpenSSL/HMAC.html)
230
-
231
- ## Next Steps
232
-
233
- 1. ✅ Implementation complete
234
- 2. ✅ Tests passing (95/95)
235
- 3. ✅ Documentation complete
236
- 4. 🔄 Ready for code review
237
- 5. 📦 Ready for release
238
-
239
- ---
240
-
241
- **Implementation Date**: October 22, 2025
242
- **Test Coverage**: 100%
243
- **Standards**: OWASP, RFC 2104, RFC 4648
244
-
data/docs/webhooks.md DELETED
@@ -1,417 +0,0 @@
1
- # Zai Payment Webhook Implementation
2
-
3
- ## Overview
4
- This document provides a summary of the webhook implementation in the zai_payment gem.
5
-
6
- ## Architecture
7
-
8
- ### Core Components
9
-
10
- 1. **Client** (`lib/zai_payment/client.rb`)
11
- - Base HTTP client for making API requests
12
- - Handles authentication automatically via TokenProvider
13
- - Supports GET, POST, PATCH, DELETE methods
14
- - Manages connection with proper headers and JSON encoding/decoding
15
-
16
- 2. **Response** (`lib/zai_payment/response.rb`)
17
- - Wraps Faraday responses
18
- - Provides convenient methods: `success?`, `client_error?`, `server_error?`
19
- - Automatically raises appropriate errors based on HTTP status
20
- - Extracts data and metadata from response body
21
-
22
- 3. **Webhook Resource** (`lib/zai_payment/resources/webhook.rb`)
23
- - Implements all CRUD operations for webhooks
24
- - Full input validation
25
- - Clean, documented API
26
-
27
- 4. **Enhanced Error Handling** (`lib/zai_payment/errors.rb`)
28
- - Specific error classes for different scenarios
29
- - Makes debugging and error handling easier
30
-
31
- ## API Methods
32
-
33
- ### List Webhooks
34
- ```ruby
35
- ZaiPayment.webhooks.list(limit: 10, offset: 0)
36
- ```
37
- - Returns paginated list of webhooks
38
- - Response includes `data` (array of webhooks) and `meta` (pagination info)
39
-
40
- ### Show Webhook
41
- ```ruby
42
- ZaiPayment.webhooks.show(webhook_id)
43
- ```
44
- - Returns details of a specific webhook
45
- - Raises `NotFoundError` if webhook doesn't exist
46
-
47
- ### Create Webhook
48
- ```ruby
49
- ZaiPayment.webhooks.create(
50
- url: 'https://example.com/webhook',
51
- object_type: 'transactions',
52
- enabled: true,
53
- description: 'Optional description'
54
- )
55
- ```
56
- - Validates URL format
57
- - Validates required fields
58
- - Returns created webhook with ID
59
-
60
- ### Update Webhook
61
- ```ruby
62
- ZaiPayment.webhooks.update(
63
- webhook_id,
64
- url: 'https://example.com/new-webhook',
65
- enabled: false
66
- )
67
- ```
68
- - All fields are optional
69
- - Only updates provided fields
70
- - Validates URL format if URL is provided
71
-
72
- ### Delete Webhook
73
- ```ruby
74
- ZaiPayment.webhooks.delete(webhook_id)
75
- ```
76
- - Permanently deletes the webhook
77
- - Returns 204 No Content on success
78
-
79
- ## Error Handling
80
-
81
- The gem provides specific error classes:
82
-
83
- | Error Class | HTTP Status | Description |
84
- |------------|-------------|-------------|
85
- | `ValidationError` | 400, 422 | Invalid input data |
86
- | `UnauthorizedError` | 401 | Authentication failed |
87
- | `ForbiddenError` | 403 | Access denied |
88
- | `NotFoundError` | 404 | Resource not found |
89
- | `RateLimitError` | 429 | Too many requests |
90
- | `ServerError` | 5xx | Server-side error |
91
- | `TimeoutError` | - | Request timeout |
92
- | `ConnectionError` | - | Connection failed |
93
-
94
- Example:
95
- ```ruby
96
- begin
97
- response = ZaiPayment.webhooks.create(...)
98
- rescue ZaiPayment::Errors::ValidationError => e
99
- puts "Validation failed: #{e.message}"
100
- rescue ZaiPayment::Errors::UnauthorizedError => e
101
- puts "Authentication failed: #{e.message}"
102
- end
103
- ```
104
-
105
- ## Best Practices Implemented
106
-
107
- 1. **Single Responsibility**: Each class has a clear, focused purpose
108
- 2. **DRY (Don't Repeat Yourself)**: Client and Response classes are reusable
109
- 3. **Error Handling**: Comprehensive error handling with specific error classes
110
- 4. **Input Validation**: All inputs are validated before making API calls
111
- 5. **Documentation**: Inline documentation with examples
112
- 6. **Testing**: Comprehensive test coverage using RSpec
113
- 7. **Thread Safety**: TokenProvider uses mutex for thread-safe token refresh
114
- 8. **Configuration**: Centralized configuration management
115
- 9. **RESTful Design**: Follows REST principles for resource management
116
- 10. **Response Wrapping**: Consistent response format across all methods
117
-
118
- ## Usage Examples
119
-
120
- See `examples/webhooks.rb` for complete examples including:
121
- - Basic CRUD operations
122
- - Pagination
123
- - Error handling
124
- - Custom client instances
125
-
126
- ## Testing
127
-
128
- Run the webhook tests:
129
- ```bash
130
- bundle exec rspec spec/zai_payment/resources/webhook_spec.rb
131
- ```
132
-
133
- The test suite covers:
134
- - All CRUD operations
135
- - Success and error scenarios
136
- - Input validation
137
- - Error handling
138
- - Edge cases
139
-
140
- ## Future Enhancements
141
-
142
- Potential improvements for future versions:
143
- 1. Webhook job management (list jobs, show job details)
144
- 2. ~~Webhook signature verification~~ ✅ **Implemented**
145
- 3. Webhook retry logic
146
- 4. Bulk operations
147
- 5. Async webhook operations
148
-
149
- ## Webhook Security: Signature Verification
150
-
151
- ### Overview
152
-
153
- Webhook signature verification ensures that webhook requests truly come from Zai and haven't been tampered with. This protection guards against:
154
- - **Man-in-the-middle attacks**: Verify the sender is Zai
155
- - **Replay attacks**: Timestamp verification prevents old webhooks from being reused
156
- - **Data tampering**: HMAC ensures the payload hasn't been modified
157
-
158
- ### Setup
159
-
160
- #### Step 1: Generate and Store a Secret Key
161
-
162
- First, create a secret key that will be shared between you and Zai:
163
-
164
- ```ruby
165
- require 'securerandom'
166
-
167
- # Generate a cryptographically secure secret key (at least 32 bytes)
168
- secret_key = SecureRandom.alphanumeric(32)
169
-
170
- # Store this securely in your environment variables
171
- # DO NOT commit this to version control!
172
- ENV['ZAI_WEBHOOK_SECRET'] = secret_key
173
-
174
- # Register the secret key with Zai
175
- response = ZaiPayment.webhooks.create_secret_key(secret_key: secret_key)
176
-
177
- if response.success?
178
- puts "Secret key registered successfully!"
179
- end
180
- ```
181
-
182
- **Important Security Notes:**
183
- - Store the secret key in environment variables or a secure vault (e.g., AWS Secrets Manager, HashiCorp Vault)
184
- - Never commit the secret key to version control
185
- - Rotate the secret key periodically
186
- - Use at least 32 bytes for the secret key
187
-
188
- #### Step 2: Verify Webhook Signatures
189
-
190
- In your webhook endpoint, verify each incoming request:
191
-
192
- ```ruby
193
- # Rails example
194
- class WebhooksController < ApplicationController
195
- skip_before_action :verify_authenticity_token
196
-
197
- def zai_webhook
198
- payload = request.body.read
199
- signature_header = request.headers['Webhooks-signature']
200
- secret_key = ENV['ZAI_WEBHOOK_SECRET']
201
-
202
- begin
203
- # Verify the signature
204
- if ZaiPayment.webhooks.verify_signature(
205
- payload: payload,
206
- signature_header: signature_header,
207
- secret_key: secret_key,
208
- tolerance: 300 # 5 minutes
209
- )
210
- # Signature is valid, process the webhook
211
- webhook_data = JSON.parse(payload)
212
- process_webhook(webhook_data)
213
-
214
- render json: { status: 'success' }, status: :ok
215
- else
216
- # Invalid signature
217
- render json: { error: 'Invalid signature' }, status: :unauthorized
218
- end
219
- rescue ZaiPayment::Errors::ValidationError => e
220
- # Signature verification failed (e.g., timestamp too old)
221
- Rails.logger.error "Webhook signature verification failed: #{e.message}"
222
- render json: { error: e.message }, status: :unauthorized
223
- end
224
- end
225
-
226
- private
227
-
228
- def process_webhook(data)
229
- # Your webhook processing logic here
230
- Rails.logger.info "Processing webhook: #{data['event']}"
231
- end
232
- end
233
- ```
234
-
235
- ### How It Works
236
-
237
- The verification process follows these steps:
238
-
239
- 1. **Extract Components**: Parse the `Webhooks-signature` header to get timestamp and signature(s)
240
- - Header format: `t=1257894000,v=signature1,v=signature2`
241
-
242
- 2. **Verify Timestamp**: Check that the webhook isn't too old (prevents replay attacks)
243
- - Default tolerance: 300 seconds (5 minutes)
244
- - Configurable via the `tolerance` parameter
245
-
246
- 3. **Generate Expected Signature**: Create HMAC SHA256 signature
247
- - Signed payload: `timestamp.request_body`
248
- - Uses base64url encoding (URL-safe, no padding)
249
-
250
- 4. **Compare Signatures**: Use constant-time comparison to prevent timing attacks
251
- - Returns `true` if any signature in the header matches
252
-
253
- ### Advanced Examples
254
-
255
- #### Custom Tolerance Window
256
-
257
- ```ruby
258
- # Allow webhooks up to 10 minutes old
259
- ZaiPayment.webhooks.verify_signature(
260
- payload: payload,
261
- signature_header: signature_header,
262
- secret_key: secret_key,
263
- tolerance: 600 # 10 minutes
264
- )
265
- ```
266
-
267
- #### Generate Signatures for Testing
268
-
269
- ```ruby
270
- # Generate a signature for testing your webhook endpoint
271
- payload = '{"event": "transaction.updated", "id": "txn_123"}'
272
- secret_key = ENV['ZAI_WEBHOOK_SECRET']
273
- timestamp = Time.now.to_i
274
-
275
- signature = ZaiPayment.webhooks.generate_signature(payload, secret_key, timestamp)
276
- signature_header = "t=#{timestamp},v=#{signature}"
277
-
278
- # Now use this in your test request
279
- # This is useful for integration tests
280
- ```
281
-
282
- #### Handling Multiple Signatures
283
-
284
- Zai may include multiple signatures in the header (e.g., during key rotation):
285
-
286
- ```ruby
287
- # The verify_signature method automatically handles multiple signatures
288
- # It returns true if ANY signature matches
289
- signature_header = "t=1257894000,v=old_sig,v=new_sig"
290
- result = ZaiPayment.webhooks.verify_signature(
291
- payload: payload,
292
- signature_header: signature_header,
293
- secret_key: secret_key
294
- )
295
- ```
296
-
297
- ### Testing Your Implementation
298
-
299
- Create a test to ensure your webhook endpoint properly validates signatures:
300
-
301
- ```ruby
302
- require 'rails_helper'
303
-
304
- RSpec.describe WebhooksController, type: :controller do
305
- let(:secret_key) { SecureRandom.alphanumeric(32) }
306
- let(:payload) { { event: 'transaction.updated', id: 'txn_123' }.to_json }
307
- let(:timestamp) { Time.now.to_i }
308
-
309
- before do
310
- ENV['ZAI_WEBHOOK_SECRET'] = secret_key
311
- end
312
-
313
- describe 'POST #zai_webhook' do
314
- context 'with valid signature' do
315
- it 'processes the webhook' do
316
- signature = ZaiPayment::Resources::Webhook.new.generate_signature(
317
- payload, secret_key, timestamp
318
- )
319
-
320
- request.headers['Webhooks-signature'] = "t=#{timestamp},v=#{signature}"
321
- post :zai_webhook, body: payload
322
-
323
- expect(response).to have_http_status(:ok)
324
- end
325
- end
326
-
327
- context 'with invalid signature' do
328
- it 'rejects the webhook' do
329
- request.headers['Webhooks-signature'] = "t=#{timestamp},v=invalid_sig"
330
- post :zai_webhook, body: payload
331
-
332
- expect(response).to have_http_status(:unauthorized)
333
- end
334
- end
335
- end
336
- end
337
- ```
338
-
339
- ### Troubleshooting
340
-
341
- #### Common Issues
342
-
343
- 1. **"Invalid signature header: missing or invalid timestamp"**
344
- - Ensure the header format is correct: `t=timestamp,v=signature`
345
- - Check that timestamp is a valid Unix timestamp
346
-
347
- 2. **"Webhook timestamp is outside tolerance"**
348
- - Check your server's clock synchronization (use NTP)
349
- - Increase the tolerance if network latency is high
350
- - Log the timestamp difference to diagnose timing issues
351
-
352
- 3. **Signature doesn't match**
353
- - Verify you're using the raw request body (not parsed JSON)
354
- - Ensure the secret key matches what you registered with Zai
355
- - Check for any character encoding issues
356
-
357
- #### Debugging Tips
358
-
359
- ```ruby
360
- # Enable detailed logging for debugging
361
- def verify_webhook_with_logging(payload, signature_header, secret_key)
362
- webhook = ZaiPayment::Resources::Webhook.new
363
-
364
- begin
365
- # Extract timestamp and signature
366
- timestamp = signature_header.match(/t=(\d+)/)[1].to_i
367
- signature = signature_header.match(/v=([^,]+)/)[1]
368
-
369
- # Log details
370
- Rails.logger.debug "Webhook timestamp: #{timestamp}"
371
- Rails.logger.debug "Current time: #{Time.now.to_i}"
372
- Rails.logger.debug "Time difference: #{Time.now.to_i - timestamp}s"
373
- Rails.logger.debug "Payload length: #{payload.bytesize} bytes"
374
-
375
- # Generate expected signature for comparison
376
- expected = webhook.generate_signature(payload, secret_key, timestamp)
377
- Rails.logger.debug "Expected signature: #{expected[0..10]}..."
378
- Rails.logger.debug "Received signature: #{signature[0..10]}..."
379
-
380
- # Verify
381
- webhook.verify_signature(
382
- payload: payload,
383
- signature_header: signature_header,
384
- secret_key: secret_key
385
- )
386
- rescue => e
387
- Rails.logger.error "Verification failed: #{e.message}"
388
- false
389
- end
390
- end
391
- ```
392
-
393
- ### Security Best Practices
394
-
395
- 1. **Always Verify Signatures**: Never process webhooks without verification in production
396
- 2. **Use HTTPS**: Ensure your webhook endpoint uses HTTPS
397
- 3. **Implement Rate Limiting**: Protect against DoS attacks
398
- 4. **Log Failed Attempts**: Monitor for suspicious activity
399
- 5. **Rotate Secrets**: Periodically update your secret key
400
- 6. **Use Environment Variables**: Never hardcode secret keys
401
- 7. **Validate Payload**: After verifying the signature, validate the payload structure
402
- 8. **Idempotency**: Design webhook handlers to be idempotent (safe to replay)
403
-
404
- ### References
405
-
406
- - [Zai Webhook Signature Documentation](https://developer.hellozai.com/docs/verify-webhook-signatures)
407
- - [Create Secret Key API](https://developer.hellozai.com/reference/createsecretkey)
408
-
409
- ## API Reference
410
-
411
- For the official Zai API documentation, see:
412
- - [List Webhooks](https://developer.hellozai.com/reference/getallwebhooks)
413
- - [Show Webhook](https://developer.hellozai.com/reference/getwebhookbyid)
414
- - [Create Webhook](https://developer.hellozai.com/reference/createwebhook)
415
- - [Update Webhook](https://developer.hellozai.com/reference/updatewebhook)
416
- - [Delete Webhook](https://developer.hellozai.com/reference/deletewebhookbyid)
417
-