zerobounce-sdk 2.1.8 → 2.2.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +146 -19
  3. data/README.md +472 -410
  4. data/lib/zerobounce/api_urls.rb +9 -0
  5. data/lib/zerobounce/base_request.rb +208 -8
  6. data/lib/zerobounce/client.rb +22 -0
  7. data/lib/zerobounce/configuration.rb +24 -3
  8. data/lib/zerobounce/error.rb +18 -0
  9. data/lib/zerobounce/get_file_helper.rb +3 -7
  10. data/lib/zerobounce/mock_request.rb +31 -12
  11. data/lib/zerobounce/priority3.rb +331 -0
  12. data/lib/zerobounce/request.rb +77 -38
  13. data/lib/zerobounce/response_error.rb +117 -0
  14. data/lib/zerobounce/validate_sub_status.rb +1 -0
  15. data/lib/zerobounce/version.rb +1 -1
  16. data/lib/zerobounce/warmup.rb +467 -0
  17. data/lib/zerobounce.rb +101 -14
  18. metadata +8 -33
  19. data/.dockerignore +0 -17
  20. data/.env.example +0 -3
  21. data/.github/auto_assign.yml +0 -5
  22. data/.github/dependabot.yml +0 -35
  23. data/.github/workflows/auto_assign_ci.yaml +0 -21
  24. data/.github/workflows/codeql.yml +0 -105
  25. data/.github/workflows/publish.yml +0 -131
  26. data/.github/workflows/sdk_ci.yml +0 -29
  27. data/.gitignore +0 -70
  28. data/.rspec +0 -2
  29. data/.ruby-version +0 -1
  30. data/CODE_OF_CONDUCT.md +0 -69
  31. data/CONTRIBUTING.md +0 -112
  32. data/Dockerfile +0 -17
  33. data/Gemfile +0 -13
  34. data/Gemfile.lock +0 -135
  35. data/Rakefile +0 -26
  36. data/SECURITY.md +0 -21
  37. data/bin/bundle +0 -105
  38. data/bin/console +0 -7
  39. data/bin/setup +0 -6
  40. data/data/zerobounce-ai-scoring.csv +0 -31
  41. data/data/zerobounce-batch-validation.csv +0 -31
  42. data/documentation.md +0 -530
  43. data/files/scoring.csv +0 -1
  44. data/files/validation.csv +0 -1
  45. data/files/zerobounce-ai-scoring.csv +0 -31
  46. data/files/zerobounce-batch-validation.csv +0 -31
  47. data/zerobounce.gemspec +0 -49
data/README.md CHANGED
@@ -2,6 +2,13 @@
2
2
  # Zerobounce
3
3
  Ruby client for Zerobounce.net API. Developed based on [this version](https://github.com/afrase/zerobounce) by Aaron Frase.
4
4
 
5
+
6
+ ## Security
7
+
8
+ - Keep API keys on a trusted server. Do not embed them in mobile apps or browser JavaScript that untrusted users can inspect.
9
+ - Custom API base URLs (when supported) must use `https://`. Do not pass end-user-controlled hosts into those settings.
10
+ - Request URLs include `api_key` as a query parameter (ZeroBounce API contract). Do not log full request URLs or enable payload debug logging in production.
11
+
5
12
  ## Usage
6
13
  Add this line to your application's Gemfile:
7
14
  ```ruby
@@ -34,6 +41,9 @@ Zerobounce.configure do |config|
34
41
  # Optional: Set custom API URLs
35
42
  config.api_root_url = 'https://api.zerobounce.net/v2'
36
43
  config.bulk_api_root_url = 'https://bulkapi.zerobounce.net/v2'
44
+ # Optional: the root the S3 and SFTP/FTP jobs use (the bulk host without
45
+ # /v2). Left unset it is derived from bulk_api_root_url.
46
+ config.bulk_root_url = 'https://bulkapi.zerobounce.net'
37
47
  end
38
48
  ```
39
49
 
@@ -69,15 +79,24 @@ Zerobounce.config.api_root_url = 'https://api.zerobounce.net/v2'
69
79
  Zerobounce.config.bulk_api_root_url = 'https://bulkapi.zerobounce.net/v2'
70
80
  ```
71
81
 
72
- #### Method 3: Using environment variables
73
- Create a `.env` file in your project root:
82
+ #### Method 3: Using a `.env` file
83
+ The gem reads the `ZEROBOUNCE_*` environment variables, but it does not load a `.env` file itself (it did up to 2.1.8). To keep the settings in one, add the [dotenv](https://github.com/bkeepers/dotenv) gem to your Gemfile and load it before the gem is first configured:
84
+
85
+ ```ruby
86
+ require 'dotenv/load'
87
+ require 'zerobounce'
88
+ ```
89
+
90
+ The `.env` file:
74
91
  ```bash
75
92
  ZEROBOUNCE_API_KEY=your_api_key_here
76
93
  ZEROBOUNCE_API_URL=https://api.zerobounce.net/v2
77
94
  ZEROBOUNCE_BULK_API_URL=https://bulkapi.zerobounce.net/v2
95
+ # Optional. Only needed to point the S3 and SFTP/FTP jobs somewhere of their own.
96
+ ZEROBOUNCE_BULK_ROOT_URL=https://bulkapi.zerobounce.net
78
97
  ```
79
98
 
80
- The gem will automatically load these environment variables when initialized. No additional configuration needed in your code.
99
+ With the variables loaded, no further configuration is needed in your code.
81
100
 
82
101
  #### Method 4: System environment variables
83
102
  Set environment variables in your system:
@@ -85,6 +104,8 @@ Set environment variables in your system:
85
104
  export ZEROBOUNCE_API_KEY=your_api_key_here
86
105
  export ZEROBOUNCE_API_URL=https://api.zerobounce.net/v2
87
106
  export ZEROBOUNCE_BULK_API_URL=https://bulkapi.zerobounce.net/v2
107
+ # Optional. Only needed to point the S3 and SFTP/FTP jobs somewhere of their own.
108
+ export ZEROBOUNCE_BULK_ROOT_URL=https://bulkapi.zerobounce.net
88
109
  ```
89
110
 
90
111
 
@@ -94,6 +115,9 @@ export ZEROBOUNCE_BULK_API_URL=https://bulkapi.zerobounce.net/v2
94
115
  If you don't specify `api_root_url` or `bulk_api_root_url`, the gem will use the following defaults:
95
116
  - **Main API**: `https://api.zerobounce.net/v2`
96
117
  - **Bulk API**: `https://bulkapi.zerobounce.net/v2`
118
+ - **S3 and SFTP/FTP jobs**: `https://bulkapi.zerobounce.net` — the bulk host *without* `/v2`
119
+
120
+ `bulk_root_url` (`ZEROBOUNCE_BULK_ROOT_URL`) is the one base URL that is derived rather than defaulted: left unset, it is `bulk_api_root_url` with the trailing `/v2` stripped, so pointing `bulk_api_root_url` at a mock server moves the S3 and SFTP/FTP endpoints with it. Set it to send those jobs somewhere on their own.
97
121
 
98
122
  These defaults are defined in the `Zerobounce::ApiUrls` class constants:
99
123
  - `Zerobounce::ApiUrls::DEFAULT_URL` - Main API URL (Global)
@@ -142,62 +166,62 @@ end
142
166
  #### Environment Variable Priority
143
167
  The configuration follows this priority order (highest to lowest):
144
168
  1. Explicitly set in code (`config.api_root_url = '...'`)
145
- 2. Environment variables (`ZEROBOUNCE_API_URL`, `ZEROBOUNCE_BULK_API_URL`)
146
- 3. Default constants (`ApiUrls::DEFAULT_URL`, `ApiUrls::BULK_DEFAULT_URL`)
169
+ 2. Environment variables (`ZEROBOUNCE_API_URL`, `ZEROBOUNCE_BULK_API_URL`, `ZEROBOUNCE_BULK_ROOT_URL`)
170
+ 3. Default constants (`ApiUrls::DEFAULT_URL`, `ApiUrls::BULK_DEFAULT_URL`); `bulk_root_url` falls back to `bulk_api_root_url` without `/v2`
147
171
 
148
172
  Credits
149
173
  ```ruby
150
174
  Zerobounce.credits
151
- => 79 # returns -1 for invalid key
175
+ # => 79 (-1 for an invalid key)
152
176
  ```
153
177
 
154
178
  Activity
155
179
  ```ruby
156
180
  Zerobounce.activity('ss@gmail.com')
157
- => {"found"=>true, "active_in_days"=>"180"}
181
+ # => {"found"=>true, "active_in_days"=>"180"}
158
182
  ```
159
183
 
160
184
  Usage statistics
161
185
  ```ruby
162
186
  Zerobounce.api_usage(Date.today, Date.today)
163
- =>
164
- {"total"=>0,
165
- "status_valid"=>0,
166
- "status_invalid"=>0,
167
- "status_catch_all"=>0,
168
- "status_do_not_mail"=>0,
169
- "status_spamtrap"=>0,
170
- "status_unknown"=>0,
171
- "sub_status_toxic"=>0,
172
- "sub_status_disposable"=>0,
173
- "sub_status_role_based"=>0,
174
- "sub_status_possible_trap"=>0,
175
- "sub_status_global_suppression"=>0,
176
- "sub_status_timeout_exceeded"=>0,
177
- "sub_status_mail_server_temporary_error"=>0,
178
- "sub_status_mail_server_did_not_respond"=>0,
179
- "sub_status_greylisted"=>0,
180
- "sub_status_antispam_system"=>0,
181
- "sub_status_does_not_accept_mail"=>0,
182
- "sub_status_exception_occurred"=>0,
183
- "sub_status_failed_syntax_check"=>0,
184
- "sub_status_mailbox_not_found"=>0,
185
- "sub_status_unroutable_ip_address"=>0,
186
- "sub_status_possible_typo"=>0,
187
- "sub_status_no_dns_entries"=>0,
188
- "sub_status_role_based_catch_all"=>0,
189
- "sub_status_mailbox_quota_exceeded"=>0,
190
- "sub_status_forcible_disconnect"=>0,
191
- "sub_status_failed_smtp_connection"=>0,
192
- "sub_status_accept_all"=>0,
193
- "sub_status_mx_forward"=>0,
194
- "sub_status_alternate"=>0,
195
- "sub_status_allowed"=>0,
196
- "sub_status_blocked"=>0,
197
- "sub_status_gold"=>0,
198
- "sub_status_role_based_accept_all"=>0,
199
- "start_date"=>"4/28/2023",
200
- "end_date"=>"4/28/2023"}
187
+ # =>
188
+ # {"total"=>0,
189
+ # "status_valid"=>0,
190
+ # "status_invalid"=>0,
191
+ # "status_catch_all"=>0,
192
+ # "status_do_not_mail"=>0,
193
+ # "status_spamtrap"=>0,
194
+ # "status_unknown"=>0,
195
+ # "sub_status_toxic"=>0,
196
+ # "sub_status_disposable"=>0,
197
+ # "sub_status_role_based"=>0,
198
+ # "sub_status_possible_trap"=>0,
199
+ # "sub_status_global_suppression"=>0,
200
+ # "sub_status_timeout_exceeded"=>0,
201
+ # "sub_status_mail_server_temporary_error"=>0,
202
+ # "sub_status_mail_server_did_not_respond"=>0,
203
+ # "sub_status_greylisted"=>0,
204
+ # "sub_status_antispam_system"=>0,
205
+ # "sub_status_does_not_accept_mail"=>0,
206
+ # "sub_status_exception_occurred"=>0,
207
+ # "sub_status_failed_syntax_check"=>0,
208
+ # "sub_status_mailbox_not_found"=>0,
209
+ # "sub_status_unroutable_ip_address"=>0,
210
+ # "sub_status_possible_typo"=>0,
211
+ # "sub_status_no_dns_entries"=>0,
212
+ # "sub_status_role_based_catch_all"=>0,
213
+ # "sub_status_mailbox_quota_exceeded"=>0,
214
+ # "sub_status_forcible_disconnect"=>0,
215
+ # "sub_status_failed_smtp_connection"=>0,
216
+ # "sub_status_accept_all"=>0,
217
+ # "sub_status_mx_forward"=>0,
218
+ # "sub_status_alternate"=>0,
219
+ # "sub_status_allowed"=>0,
220
+ # "sub_status_blocked"=>0,
221
+ # "sub_status_gold"=>0,
222
+ # "sub_status_role_based_accept_all"=>0,
223
+ # "start_date"=>"4/28/2023",
224
+ # "end_date"=>"4/28/2023"}
201
225
  ```
202
226
 
203
227
  ### Email Validation
@@ -206,159 +230,164 @@ Validate single eMail
206
230
  ```ruby
207
231
  Zerobounce.validate('valid@example.com')
208
232
  # Zerobounce.validate('valid@example.com', '192.168.0.1') # optional IP address
209
- =>
210
- {"address"=>"valid@example.com",
211
- "status"=>"valid",
212
- "sub_status"=>"",
213
- "free_email"=>false,
214
- "did_you_mean"=>nil,
215
- "account"=>nil,
216
- "domain"=>nil,
217
- "domain_age_days"=>"9692",
218
- "smtp_provider"=>"example",
219
- "mx_found"=>"true",
220
- "mx_record"=>"mx.example.com",
221
- "firstname"=>"zero",
222
- "lastname"=>"bounce",
223
- "gender"=>"male",
224
- "country"=>nil,
225
- "region"=>nil,
226
- "city"=>nil,
227
- "zipcode"=>nil,
228
- "processed_at"=>"2023-04-28 15:18:45.631"}
233
+ # =>
234
+ # {"address"=>"valid@example.com",
235
+ # "status"=>"valid",
236
+ # "sub_status"=>"",
237
+ # "free_email"=>false,
238
+ # "did_you_mean"=>nil,
239
+ # "account"=>nil,
240
+ # "domain"=>nil,
241
+ # "domain_age_days"=>"9692",
242
+ # "smtp_provider"=>"example",
243
+ # "mx_found"=>"true",
244
+ # "mx_record"=>"mx.example.com",
245
+ # "firstname"=>"zero",
246
+ # "lastname"=>"bounce",
247
+ # "gender"=>"male",
248
+ # "country"=>nil,
249
+ # "region"=>nil,
250
+ # "city"=>nil,
251
+ # "zipcode"=>nil,
252
+ # "processed_at"=>"2023-04-28 15:18:45.631"}
229
253
  ```
230
254
 
231
255
  Validate batch
256
+
257
+ `validate_batch` returns the whole response: a Hash with `email_batch` (one result per address) and `errors`. Up to 2.1.8 it returned only the `email_batch` Array and raised on any error; now a problem with one address is listed in `errors` and does not raise. An account-level error (reported for the address `all`, such as an invalid key or no credits left) still raises.
258
+
232
259
  ```ruby
233
- emails
234
- =>
235
- ["disposable@example.com",
236
- "invalid@example.com",
237
- "valid@example.com",
238
- "toxic@example.com",
239
- "donotmail@example.com",
240
- "spamtrap@example.com"]
241
-
242
- # ip_addresses
243
- => ["192.168.0.1", "192.168.0.1", "192.168.0.1", "192.168.0.1", "192.168.0.1", "192.168.0.1"]
244
- # Zerobounce.validate_batch(emails, ip_addresses) # optional ip_addresses parameter
245
-
246
- Zerobounce.validate_batch(emails)
247
- =>
248
- [{"address"=>"disposable@example.com",
249
- "status"=>"do_not_mail",
250
- "sub_status"=>"disposable",
251
- "free_email"=>false,
252
- "did_you_mean"=>nil,
253
- "account"=>nil,
254
- "domain"=>nil,
255
- "domain_age_days"=>"9692",
256
- "smtp_provider"=>"example",
257
- "mx_found"=>"true",
258
- "mx_record"=>"mx.example.com",
259
- "firstname"=>"zero",
260
- "lastname"=>"bounce",
261
- "gender"=>"male",
262
- "country"=>nil,
263
- "region"=>nil,
264
- "city"=>nil,
265
- "zipcode"=>nil,
266
- "processed_at"=>"2023-04-28 15:21:44.340"},
267
- {"address"=>"invalid@example.com",
268
- "status"=>"invalid",
269
- "sub_status"=>"mailbox_not_found",
270
- "free_email"=>false,
271
- "did_you_mean"=>nil,
272
- "account"=>nil,
273
- "domain"=>nil,
274
- "domain_age_days"=>"9692",
275
- "smtp_provider"=>"example",
276
- "mx_found"=>"true",
277
- "mx_record"=>"mx.example.com",
278
- "firstname"=>"zero",
279
- "lastname"=>"bounce",
280
- "gender"=>"male",
281
- "country"=>nil,
282
- "region"=>nil,
283
- "city"=>nil,
284
- "zipcode"=>nil,
285
- "processed_at"=>"2023-04-28 15:21:44.340"},
286
- {"address"=>"valid@example.com",
287
- "status"=>"valid",
288
- "sub_status"=>"",
289
- "free_email"=>false,
290
- "did_you_mean"=>nil,
291
- "account"=>nil,
292
- "domain"=>nil,
293
- "domain_age_days"=>"9692",
294
- "smtp_provider"=>"example",
295
- "mx_found"=>"true",
296
- "mx_record"=>"mx.example.com",
297
- "firstname"=>"zero",
298
- "lastname"=>"bounce",
299
- "gender"=>"male",
300
- "country"=>nil,
301
- "region"=>nil,
302
- "city"=>nil,
303
- "zipcode"=>nil,
304
- "processed_at"=>"2023-04-28 15:21:44.340"},
305
- {"address"=>"toxic@example.com",
306
- "status"=>"do_not_mail",
307
- "sub_status"=>"toxic",
308
- "free_email"=>false,
309
- "did_you_mean"=>nil,
310
- "account"=>nil,
311
- "domain"=>nil,
312
- "domain_age_days"=>"9692",
313
- "smtp_provider"=>"example",
314
- "mx_found"=>"true",
315
- "mx_record"=>"mx.example.com",
316
- "firstname"=>"zero",
317
- "lastname"=>"bounce",
318
- "gender"=>"male",
319
- "country"=>nil,
320
- "region"=>nil,
321
- "city"=>nil,
322
- "zipcode"=>nil,
323
- "processed_at"=>"2023-04-28 15:21:44.340"},
324
- {"address"=>"donotmail@example.com",
325
- "status"=>"do_not_mail",
326
- "sub_status"=>"role_based",
327
- "free_email"=>false,
328
- "did_you_mean"=>nil,
329
- "account"=>nil,
330
- "domain"=>nil,
331
- "domain_age_days"=>"9692",
332
- "smtp_provider"=>"example",
333
- "mx_found"=>"true",
334
- "mx_record"=>"mx.example.com",
335
- "firstname"=>"zero",
336
- "lastname"=>"bounce",
337
- "gender"=>"male",
338
- "country"=>nil,
339
- "region"=>nil,
340
- "city"=>nil,
341
- "zipcode"=>nil,
342
- "processed_at"=>"2023-04-28 15:21:44.340"},
343
- {"address"=>"spamtrap@example.com",
344
- "status"=>"spamtrap",
345
- "sub_status"=>"",
346
- "free_email"=>false,
347
- "did_you_mean"=>nil,
348
- "account"=>nil,
349
- "domain"=>nil,
350
- "domain_age_days"=>"9692",
351
- "smtp_provider"=>"example",
352
- "mx_found"=>"true",
353
- "mx_record"=>"mx.example.com",
354
- "firstname"=>"zero",
355
- "lastname"=>"bounce",
356
- "gender"=>"male",
357
- "country"=>nil,
358
- "region"=>nil,
359
- "city"=>nil,
360
- "zipcode"=>nil,
361
- "processed_at"=>"2023-04-28 15:21:44.340"}]
260
+ emails = ["disposable@example.com",
261
+ "invalid@example.com",
262
+ "valid@example.com",
263
+ "toxic@example.com",
264
+ "donotmail@example.com",
265
+ "spamtrap@example.com"]
266
+ ip_addresses = ["192.168.0.1"] * emails.length
267
+
268
+ result = Zerobounce.validate_batch(emails)
269
+ # result = Zerobounce.validate_batch(emails, ip_addresses) # optional ip_addresses parameter
270
+ # => {"email_batch"=>[...], "errors"=>[]}
271
+
272
+ result['errors']
273
+ # => [] e.g. [{"email_address"=>"bad@", "error"=>"Invalid email address"}]
274
+
275
+ result['email_batch']
276
+ # =>
277
+ # [{"address"=>"disposable@example.com",
278
+ # "status"=>"do_not_mail",
279
+ # "sub_status"=>"disposable",
280
+ # "free_email"=>false,
281
+ # "did_you_mean"=>nil,
282
+ # "account"=>nil,
283
+ # "domain"=>nil,
284
+ # "domain_age_days"=>"9692",
285
+ # "smtp_provider"=>"example",
286
+ # "mx_found"=>"true",
287
+ # "mx_record"=>"mx.example.com",
288
+ # "firstname"=>"zero",
289
+ # "lastname"=>"bounce",
290
+ # "gender"=>"male",
291
+ # "country"=>nil,
292
+ # "region"=>nil,
293
+ # "city"=>nil,
294
+ # "zipcode"=>nil,
295
+ # "processed_at"=>"2023-04-28 15:21:44.340"},
296
+ # {"address"=>"invalid@example.com",
297
+ # "status"=>"invalid",
298
+ # "sub_status"=>"mailbox_not_found",
299
+ # "free_email"=>false,
300
+ # "did_you_mean"=>nil,
301
+ # "account"=>nil,
302
+ # "domain"=>nil,
303
+ # "domain_age_days"=>"9692",
304
+ # "smtp_provider"=>"example",
305
+ # "mx_found"=>"true",
306
+ # "mx_record"=>"mx.example.com",
307
+ # "firstname"=>"zero",
308
+ # "lastname"=>"bounce",
309
+ # "gender"=>"male",
310
+ # "country"=>nil,
311
+ # "region"=>nil,
312
+ # "city"=>nil,
313
+ # "zipcode"=>nil,
314
+ # "processed_at"=>"2023-04-28 15:21:44.340"},
315
+ # {"address"=>"valid@example.com",
316
+ # "status"=>"valid",
317
+ # "sub_status"=>"",
318
+ # "free_email"=>false,
319
+ # "did_you_mean"=>nil,
320
+ # "account"=>nil,
321
+ # "domain"=>nil,
322
+ # "domain_age_days"=>"9692",
323
+ # "smtp_provider"=>"example",
324
+ # "mx_found"=>"true",
325
+ # "mx_record"=>"mx.example.com",
326
+ # "firstname"=>"zero",
327
+ # "lastname"=>"bounce",
328
+ # "gender"=>"male",
329
+ # "country"=>nil,
330
+ # "region"=>nil,
331
+ # "city"=>nil,
332
+ # "zipcode"=>nil,
333
+ # "processed_at"=>"2023-04-28 15:21:44.340"},
334
+ # {"address"=>"toxic@example.com",
335
+ # "status"=>"do_not_mail",
336
+ # "sub_status"=>"toxic",
337
+ # "free_email"=>false,
338
+ # "did_you_mean"=>nil,
339
+ # "account"=>nil,
340
+ # "domain"=>nil,
341
+ # "domain_age_days"=>"9692",
342
+ # "smtp_provider"=>"example",
343
+ # "mx_found"=>"true",
344
+ # "mx_record"=>"mx.example.com",
345
+ # "firstname"=>"zero",
346
+ # "lastname"=>"bounce",
347
+ # "gender"=>"male",
348
+ # "country"=>nil,
349
+ # "region"=>nil,
350
+ # "city"=>nil,
351
+ # "zipcode"=>nil,
352
+ # "processed_at"=>"2023-04-28 15:21:44.340"},
353
+ # {"address"=>"donotmail@example.com",
354
+ # "status"=>"do_not_mail",
355
+ # "sub_status"=>"role_based",
356
+ # "free_email"=>false,
357
+ # "did_you_mean"=>nil,
358
+ # "account"=>nil,
359
+ # "domain"=>nil,
360
+ # "domain_age_days"=>"9692",
361
+ # "smtp_provider"=>"example",
362
+ # "mx_found"=>"true",
363
+ # "mx_record"=>"mx.example.com",
364
+ # "firstname"=>"zero",
365
+ # "lastname"=>"bounce",
366
+ # "gender"=>"male",
367
+ # "country"=>nil,
368
+ # "region"=>nil,
369
+ # "city"=>nil,
370
+ # "zipcode"=>nil,
371
+ # "processed_at"=>"2023-04-28 15:21:44.340"},
372
+ # {"address"=>"spamtrap@example.com",
373
+ # "status"=>"spamtrap",
374
+ # "sub_status"=>"",
375
+ # "free_email"=>false,
376
+ # "did_you_mean"=>nil,
377
+ # "account"=>nil,
378
+ # "domain"=>nil,
379
+ # "domain_age_days"=>"9692",
380
+ # "smtp_provider"=>"example",
381
+ # "mx_found"=>"true",
382
+ # "mx_record"=>"mx.example.com",
383
+ # "firstname"=>"zero",
384
+ # "lastname"=>"bounce",
385
+ # "gender"=>"male",
386
+ # "country"=>nil,
387
+ # "region"=>nil,
388
+ # "city"=>nil,
389
+ # "zipcode"=>nil,
390
+ # "processed_at"=>"2023-04-28 15:21:44.340"}]
362
391
  ```
363
392
 
364
393
  #### Batch File Validation
@@ -366,24 +395,24 @@ Zerobounce.validate_batch(emails)
366
395
  Send file
367
396
  ```ruby
368
397
  # sample file can be found in the files folder of this repository
369
- validate_file_path = File.join(Dir.pwd, 'files', 'validation.csv')
370
- => "/home/zb/wrappers/ruby-test/files/validation.csv"
371
- Zerobounce.validate_file_send(validate_file_path)
372
- =>
373
- {"success"=>true,
374
- "message"=>"File Accepted",
375
- "file_name"=>"validation.csv",
376
- "file_id"=>"75d854a6-565c-49f9-b4c8-b3344480ec4c"}
377
- # file_id will be required for next steps
378
- # optional named parameters:
379
- email_address_column: 1,
380
- first_name_column: 2,
381
- last_name_column: 3,
382
- gender_column: 4,
383
- has_header_row: true,
384
- return_url: nil, # results callback url
385
- allow_phase_2: true # optional; validation bulk sendfile only (omit or nil to skip)
386
- # Zerobounce.validate_file_send(validate_file_path, email_address_column: 1, gender_column: 4)
398
+ validate_file_path = File.join(Dir.pwd, 'files', 'validation.csv')
399
+ # => "/home/zb/wrappers/ruby-test/files/validation.csv"
400
+ Zerobounce.validate_file_send(validate_file_path)
401
+ # =>
402
+ # {"success"=>true,
403
+ # "message"=>"File Accepted",
404
+ # "file_name"=>"validation.csv",
405
+ # "file_id"=>"75d854a6-565c-49f9-b4c8-b3344480ec4c"}
406
+ # file_id will be required for next steps
407
+ # optional named parameters:
408
+ # email_address_column: 1,
409
+ # first_name_column: 2,
410
+ # last_name_column: 3,
411
+ # gender_column: 4,
412
+ # has_header_row: true,
413
+ # return_url: nil, # results callback url
414
+ # allow_phase_2: true # optional; validation bulk sendfile only (omit or nil to skip)
415
+ Zerobounce.validate_file_send(validate_file_path, email_address_column: 1, gender_column: 4)
387
416
  ```
388
417
 
389
418
  Bulk validation uses `https://bulkapi.zerobounce.net/v2`. See [v2 send file](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-send-file), [v2 file status](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-file-status), and [v2 get file](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-get-file).
@@ -391,26 +420,24 @@ Bulk validation uses `https://bulkapi.zerobounce.net/v2`. See [v2 send file](htt
391
420
  Check file
392
421
  ```ruby
393
422
  file_id = "75d854a6-565c-49f9-b4c8-b3344480ec4c"
394
- => "75d854a6-565c-49f9-b4c8-b3344480ec4c"
395
423
  Zerobounce.validate_file_check(file_id)
396
- =>
397
- {"success"=>true,
398
- "file_id"=>"75d854a6-565c-49f9-b4c8-b3344480ec4c",
399
- "file_name"=>"validation.csv",
400
- "upload_date"=>"2023-04-28T15:25:41Z",
401
- "file_status"=>"Greylisted",
402
- "complete_percentage"=>"0%",
403
- "file_phase_2_status"=>"N/A", # when present (validation bulk v2)
404
- "error_reason"=>nil,
405
- "return_url"=>nil}
424
+ # =>
425
+ # {"success"=>true,
426
+ # "file_id"=>"75d854a6-565c-49f9-b4c8-b3344480ec4c",
427
+ # "file_name"=>"validation.csv",
428
+ # "upload_date"=>"2023-04-28T15:25:41Z",
429
+ # "file_status"=>"Greylisted",
430
+ # "complete_percentage"=>"0%",
431
+ # "file_phase_2_status"=>"N/A", # when present (validation bulk v2)
432
+ # "error_reason"=>nil,
433
+ # "return_url"=>nil}
406
434
  ```
407
435
 
408
436
  Get file
409
437
  ```ruby
410
438
  file_id = "75d854a6-565c-49f9-b4c8-b3344480ec4c"
411
- => "75d854a6-565c-49f9-b4c8-b3344480ec4c"
412
439
  Zerobounce.validate_file_get(file_id)
413
- => "\"email\",\"first\",\"last\",\"gender\",\"ip\",\"ZB Status\",\"ZB Sub Status\",\"ZB Account\",\"ZB Domain\",\"ZB First Name\",\"ZB Last Name\",\"ZB Gender\",\"ZB Free Email\",\"ZB MX Found\",\"ZB MX Record\",\"ZB SMTP Provider\",\"ZB Did You Mean\"\n\"disposable@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"disposable\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"invalid@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"mailbox_not_found\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"valid@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"valid\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"toxic@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"toxic\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"donotmail@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"role_based\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"spamtrap@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"spamtrap\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"abuse@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"abuse\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"unknown@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"mail_server_temporary_error\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"catch_all@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"catch-all\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"antispam_system@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"antispam_system\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"does_not_accept_mail@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"does_not_accept_mail\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"exception_occurred@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"exception_occurred\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"failed_smtp_connection@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"failed_smtp_connection\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"failed_syntax_check@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"failed_syntax_check\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"forcible_disconnect@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"forcible_disconnect\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"global_suppression@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"global_suppression\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"greylisted@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"greylisted\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"leading_period_removed@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"valid\",\"leading_period_removed\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mail_server_did_not_respond@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"mail_server_did_not_respond\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mail_server_temporary_error@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"mail_server_temporary_error\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mailbox_quota_exceeded@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"mailbox_quota_exceeded\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mailbox_not_found@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"mailbox_not_found\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"no_dns_entries@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"no_dns_entries\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"possible_trap@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"possible_trap\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"possible_typo@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"possible_typo\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"role_based@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"role_based\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"timeout_exceeded@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"timeout_exceeded\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"unroutable_ip_address@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"unroutable_ip_address\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"free_email@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"valid\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"True\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"role_based_catch_all@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"role_based_catch_all\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n"
440
+ # => "\"email\",\"first\",\"last\",\"gender\",\"ip\",\"ZB Status\",\"ZB Sub Status\",\"ZB Account\",\"ZB Domain\",\"ZB First Name\",\"ZB Last Name\",\"ZB Gender\",\"ZB Free Email\",\"ZB MX Found\",\"ZB MX Record\",\"ZB SMTP Provider\",\"ZB Did You Mean\"\n\"disposable@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"disposable\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"invalid@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"mailbox_not_found\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"valid@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"valid\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"toxic@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"toxic\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"donotmail@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"role_based\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"spamtrap@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"spamtrap\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"abuse@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"abuse\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"unknown@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"mail_server_temporary_error\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"catch_all@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"catch-all\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"antispam_system@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"antispam_system\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"does_not_accept_mail@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"does_not_accept_mail\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"exception_occurred@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"exception_occurred\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"failed_smtp_connection@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"failed_smtp_connection\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"failed_syntax_check@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"failed_syntax_check\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"forcible_disconnect@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"forcible_disconnect\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"global_suppression@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"global_suppression\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"greylisted@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"greylisted\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"leading_period_removed@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"valid\",\"leading_period_removed\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mail_server_did_not_respond@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"mail_server_did_not_respond\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mail_server_temporary_error@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"mail_server_temporary_error\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mailbox_quota_exceeded@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"mailbox_quota_exceeded\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"mailbox_not_found@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"mailbox_not_found\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"no_dns_entries@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"no_dns_entries\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"possible_trap@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"possible_trap\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"possible_typo@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"possible_typo\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"role_based@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"role_based\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"timeout_exceeded@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"unknown\",\"timeout_exceeded\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"unroutable_ip_address@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"invalid\",\"unroutable_ip_address\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"free_email@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"valid\",\"\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"True\",\"true\",\"mx.example.com\",\"example\",\"\"\n\"role_based_catch_all@example.com\",\"First\",\"Last\",\"male\",\"127.0.0.1\",\"do_not_mail\",\"role_based_catch_all\",\"\",\"\",\"zero\",\"bounce\",\"male\",\"False\",\"true\",\"mx.example.com\",\"example\",\"\"\n"
414
441
  ```
415
442
 
416
443
  Optional [v2 get file](https://www.zerobounce.net/docs/email-validation-api-quickstart/v2-get-file) query parameters use `Zerobounce::GetFileOptions` and `Zerobounce::DownloadType` (`PHASE_1`, `PHASE_2`, `COMBINED`). Set `activity_data` for validation `validate_file_get` only; it is not sent for `scoring_file_get`.
@@ -423,18 +450,17 @@ opts = Zerobounce::GetFileOptions.new(
423
450
  csv = Zerobounce.validate_file_get(file_id, opts)
424
451
  ```
425
452
 
426
- Non-success HTTP responses or JSON error bodies (including some HTTP 200 responses with `"success": false`) raise `RuntimeError` with the API message. To inspect a raw body string, use `Zerobounce.get_file_json_indicates_error?(body)`.
453
+ Non-success HTTP responses and JSON error bodies (including HTTP 200 responses with `"success": false`) raise with the API message; see [Errors](#errors). To inspect a raw body string, use `Zerobounce.get_file_json_indicates_error?(body)`.
427
454
 
428
455
  Delete file
429
456
  ```ruby
430
457
  file_id = "75d854a6-565c-49f9-b4c8-b3344480ec4c"
431
- => "75d854a6-565c-49f9-b4c8-b3344480ec4c"
432
458
  Zerobounce.validate_file_delete(file_id)
433
- =>
434
- {"success"=>true,
435
- "message"=>"File Deleted",
436
- "file_name"=>"validation.csv",
437
- "file_id"=>"75d854a6-565c-49f9-b4c8-b3344480ec4c"}
459
+ # =>
460
+ # {"success"=>true,
461
+ # "message"=>"File Deleted",
462
+ # "file_name"=>"validation.csv",
463
+ # "file_id"=>"75d854a6-565c-49f9-b4c8-b3344480ec4c"}
438
464
  ```
439
465
 
440
466
  ### AI Scoring
@@ -442,52 +468,61 @@ Zerobounce.validate_file_delete(file_id)
442
468
  Send file
443
469
  ```ruby
444
470
  scoring_file_path = File.join(Dir.pwd, 'files', 'scoring.csv')
445
- => "/home/odin/zb/wrappers/ruby-test/files/scoring.csv"
471
+ # => "/home/odin/zb/wrappers/ruby-test/files/scoring.csv"
446
472
  Zerobounce.scoring_file_send(scoring_file_path)
447
- =>
448
- {"success"=>true,
449
- "message"=>"File Accepted",
450
- "file_name"=>"scoring.csv",
451
- "file_id"=>"89fb7262-b845-4fa1-aa25-e486347ec64e"}
473
+ # =>
474
+ # {"success"=>true,
475
+ # "message"=>"File Accepted",
476
+ # "file_name"=>"scoring.csv",
477
+ # "file_id"=>"89fb7262-b845-4fa1-aa25-e486347ec64e"}
452
478
  # file_id will be required for the next steps
453
479
  # optional named parameters
454
- email_address_column: 1,
455
- has_header_row: true,
456
- return_url: nil # results callback url
457
- # Zerobounce.scoring_file_send(scoring_file_path, email_address_column: 1)
480
+ # email_address_column: 1,
481
+ # has_header_row: true,
482
+ # return_url: nil # results callback url
483
+ Zerobounce.scoring_file_send(scoring_file_path, email_address_column: 1)
458
484
  ```
459
485
 
460
486
  File check
461
487
  ```ruby
462
488
  file_id = "89fb7262-b845-4fa1-aa25-e486347ec64e"
463
- => "89fb7262-b845-4fa1-aa25-e486347ec64e"
464
489
  Zerobounce.scoring_file_check(file_id)
465
- =>
466
- {"success"=>true,
467
- "file_id"=>"89fb7262-b845-4fa1-aa25-e486347ec64e",
468
- "file_name"=>"scoring.csv",
469
- "upload_date"=>"2023-04-28T16:57:18Z",
470
- "file_status"=>"Complete",
471
- "complete_percentage"=>"100% Complete.",
472
- "return_url"=>nil}
490
+ # =>
491
+ # {"success"=>true,
492
+ # "file_id"=>"89fb7262-b845-4fa1-aa25-e486347ec64e",
493
+ # "file_name"=>"scoring.csv",
494
+ # "upload_date"=>"2023-04-28T16:57:18Z",
495
+ # "file_status"=>"Complete",
496
+ # "complete_percentage"=>"100% Complete.",
497
+ # "return_url"=>nil}
473
498
  ```
474
499
 
475
500
  Get file
476
501
  ```ruby
477
502
  file_id = "89fb7262-b845-4fa1-aa25-e486347ec64e"
478
- => "89fb7262-b845-4fa1-aa25-e486347ec64e"
479
503
  Zerobounce.scoring_file_get(file_id)
480
- => "\"email\",\"ZeroBounceQualityScore\"\r\n\"disposable@example.com\",\"0\"\r\n\"invalid@example.com\",\"10\"\r\n\"valid@example.com\",\"10\"\r\n\"toxic@example.com\",\"2\"\r\n\"donotmail@example.com\",\"0\"\r\n\"spamtrap@example.com\",\"0\"\r\n\"abuse@example.com\",\"10\"\r\n\"unknown@example.com\",\"10\"\r\n\"catch_all@example.com\",\"10\"\r\n\"antispam_system@example.com\",\"0\"\r\n\"does_not_accept_mail@example.com\",\"0\"\r\n\"exception_occurred@example.com\",\"0\"\r\n\"failed_smtp_connection@example.com\",\"0\"\r\n\"failed_syntax_check@example.com\",\"0\"\r\n\"forcible_disconnect@example.com\",\"0\"\r\n\"global_suppression@example.com\",\"0\"\r\n\"greylisted@example.com\",\"0\"\r\n\"leading_period_removed@example.com\",\"0\"\r\n\"mail_server_did_not_respond@example.com\",\"0\"\r\n\"mail_server_temporary_error@example.com\",\"0\"\r\n\"mailbox_quota_exceeded@example.com\",\"0\"\r\n\"mailbox_not_found@example.com\",\"0\"\r\n\"no_dns_entries@example.com\",\"0\"\r\n\"possible_trap@example.com\",\"0\"\r\n\"possible_typo@example.com\",\"0\"\r\n\"role_based@example.com\",\"0\"\r\n\"timeout_exceeded@example.com\",\"0\"\r\n\"unroutable_ip_address@example.com\",\"0\"\r\n\"free_email@example.com\",\"0\"\r\n\"role_based_catch_all@example.com\",\"0\""
504
+ # => "\"email\",\"ZeroBounceQualityScore\"\r\n\"disposable@example.com\",\"0\"\r\n\"invalid@example.com\",\"10\"\r\n\"valid@example.com\",\"10\"\r\n\"toxic@example.com\",\"2\"\r\n\"donotmail@example.com\",\"0\"\r\n\"spamtrap@example.com\",\"0\"\r\n\"abuse@example.com\",\"10\"\r\n\"unknown@example.com\",\"10\"\r\n\"catch_all@example.com\",\"10\"\r\n\"antispam_system@example.com\",\"0\"\r\n\"does_not_accept_mail@example.com\",\"0\"\r\n\"exception_occurred@example.com\",\"0\"\r\n\"failed_smtp_connection@example.com\",\"0\"\r\n\"failed_syntax_check@example.com\",\"0\"\r\n\"forcible_disconnect@example.com\",\"0\"\r\n\"global_suppression@example.com\",\"0\"\r\n\"greylisted@example.com\",\"0\"\r\n\"leading_period_removed@example.com\",\"0\"\r\n\"mail_server_did_not_respond@example.com\",\"0\"\r\n\"mail_server_temporary_error@example.com\",\"0\"\r\n\"mailbox_quota_exceeded@example.com\",\"0\"\r\n\"mailbox_not_found@example.com\",\"0\"\r\n\"no_dns_entries@example.com\",\"0\"\r\n\"possible_trap@example.com\",\"0\"\r\n\"possible_typo@example.com\",\"0\"\r\n\"role_based@example.com\",\"0\"\r\n\"timeout_exceeded@example.com\",\"0\"\r\n\"unroutable_ip_address@example.com\",\"0\"\r\n\"free_email@example.com\",\"0\"\r\n\"role_based_catch_all@example.com\",\"0\""
481
505
  # Optional second argument: GetFileOptions with download_type only (activity_data is not sent for scoring getfile)
482
- # Zerobounce.scoring_file_get(file_id, Zerobounce::GetFileOptions.new(download_type: Zerobounce::DownloadType::PHASE_2))
506
+ Zerobounce.scoring_file_get(file_id, Zerobounce::GetFileOptions.new(download_type: Zerobounce::DownloadType::PHASE_2))
483
507
  ```
484
508
 
485
509
  Delete file
486
510
  ```ruby
487
511
  file_id = "89fb7262-b845-4fa1-aa25-e486347ec64e"
488
- => "89fb7262-b845-4fa1-aa25-e486347ec64e"
489
512
  Zerobounce.scoring_file_delete(file_id)
490
- => {"success"=>false, "message"=>"File cannot be found."}
513
+ # =>
514
+ # {"success"=>true,
515
+ # "message"=>"File Deleted",
516
+ # "file_name"=>"scoring.csv",
517
+ # "file_id"=>"89fb7262-b845-4fa1-aa25-e486347ec64e"}
518
+
519
+ # The API answers an unknown file id - and, for a while, a file that was only
520
+ # just uploaded - with {"success": false} on an HTTP 200. That raises:
521
+ begin
522
+ Zerobounce.scoring_file_delete("00000000-0000-0000-0000-000000000000")
523
+ rescue RuntimeError => e
524
+ e.message # => "File cannot be found."
525
+ end
491
526
  ```
492
527
 
493
528
 
@@ -542,176 +577,189 @@ When `has_header_row: false` is provided to `scoring_file_send()` method, column
542
577
 
543
578
  ### Email Finder
544
579
 
545
- Guess Format by Domain
580
+ Use `find_email` to find one person's address and `find_domain` to get a domain's address formats; both take either `domain:` or `company_name:`.
581
+
582
+ Guess Format (deprecated)
583
+
584
+ `guessformat(domain, first_name: '', middle_name: '', last_name: '')` takes the domain as its only positional argument and has no `company_name:` option. It is deprecated, prints a deprecation warning on every call, and will be removed; use `find_domain` (formats) or `find_email` (one address) below.
546
585
  ```ruby
547
- # New keyword argument syntax (recommended)
548
- Zerobounce.guessformat(domain: "zerobounce.net")
549
- =>
550
- {"email"=>"",
551
- "domain"=>"zerobounce.net",
552
- "format"=>"first.last",
553
- "status"=>"",
554
- "sub_status"=>"",
555
- "confidence"=>"high",
556
- "did_you_mean"=>"",
557
- "failure_reason"=>"",
558
- "other_domain_formats"=>
559
- [{"format"=>"first", "confidence"=>"high"},
560
- {"format"=>"last.first", "confidence"=>"high"},
561
- {"format"=>"lfirst", "confidence"=>"high"},
562
- {"format"=>"lastfirst", "confidence"=>"high"},
563
- {"format"=>"firstl", "confidence"=>"high"},
564
- {"format"=>"last", "confidence"=>"medium"},
565
- {"format"=>"first.middle.last", "confidence"=>"medium"},
566
- {"format"=>"first-last", "confidence"=>"medium"},
567
- {"format"=>"l.first", "confidence"=>"medium"},
568
- {"format"=>"f.last", "confidence"=>"medium"},
569
- {"format"=>"f-last", "confidence"=>"medium"},
570
- {"format"=>"first.l", "confidence"=>"medium"},
571
- {"format"=>"first-l", "confidence"=>"medium"},
572
- {"format"=>"firstlast", "confidence"=>"medium"},
573
- {"format"=>"first_l", "confidence"=>"medium"},
574
- {"format"=>"f_last", "confidence"=>"medium"},
575
- {"format"=>"last.f", "confidence"=>"medium"},
576
- {"format"=>"last-f", "confidence"=>"medium"},
577
- {"format"=>"last-first", "confidence"=>"medium"},
578
- {"format"=>"first_last", "confidence"=>"medium"},
579
- {"format"=>"last_f", "confidence"=>"medium"},
580
- {"format"=>"last_first", "confidence"=>"medium"},
581
- {"format"=>"flast", "confidence"=>"medium"},
582
- {"format"=>"lastf", "confidence"=>"medium"},
583
- {"format"=>"l-first", "confidence"=>"low"},
584
- {"format"=>"l_first", "confidence"=>"low"}]}
585
-
586
- # With names for better accuracy (new syntax)
587
- Zerobounce.guessformat(domain: "zerobounce.net", first_name: "John", middle_name: 'Deere', last_name: "Doe")
588
-
589
- # Backwards compatible syntax (still supported)
586
+ # Deprecated - prefer Zerobounce.find_domain(domain: "zerobounce.net")
590
587
  Zerobounce.guessformat("zerobounce.net")
591
- Zerobounce.guessformat("zerobounce.net", first_name: "John", middle_name: 'Deere', last_name: "Doe")
592
- ```
588
+ # => {"email"=>"", "domain"=>"zerobounce.net", "format"=>"first.last", "confidence"=>"high", "other_domain_formats"=>[...], ...}
593
589
 
594
- Guess Format by Company Name
595
- ```ruby
596
- # New keyword argument syntax (recommended)
597
- Zerobounce.guessformat(company_name: "Zero Bounce")
598
- =>
599
- {"email"=>"",
600
- "company_name"=>"Zero Bounce",
601
- "format"=>"first.last",
602
- "status"=>"",
603
- "sub_status"=>"",
604
- "confidence"=>"high",
605
- "did_you_mean"=>"",
606
- "failure_reason"=>"",
607
- "other_domain_formats"=>[...]}
608
-
609
- # With names for better accuracy (new syntax)
610
- Zerobounce.guessformat(first_name: "John", last_name: "Doe", company_name: "Zero Bounce")
590
+ # Deprecated - prefer Zerobounce.find_email("John", domain: "zerobounce.net", middle_name: "Deere", last_name: "Doe")
591
+ Zerobounce.guessformat("zerobounce.net", first_name: "John", middle_name: 'Deere', last_name: "Doe")
611
592
  ```
612
593
 
613
594
  Find Email Address
614
595
  ```ruby
615
596
  # Find email by domain
616
597
  Zerobounce.find_email("John", domain: "zerobounce.net")
617
- =>
618
- {
619
- "email": "john@zerobounce.net",
620
- "email_confidence": "medium",
621
- "domain": "zerobounce.net",
622
- "company_name": "ZeroBounce",
623
- "did_you_mean": "",
624
- "failure_reason": ""
625
- }
598
+ # =>
599
+ # {
600
+ # "email"=>"john@zerobounce.net",
601
+ # "email_confidence"=>"medium",
602
+ # "domain"=>"zerobounce.net",
603
+ # "company_name"=>"ZeroBounce",
604
+ # "did_you_mean"=>"",
605
+ # "failure_reason"=>""
606
+ # }
626
607
 
627
608
  # Find email by company name
628
609
  Zerobounce.find_email("John", company_name: "Zero Bounce")
629
- =>
630
- {
631
- "email": "john@zerobounce.net",
632
- "email_confidence": "medium",
633
- "domain": "zerobounce.net",
634
- "company_name": "ZeroBounce",
635
- "did_you_mean": "",
636
- "failure_reason": ""
637
- }
610
+ # =>
611
+ # {
612
+ # "email"=>"john@zerobounce.net",
613
+ # "email_confidence"=>"medium",
614
+ # "domain"=>"zerobounce.net",
615
+ # "company_name"=>"ZeroBounce",
616
+ # "did_you_mean"=>"",
617
+ # "failure_reason"=>""
618
+ # }
638
619
 
639
620
  # With additional name information for better accuracy
640
621
  Zerobounce.find_email("John", domain: "zerobounce.net", middle_name: "Deere", last_name: "Doe")
622
+
623
+ # The first name is optional. Without one the call answers with the domain's
624
+ # address format rather than one person's address.
625
+ Zerobounce.find_email(domain: "zerobounce.net")
641
626
  ```
642
627
 
643
628
  Find Domain Information
644
629
  ```ruby
645
630
  # Find domain format by domain
646
631
  Zerobounce.find_domain(domain: "zerobounce.net")
647
- =>
648
- {
649
- "domain": "zerobounce.net",
650
- "company_name": "Hertza, LLC",
651
- "format": "first.last",
652
- "confidence": "high",
653
- "did_you_mean": "",
654
- "failure_reason": "",
655
- "other_domain_formats": [
656
- {"format": "first", "confidence": "high"},
657
- {"format": "last.first", "confidence": "high"},
658
- {"format": "lastfirst", "confidence": "high"},
659
- {"format": "firstl", "confidence": "high"},
660
- {"format": "lfirst", "confidence": "high"},
661
- {"format": "firstlast", "confidence": "high"},
662
- {"format": "last_middle_f", "confidence": "high"},
663
- {"format": "last", "confidence": "high"},
664
- {"format": "f.last", "confidence": "medium"},
665
- {"format": "last-f", "confidence": "medium"},
666
- {"format": "l.first", "confidence": "medium"},
667
- {"format": "last_f", "confidence": "medium"},
668
- {"format": "first.middle.last", "confidence": "medium"},
669
- {"format": "first-last", "confidence": "medium"},
670
- {"format": "last.f", "confidence": "medium"},
671
- {"format": "last_first", "confidence": "medium"},
672
- {"format": "f-last", "confidence": "medium"},
673
- {"format": "first.l", "confidence": "medium"},
674
- {"format": "first-l", "confidence": "medium"},
675
- {"format": "first_l", "confidence": "medium"},
676
- {"format": "first_last", "confidence": "medium"},
677
- {"format": "f_last", "confidence": "medium"},
678
- {"format": "last-first", "confidence": "medium"},
679
- {"format": "flast", "confidence": "medium"},
680
- {"format": "lastf", "confidence": "medium"},
681
- {"format": "l_first", "confidence": "medium"},
682
- {"format": "l-first", "confidence": "medium"},
683
- {"format": "first-middle-last", "confidence": "low"},
684
- {"format": "firstmlast", "confidence": "low"},
685
- {"format": "last.middle.first", "confidence": "low"},
686
- {"format": "last_middle_first", "confidence": "low"},
687
- {"format": "first_middle_last", "confidence": "low"},
688
- {"format": "last-middle-first", "confidence": "low"},
689
- {"format": "first-m-last", "confidence": "low"},
690
- {"format": "firstmiddlelast", "confidence": "low"},
691
- {"format": "last.m.first", "confidence": "low"},
692
- {"format": "lastmfirst", "confidence": "low"},
693
- {"format": "lastmiddlefirst", "confidence": "low"},
694
- {"format": "last_m_first", "confidence": "low"},
695
- {"format": "first.m.last", "confidence": "low"},
696
- {"format": "first_m_last", "confidence": "low"},
697
- {"format": "last-m-first", "confidence": "low"}
698
- ]
699
- }
632
+ # =>
633
+ # {
634
+ # "domain"=>"zerobounce.net",
635
+ # "company_name"=>"Hertza, LLC",
636
+ # "format"=>"first.last",
637
+ # "confidence"=>"high",
638
+ # "did_you_mean"=>"",
639
+ # "failure_reason"=>"",
640
+ # "other_domain_formats"=>[
641
+ # {"format"=>"first", "confidence"=>"high"},
642
+ # {"format"=>"last.first", "confidence"=>"high"},
643
+ # {"format"=>"lastfirst", "confidence"=>"high"},
644
+ # {"format"=>"firstl", "confidence"=>"high"},
645
+ # {"format"=>"lfirst", "confidence"=>"high"},
646
+ # {"format"=>"firstlast", "confidence"=>"high"},
647
+ # {"format"=>"last_middle_f", "confidence"=>"high"},
648
+ # {"format"=>"last", "confidence"=>"high"},
649
+ # {"format"=>"f.last", "confidence"=>"medium"},
650
+ # {"format"=>"last-f", "confidence"=>"medium"},
651
+ # {"format"=>"l.first", "confidence"=>"medium"},
652
+ # {"format"=>"last_f", "confidence"=>"medium"},
653
+ # {"format"=>"first.middle.last", "confidence"=>"medium"},
654
+ # {"format"=>"first-last", "confidence"=>"medium"},
655
+ # {"format"=>"last.f", "confidence"=>"medium"},
656
+ # {"format"=>"last_first", "confidence"=>"medium"},
657
+ # {"format"=>"f-last", "confidence"=>"medium"},
658
+ # {"format"=>"first.l", "confidence"=>"medium"},
659
+ # {"format"=>"first-l", "confidence"=>"medium"},
660
+ # {"format"=>"first_l", "confidence"=>"medium"},
661
+ # {"format"=>"first_last", "confidence"=>"medium"},
662
+ # {"format"=>"f_last", "confidence"=>"medium"},
663
+ # {"format"=>"last-first", "confidence"=>"medium"},
664
+ # {"format"=>"flast", "confidence"=>"medium"},
665
+ # {"format"=>"lastf", "confidence"=>"medium"},
666
+ # {"format"=>"l_first", "confidence"=>"medium"},
667
+ # {"format"=>"l-first", "confidence"=>"medium"},
668
+ # {"format"=>"first-middle-last", "confidence"=>"low"},
669
+ # {"format"=>"firstmlast", "confidence"=>"low"},
670
+ # {"format"=>"last.middle.first", "confidence"=>"low"},
671
+ # {"format"=>"last_middle_first", "confidence"=>"low"},
672
+ # {"format"=>"first_middle_last", "confidence"=>"low"},
673
+ # {"format"=>"last-middle-first", "confidence"=>"low"},
674
+ # {"format"=>"first-m-last", "confidence"=>"low"},
675
+ # {"format"=>"firstmiddlelast", "confidence"=>"low"},
676
+ # {"format"=>"last.m.first", "confidence"=>"low"},
677
+ # {"format"=>"lastmfirst", "confidence"=>"low"},
678
+ # {"format"=>"lastmiddlefirst", "confidence"=>"low"},
679
+ # {"format"=>"last_m_first", "confidence"=>"low"},
680
+ # {"format"=>"first.m.last", "confidence"=>"low"},
681
+ # {"format"=>"first_m_last", "confidence"=>"low"},
682
+ # {"format"=>"last-m-first", "confidence"=>"low"}
683
+ # ]
684
+ # }
700
685
 
701
686
  # Find domain format by company name
702
687
  Zerobounce.find_domain(company_name: "Zero Bounce")
703
- =>
704
- {
705
- "domain": "zerobounce.net",
706
- "company_name": "Zero Bounce",
707
- "format": "first.last",
708
- "confidence": "high",
709
- "did_you_mean": "",
710
- "failure_reason": "",
711
- "other_domain_formats": [...]
712
- }
688
+ # =>
689
+ # {
690
+ # "domain"=>"zerobounce.net",
691
+ # "company_name"=>"Zero Bounce",
692
+ # "format"=>"first.last",
693
+ # "confidence"=>"high",
694
+ # "did_you_mean"=>"",
695
+ # "failure_reason"=>"",
696
+ # "other_domain_formats"=>[...]
697
+ # }
698
+ ```
699
+
700
+ ## Errors
701
+
702
+ - An HTTP error status raises the `RestClient` exception for that status, as it did up to 2.1.8: `RestClient::NotFound`, `RestClient::Forbidden` and so on, all subclasses of `RestClient::ExceptionWithResponse` and of `RuntimeError`. `#http_code` gives the status. The message is the API's own short error text (up to 2.1.8 it was RestClient's, such as `404 Not Found`); an HTML error page is reduced to its title.
703
+ - An error reported with an HTTP 200 - an `error` field, `{"success": false}`, or an account-level `validate_batch` failure - raises `RuntimeError` with the API's message.
704
+ - Invalid arguments, and a base URL that is not `https://`, raise `ArgumentError` before any request is sent.
705
+ - Redirects are not followed; a 3xx raises its `RestClient` exception (for example `RestClient::Found`).
706
+ - The Warmup client raises `Zerobounce::ApiResponseError`, whose `#status_code` gives the HTTP status.
707
+
708
+ The API key is removed from every error message.
709
+
710
+ ```ruby
711
+ begin
712
+ Zerobounce.validate_file_check("00000000-0000-0000-0000-000000000000")
713
+ rescue RestClient::ExceptionWithResponse => e
714
+ e.http_code # => 404, for example
715
+ e.message # => the API's error text
716
+ rescue RuntimeError => e
717
+ e.message # => e.g. "File cannot be found."
718
+ end
719
+ ```
720
+
721
+ ## Warmup API
722
+
723
+ The Warmup API is a separate product. It has its own host, its own team-scoped key, and its own client; your validation key will not work with it.
724
+
725
+ ```ruby
726
+ warmup = Zerobounce::Warmup.new('<YOUR_WARMUP_API_KEY>')
727
+
728
+ warmup.list_sending_accounts
729
+ warmup.progress_today
730
+ warmup.download_seeds # CSV text, columns seed,provider
731
+ ```
732
+
733
+ Add a from-address you run yourself, with one of the five standard engagement rules (0 Default, 1 Slow Pace, 2 Repair, 3 Boost, 4 Protect):
734
+
735
+ ```ruby
736
+ warmup.add_sending_account('sender@example.com', 'self_managed', engagement_rule_id: 0)
737
+ ```
738
+
739
+ Or have ZeroBounce provision the mailbox. A 200 means the request was accepted; provisioning happens afterwards.
740
+
741
+ ```ruby
742
+ warmup.add_sending_account(
743
+ 'sender@example.com', 'zb_managed',
744
+ email_content: { types_of_emails: %w[newsletter marketing] },
745
+ managed_sending: { timezone_offset: -5, warmup_start_date: Date.new(2026, 3, 4) }
746
+ )
747
+ ```
748
+
749
+ A custom engagement rule can be changed only once every seven days; inside that window the API answers 429.
750
+
751
+ ```ruby
752
+ warmup.create_custom_engagement('sender@example.com', 20, 20, 35) # reply, important, click %
713
753
  ```
714
754
 
755
+ `check_domain_dmarc` takes an **email address**, not a domain — the live API rejects a bare domain even though the docs describe one:
756
+
757
+ ```ruby
758
+ warmup.check_domain_dmarc('sender@example.com')
759
+ ```
760
+
761
+ Every documented empty success (204, or an empty 200) comes back as `nil`, and a payload that is not a JSON object — `/seeds` is an array, `/seed-count` a bare number — comes back as the parsed value itself. Failures raise `Zerobounce::ApiResponseError`, whose `#status_code` gives the HTTP status. Nothing is stored by `test_smtp_configuration`, and the credentials you pass it are masked out of any error it raises.
762
+
715
763
  ## Development
716
764
 
717
765
  ### Local setup
@@ -747,14 +795,7 @@ docker run --rm zb-ruby-sdk
747
795
  bundle exec rspec
748
796
  ```
749
797
 
750
- You should see an output like this
751
- ```bash
752
- running tests
753
- .....................................................
754
-
755
- Finished in 6.81 seconds (files took 0.40587 seconds to load)
756
- 53 examples, 0 failures
757
- ```
798
+ It should end with `0 failures`. The suite is offline: recorded cassettes and WebMock stubs, no network.
758
799
 
759
800
  ### Test parameters
760
801
  The tests use the following environment parameter:
@@ -767,9 +808,30 @@ To set your key:
767
808
  export ZEROBOUNCE_API_KEY=your_api_key_here
768
809
  ```
769
810
 
770
- A .env.example file is provided.
811
+ A `.env.example` file is provided; it holds a placeholder (`your_api_key_here`), not a key. The spec helper loads a `.env` file if there is one.
812
+
813
+ Tests use webmock and vcr for mocking HTTP requests. The cassettes in `spec/cassettes` were recorded against the live API; the key in them has been replaced with a placeholder, and requests are matched ignoring `api_key`, so any value of `ZEROBOUNCE_API_KEY` works (CI uses `invalid_key_for_tests`). To re-record a cassette, delete it and run the spec with a real key; check that the new cassette holds no key before committing it.
814
+
815
+ ### Live API suite
816
+
817
+ `rspec` never touches the network: the live suite lives outside `spec/`, so CI does not even load it. It needs real keys, which come from the environment and are never written anywhere:
818
+
819
+ ```bash
820
+ export ZEROBOUNCE_API_KEY=... # validation API
821
+ export ZEROBOUNCE_WARMUP_API_KEY=... # Warmup API
822
+ ruby -Ilib live/live_api.rb
823
+ ```
824
+
825
+ By default it only makes free calls: sandbox addresses, reads, and requests that are expected to fail but still prove the API accepts the payload. Nothing in the free tier submits a job, and it checks that the credit balance did not move. Two flags opt in to more:
826
+
827
+ | Flag | What it adds |
828
+ | --- | --- |
829
+ | `ZEROBOUNCE_LIVE_SPEND=1` | billable calls (scoring, activity, email finder), a bulk round trip that deletes its own job, and a complete S3 job payload (a real job if the account has an S3 integration) |
830
+ | `ZEROBOUNCE_LIVE_CHANGES=1` | reversible account changes — a test filter and a self-managed Warmup sending account — each undone in cleanup even when a check fails |
831
+
832
+ It never creates an Inbox Placement test, sets the Inbox Placement webhook, or starts ZB-managed Warmup onboarding, because the API cannot undo any of those.
771
833
 
772
- Tests use webmock and vcr for mocking HTTP requests. This means that actual requests were made and recorded in the spec/cassettes with an (at the time) valid API key used for testing purposes. This key has been invalidated in the meantime, however it is provided in the .env.example file for the mock tests to work. If you do not wish to use this key for mocks, you can replace it with any value in the .yml files under spec/cassettes or delete all of them and rerun the tests so that vcr records them with a new key.
834
+ Run it by hand before every release: the offline suite proves the SDK sends what we think it sends, and only the live suite proves the API accepts it.
773
835
 
774
836
  ## Publish
775
837