domainnameapi 1.0.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.
data/README.md ADDED
@@ -0,0 +1,995 @@
1
+ <div align="center">
2
+ <a href="README-TR.md" > TR <img style="padding-top: 8px" src="https://raw.githubusercontent.com/yammadev/flag-icons/master/png/TR.png" alt="TR" height="20" /></a>
3
+ <a href="README.md"> | EN <img style="padding-top: 8px" src="https://raw.githubusercontent.com/yammadev/flag-icons/master/png/US.png" alt="EN" height="20" /></a>
4
+ <a href="README-DE.md"> | DE <img style="padding-top: 8px" src="https://raw.githubusercontent.com/yammadev/flag-icons/master/png/DE.png" alt="DE" height="20" /></a>
5
+ <a href="README-SA.md"> | SA <img style="padding-top: 8px" src="https://raw.githubusercontent.com/yammadev/flag-icons/master/png/SA.png" alt="AR" height="20" /></a>
6
+ <a href="README-NL.md"> | NL <img style="padding-top: 8px" src="https://raw.githubusercontent.com/yammadev/flag-icons/master/png/NL.png" alt="NL" height="20" /></a>
7
+ <a href="README-AZ.md"> | AZ <img style="padding-top: 8px" src="https://raw.githubusercontent.com/yammadev/flag-icons/master/png/AZ.png" alt="AZ" height="20" /></a>
8
+ <a href="README-CN.md"> | CN <img style="padding-top: 8px" src="https://raw.githubusercontent.com/yammadev/flag-icons/master/png/CN.png" alt="CN" height="20" /></a>
9
+ <a href="README-FR.md"> | FR <img style="padding-top: 8px" src="https://raw.githubusercontent.com/yammadev/flag-icons/master/png/FR.png" alt="FR" height="20" /></a>
10
+ <a href="README-IT.md"> | IT <img style="padding-top: 8px" src="https://raw.githubusercontent.com/yammadev/flag-icons/master/png/IT.png" alt="IT" height="20" /></a>
11
+ </div>
12
+
13
+ # ruby-dna — DomainNameAPI client for Ruby
14
+
15
+ Register, renew and transfer domains, manage contacts, nameservers, child nameservers, DNS zones and URL forwarding through the DomainNameAPI REST API. `ruby-dna` is a faithful port of [php-dna](https://github.com/domainreseller/php-dna): the same methods, the same requests on the wire and the same return values, verified against php-dna's conformance fixtures.
16
+
17
+ ## 📦 Download — always use Releases!
18
+
19
+ ⬇️ **Get the latest tested version here: https://github.com/domainreseller/ruby-dna/releases/latest**
20
+
21
+ > ⚠️ Do **not** use the green **Code → Download ZIP** button — that downloads the raw development branch. Release packages are versioned, tested and production-ready.
22
+
23
+ ## Installation and Integration Guide
24
+
25
+ ### Minimum Requirements
26
+
27
+ - Ruby 2.7 or higher (Recommended 3.3+)
28
+ - No runtime dependencies: only the standard library (`net/http`, `json`, `openssl`).
29
+
30
+ ### A) Bundler
31
+
32
+ ```ruby
33
+ # Gemfile
34
+ gem 'domainnameapi', git: 'https://github.com/domainreseller/ruby-dna.git', tag: 'v1.0.0'
35
+ ```
36
+
37
+ ```bash
38
+ bundle install
39
+ ```
40
+
41
+ ### B) Gem file from the release
42
+
43
+ Download `domainnameapi-1.0.0.gem` from the [latest release](https://github.com/domainreseller/ruby-dna/releases/latest), then:
44
+
45
+ ```bash
46
+ gem install ./domainnameapi-1.0.0.gem
47
+ ```
48
+
49
+ ### C) Manual usage
50
+
51
+ Download the release archive, copy the `lib` folder into your project and look at the examples in the [examples](examples) folder:
52
+
53
+ ```ruby
54
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
55
+ require 'domainnameapi'
56
+ ```
57
+
58
+ ## 🔑 API Credentials — Reseller ID & API Key
59
+
60
+ The library talks to the DomainNameAPI REST API and needs exactly two credentials, passed to the constructor:
61
+
62
+ | Parameter | Value |
63
+ |---|---|
64
+ | First | Reseller ID — UUID like `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
65
+ | Second | API Key |
66
+
67
+ > 💡 Find your **Reseller ID** and **API Key** in your DomainNameAPI panel under **API Settings**.
68
+ > ⚠️ These are **API credentials** — your panel login details will **not** work here.
69
+
70
+ ## Quick start
71
+
72
+ ```ruby
73
+ require 'domainnameapi'
74
+
75
+ reseller_id = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
76
+ api_key = 'your-api-key'
77
+
78
+ # Live environment
79
+ dna = DomainNameApi::Client.new(reseller_id, api_key)
80
+
81
+ # Test (OTE) environment
82
+ dna = DomainNameApi::Client.new(reseller_id, api_key, true)
83
+
84
+ result = dna.get_details('example.com')
85
+ if result['result'] == 'OK'
86
+ puts result['data']['Dates']['Expiration']
87
+ else
88
+ puts "#{result['error']['Code']}: #{result['error']['Message']}"
89
+ end
90
+ ```
91
+
92
+ Method names are the snake_case form of php-dna's (`getDetails` → `get_details`, `registerWithContactInfo` → `register_with_contact_info`), with the same parameter order and defaults. Results are plain Ruby Hashes and Arrays with **String keys**, exactly the structure php-dna returns (including string flags such as `'LockStatus' => 'true'` and formatted prices such as `'11.9900'`). Credentials are trimmed, and the test-mode flag also accepts strings: `"true"`, `"1"`, `"on"` and `"yes"` mean test mode, anything else means live.
93
+
94
+ ## Error handling
95
+
96
+ Methods never raise for API or network problems. A failed call returns an error envelope:
97
+
98
+ ```ruby
99
+ {
100
+ 'result' => 'ERROR',
101
+ 'error' => {
102
+ 'Code' => 'API_404',
103
+ 'Message' => 'The domain name you requested could not be found',
104
+ 'Details' => '...'
105
+ }
106
+ }
107
+ ```
108
+
109
+ - `Code` is `API_<HTTP status>` for gateway errors (`API_400`, `API_401`, `API_429`, `API_500`, ...). A `301`/`302` answer means bad credentials (`Invalid API credentials`).
110
+ - When the gateway sends its own error code, the envelope also carries it as `GatewayCode` (e.g. `'GatewayCode' => 'Dna.DomainService:Domain:10007'`), so you can tell one `API_404` from another.
111
+ - An empty error body becomes a readable message such as `HTTP 429 - Too Many Requests`. The raw response body is never used as the message when the gateway's message is boilerplate; a stable text naming the gateway code is used instead.
112
+ - Field validation errors are flattened into `Details`, e.g. `Admin PostalCode - 'PostalCode' is required`.
113
+ - Network failures are errors, never an empty success. A timeout reads `Could not reach the DomainNameAPI service: the connection timed out. (...)`; other transport failures read `Curl error during request: ...`. Their codes are `CURL_<n>` with curl's numbers (`CURL_28` timeout, `CURL_6` DNS, `CURL_7` connection refused, `CURL_35`/`CURL_60` TLS, `CURL_56` reset); `API_<n>` is reserved for HTTP statuses.
114
+ - A registration, transfer or renewal that the gateway answers with HTTP 200 but `"success": false` is a refusal, not a success: it returns the error envelope with code `REGISTER_DOMAIN`, `TRANSFER_DOMAIN` or `RENEW_DOMAIN` and the gateway's message.
115
+ - `get_current_balance` keeps its own envelope: `'OperationResult' => 'FAILED'` with `ErrorCode` and `OperationMessage`.
116
+ - `check_availability` returns a plain list on success and the error envelope on failure.
117
+
118
+ Only programmer errors raise (for example `set_resource_record_set` with `contents` that is not a list raises `TypeError`).
119
+
120
+ Timeouts are 30 seconds per call and 10 seconds to connect. Calls to the DNS zone endpoints are kept at least one second apart, because the gateway answers `HTTP 429` to bursts.
121
+
122
+ ### Diagnostics
123
+
124
+ ```ruby
125
+ dna.get_request_data # { 'url' => ..., 'method' => 'GET', 'payload' => {...} } of the last call
126
+ dna.get_response_data # last decoded response body (or { 'raw_response' => body })
127
+ dna.get_response_headers # { 'url', 'http_code', 'headers', 'total_time' }
128
+ dna.get_parsed_response_data # last parsed body, or the last error { 'Code', 'Message', 'Details' }
129
+ dna.get_last_function # e.g. 'request:GET domains/info'
130
+ dna.get_service_url # https://api.domainresellerapi.com/api/v1
131
+ dna.set_service_url('https://ote.domainresellerapi.com/api/v1')
132
+ ```
133
+
134
+ ## Test mode
135
+
136
+ Pass `true` as the third constructor argument to use the OTE test gateway (`https://ote.domainresellerapi.com/api/v1`). Live calls go to `https://api.domainresellerapi.com/api/v1`. Test mode uses separate test credentials.
137
+
138
+ ## Error reporting (telemetry)
139
+
140
+ When an API call fails, the library can send an error report to DomainNameAPI's own error tracker (Sentry). A small random share of successful calls (4%, plus every call slower than 1 second) can be sent as a performance record. This helps DomainNameAPI find and fix problems. Error reporting is **on by default**.
141
+
142
+ > ℹ️ In version 1.0.0 the Sentry project for ruby-dna does not exist yet, so the built-in DSN is empty and **nothing is sent**. Reports start once a release ships the DSN. `DomainNameApi::Client.new(id, key, false, sentry_dsn: '...')` sets one explicitly.
143
+
144
+ **What is sent:**
145
+
146
+ - error code and message
147
+ - called endpoint path and HTTP method
148
+ - call duration
149
+ - library name (`DomainNameAPI-Ruby`), library version and Ruby version
150
+ - reseller ID and domain name
151
+ - the server's public IP address (looked up via `api.ipify.org`, in the background)
152
+ - the operating-system user name the process runs as
153
+ - the API request and response, including the domain's contact details; only credentials are masked — EPP/auth codes, API keys, passwords and tokens are replaced with `***`
154
+
155
+ **What is never sent:** EPP/auth codes, API keys, passwords or tokens in readable form; method arguments in stack traces.
156
+
157
+ Expected, high-volume errors (domain not found, insufficient balance, invalid credentials, ...) are not reported. Reports are posted from a background thread with short timeouts: they never slow down or break your call.
158
+
159
+ **Why and how long:** the data is used only for usage measurement and early diagnosis of problems, and is kept for 30 days on DomainNameAPI's own Sentry server, after which it is deleted automatically.
160
+
161
+ **How to turn it off:**
162
+
163
+ ```ruby
164
+ dna = DomainNameApi::Client.new(reseller_id, api_key)
165
+ dna.set_error_reporting_enabled(false)
166
+ ```
167
+
168
+ **Minimal mode** sends only the error code and message, endpoint, timing, versions and the reseller ID — no request/response data, domain, server IP or user name:
169
+
170
+ ```ruby
171
+ dna.set_telemetry_minimal(true)
172
+ ```
173
+
174
+ > 🔒 **Security:** TLS certificate verification is on. On a host with an outdated CA bundle it can be switched off with `dna.set_ssl_verification(false)` — this is not recommended; updating the CA bundle is the better fix.
175
+
176
+ ## Usage
177
+
178
+ All examples assume `dna = DomainNameApi::Client.new(reseller_id, api_key)`. Runnable scripts for every method are in [examples](examples).
179
+
180
+ ### Account
181
+
182
+ #### Reseller details
183
+
184
+ ```ruby
185
+ dna.get_reseller_details
186
+ ```
187
+
188
+ <details>
189
+ <summary>Sample output</summary>
190
+
191
+ ```ruby
192
+ {
193
+ 'result' => 'OK',
194
+ 'id' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
195
+ 'active' => true,
196
+ 'name' => 'example-reseller',
197
+ 'balance' => '601.64',
198
+ 'currency' => 'USD',
199
+ 'symbol' => '$',
200
+ 'balances' => [
201
+ { 'balance' => '601.64', 'currency' => 'USD', 'symbol' => '$' },
202
+ { 'balance' => '1250.50', 'currency' => 'TL', 'symbol' => 'TL' }
203
+ ]
204
+ }
205
+ ```
206
+
207
+ </details>
208
+
209
+ #### Current balance
210
+
211
+ `get_current_balance(currency = 'USD')` — the account reports `'USD'` and `'TRY'` balances. For any other currency (`'EUR'`, `'GBP'`, ...) the call fails with `'ErrorCode' => 'BALANCE'` instead of answering `0.00`.
212
+
213
+ ```ruby
214
+ dna.get_current_balance('USD')
215
+ ```
216
+
217
+ <details>
218
+ <summary>Sample output</summary>
219
+
220
+ ```ruby
221
+ {
222
+ 'ErrorCode' => 0,
223
+ 'OperationMessage' => 'Command completed succesfully.',
224
+ 'OperationResult' => 'SUCCESS',
225
+ 'Balance' => '601.64',
226
+ 'CurrencyId' => 2,
227
+ 'CurrencyInfo' => nil,
228
+ 'CurrencyName' => 'USD',
229
+ 'CurrencySymbol' => '$'
230
+ }
231
+ # get_current_balance('EUR')
232
+ {
233
+ 'ErrorCode' => 'BALANCE',
234
+ 'OperationMessage' => 'No EUR balance is reported for this account. Available: USD, TRY.',
235
+ 'OperationResult' => 'FAILED',
236
+ 'Balance' => '0.00',
237
+ 'CurrencyId' => 3,
238
+ 'CurrencyInfo' => nil,
239
+ 'CurrencyName' => 'EUR',
240
+ 'CurrencySymbol' => '€'
241
+ }
242
+ ```
243
+
244
+ </details>
245
+
246
+ <hr style="border: 4px solid #000; border-style: dashed;">
247
+
248
+ ### Search and lists
249
+
250
+ #### Domain availability check
251
+
252
+ `check_availability(domains, extensions, period, command = 'create')` — `DomainName` in the result is the name without its trailing `.tld`.
253
+
254
+ ```ruby
255
+ dna.check_availability(['hello', 'world123x0'], ['com', 'net'], 1, 'create')
256
+ ```
257
+
258
+ <details>
259
+ <summary>Sample output</summary>
260
+
261
+ ```ruby
262
+ [
263
+ { 'TLD' => 'com', 'DomainName' => 'hello', 'Status' => 'notavailable', 'Command' => 'create',
264
+ 'Period' => 1, 'IsFee' => false, 'Price' => '10.8100', 'Currency' => 'USD', 'Reason' => 'Domain exists' },
265
+ { 'TLD' => 'com', 'DomainName' => 'world123x0', 'Status' => 'available', 'Command' => 'create',
266
+ 'Period' => 1, 'IsFee' => false, 'Price' => '10.8100', 'Currency' => 'USD', 'Reason' => nil }
267
+ ]
268
+ ```
269
+
270
+ </details>
271
+
272
+ #### Domain list
273
+
274
+ `get_list(extra_parameters = {})` — `PageNumber` (0-based), `PageSize`, `OrderColumn`, `OrderDirection` and the gateway's own filters (e.g. `DomainName`).
275
+
276
+ ```ruby
277
+ dna.get_list('OrderColumn' => 'Id', 'OrderDirection' => 'ASC', 'PageNumber' => 0, 'PageSize' => 1000)
278
+ ```
279
+
280
+ <details>
281
+ <summary>Sample output</summary>
282
+
283
+ ```ruby
284
+ {
285
+ 'data' => {
286
+ 'Domains' => [
287
+ {
288
+ 'ID' => 564346, 'Status' => 'Active', 'DomainName' => 'example.com', 'AuthCode' => '',
289
+ 'LockStatus' => 'true', 'PrivacyProtectionStatus' => 'false', 'IsChildNameServer' => 'false',
290
+ 'Contacts' => { 'Billing' => { 'ID' => '' }, 'Technical' => { 'ID' => '' },
291
+ 'Administrative' => { 'ID' => '' }, 'Registrant' => { 'ID' => '' } },
292
+ 'Dates' => { 'Start' => '2024-03-15T10:00:00', 'Expiration' => '2027-03-15T10:00:00', 'RemainingDays' => 126 },
293
+ 'NameServers' => ['ns1.example.com', 'ns2.example.com'],
294
+ 'Additional' => {},
295
+ 'ChildNameServers' => []
296
+ }
297
+ ]
298
+ },
299
+ 'result' => 'OK',
300
+ 'TotalCount' => 1
301
+ }
302
+ ```
303
+
304
+ </details>
305
+
306
+ #### TLD list
307
+
308
+ `get_tld_list(count = 20)`
309
+
310
+ ```ruby
311
+ dna.get_tld_list(100)
312
+ ```
313
+
314
+ <details>
315
+ <summary>Sample output</summary>
316
+
317
+ ```ruby
318
+ {
319
+ 'data' => [
320
+ {
321
+ 'id' => 1, 'status' => 'Active', 'maxchar' => 63, 'maxperiod' => 10, 'minchar' => 1, 'minperiod' => 1,
322
+ 'tld' => 'com',
323
+ 'pricing' => {
324
+ 'registration' => { '1' => '10.8100' },
325
+ 'renew' => { '1' => '11.0100' },
326
+ 'transfer' => { '1' => '10.6100' },
327
+ 'restore' => { '1' => '99.9000' }
328
+ },
329
+ 'currencies' => { 'registration' => 'USD', 'renew' => 'USD', 'transfer' => 'USD', 'restore' => 'USD' }
330
+ }
331
+ ],
332
+ 'result' => 'OK'
333
+ }
334
+ ```
335
+
336
+ </details>
337
+
338
+ <hr style="border: 4px solid #000; border-style: dashed;">
339
+
340
+ ### Domain
341
+
342
+ #### Domain registration
343
+
344
+ `register_with_contact_info(domain, period, contacts, name_servers = [], epp_lock = true, privacy_lock = false, additional_attributes = {})`
345
+
346
+ Contacts use php-dna's keys. Both the flat shape below and the nested one returned by `get_contacts` (`Address => { Line1, City, ... }`, `Phone => { Phone => { Number, CountryCode } }`) are accepted; missing fields are completed and names, phones and countries are normalized (see `validate_contact`). With no nameservers, `ns1.domainnameapi.com` and `ns2.domainnameapi.com` are used.
347
+
348
+ ```ruby
349
+ contact = {
350
+ 'FirstName' => 'John',
351
+ 'LastName' => 'Doe',
352
+ 'Company' => 'Example Corp',
353
+ 'EMail' => 'john.doe@example.com',
354
+ 'AddressLine1' => '123 Lorem Street',
355
+ 'AddressLine2' => 'Suite 456',
356
+ 'AddressLine3' => '',
357
+ 'City' => 'Springfield',
358
+ 'Country' => 'US',
359
+ 'Fax' => '1234567890',
360
+ 'FaxCountryCode' => '1',
361
+ 'Phone' => '9876543210',
362
+ 'PhoneCountryCode' => 1,
363
+ 'Type' => 'Contact',
364
+ 'ZipCode' => '12345',
365
+ 'State' => 'IL'
366
+ }
367
+
368
+ dna.register_with_contact_info(
369
+ 'example.com',
370
+ 1,
371
+ { 'Administrative' => contact, 'Billing' => contact, 'Technical' => contact, 'Registrant' => contact },
372
+ ['ns1.example.com', 'ns2.example.com'],
373
+ true,
374
+ false
375
+ )
376
+ ```
377
+
378
+ For `.tr` domains pass the TRABIS attributes as the 7th argument:
379
+
380
+ ```ruby
381
+ # Individual owner
382
+ { 'TRABISDOMAINCATEGORY' => 1, 'TRABISCITIZIENID' => '12345678901' }
383
+ # Company owner
384
+ { 'TRABISDOMAINCATEGORY' => 0, 'TRABISTAXOFFICE' => 'Istanbul VD', 'TRABISTAXNUMBER' => '1234567890' }
385
+ ```
386
+
387
+ The category is sent as `Owner`/`Corporate`, `TRABISCITIZIENID` as `TRABISCITIZENID`, and legacy keys (`TRABISNAMESURNAME`, `TRABISCOUNTRYID`, `TRABISCITYID`, ...) are dropped, because the gateway now reads them from the contact.
388
+
389
+ <details>
390
+ <summary>Sample output</summary>
391
+
392
+ ```ruby
393
+ {
394
+ 'data' => {
395
+ 'ID' => 123456, 'Status' => 'Active', 'DomainName' => 'example.com', 'AuthCode' => 'Xy9#mK2$pL5@vN8',
396
+ 'LockStatus' => 'true', 'PrivacyProtectionStatus' => 'false', 'IsChildNameServer' => 'false',
397
+ 'Contacts' => { 'Billing' => { 'ID' => 987654 }, 'Technical' => { 'ID' => 987654 },
398
+ 'Administrative' => { 'ID' => 987654 }, 'Registrant' => { 'ID' => 987654 } },
399
+ 'Dates' => { 'Start' => '2024-03-15T10:00:00', 'Expiration' => '2025-03-15T10:00:00', 'RemainingDays' => 365 },
400
+ 'NameServers' => ['ns1.example.com', 'ns2.example.com'],
401
+ 'Additional' => {},
402
+ 'ChildNameServers' => []
403
+ },
404
+ 'result' => 'OK'
405
+ }
406
+ ```
407
+
408
+ </details>
409
+
410
+ #### Domain renewal
411
+
412
+ ```ruby
413
+ dna.renew('example.com', 1)
414
+ ```
415
+
416
+ <details>
417
+ <summary>Sample output</summary>
418
+
419
+ ```ruby
420
+ { 'result' => 'OK', 'data' => { 'ExpirationDate' => '2025-03-15T10:00:00' } }
421
+ ```
422
+
423
+ </details>
424
+
425
+ #### Domain details
426
+
427
+ ```ruby
428
+ dna.get_details('example.com')
429
+ ```
430
+
431
+ <details>
432
+ <summary>Sample output</summary>
433
+
434
+ ```ruby
435
+ {
436
+ 'data' => {
437
+ 'ID' => 564346, 'Status' => 'Active', 'DomainName' => 'example.com', 'AuthCode' => 'DHQ!K52',
438
+ 'LockStatus' => 'true', 'PrivacyProtectionStatus' => 'false', 'IsChildNameServer' => 'true',
439
+ 'Contacts' => { 'Billing' => { 'ID' => 'DNA-B-4' }, 'Technical' => { 'ID' => 'DNA-T-3' },
440
+ 'Administrative' => { 'ID' => 'DNA-A-2' }, 'Registrant' => { 'ID' => 'DNA-R-1' } },
441
+ 'Dates' => { 'Start' => '2025-05-26T16:08:37', 'Expiration' => '2027-05-26T16:08:37', 'RemainingDays' => 449 },
442
+ 'NameServers' => ['ns1.example.com', 'ns2.example.com'],
443
+ 'Additional' => {},
444
+ 'ChildNameServers' => [{ 'ns' => 'ns1.example.com', 'ip' => '1.2.3.4' }]
445
+ },
446
+ 'result' => 'OK'
447
+ }
448
+ ```
449
+
450
+ </details>
451
+
452
+ #### Sync from registry
453
+
454
+ Same call and output as `get_details`.
455
+
456
+ ```ruby
457
+ dna.sync_from_registry('example.com')
458
+ ```
459
+
460
+ #### Nameserver modification
461
+
462
+ Pass a list or a Hash (the values are used). Names are trimmed, lower-cased, stripped of a trailing dot and de-duplicated; the result reports that normalised list.
463
+
464
+ ```ruby
465
+ dna.modify_name_server('example.com', ['ns1.example.com', 'ns2.example.com'])
466
+ ```
467
+
468
+ <details>
469
+ <summary>Sample output</summary>
470
+
471
+ ```ruby
472
+ { 'result' => 'OK', 'data' => { 'NameServers' => ['ns1.example.com', 'ns2.example.com'] } }
473
+ ```
474
+
475
+ </details>
476
+
477
+ #### Enable domain lock
478
+
479
+ ```ruby
480
+ dna.enable_theft_protection_lock('example.com')
481
+ ```
482
+
483
+ <details>
484
+ <summary>Sample output</summary>
485
+
486
+ ```ruby
487
+ { 'result' => 'OK', 'data' => { 'LockStatus' => true } }
488
+ ```
489
+
490
+ </details>
491
+
492
+ #### Disable domain lock
493
+
494
+ ```ruby
495
+ dna.disable_theft_protection_lock('example.com')
496
+ ```
497
+
498
+ <details>
499
+ <summary>Sample output</summary>
500
+
501
+ ```ruby
502
+ { 'result' => 'OK', 'data' => { 'LockStatus' => false } }
503
+ ```
504
+
505
+ </details>
506
+
507
+ #### Modify domain privacy
508
+
509
+ `modify_privacy_protection_status(domain, status, reason = 'Owner request')` — `status` may be `true`/`false` or a panel-style value: `1`, `'1'`, `'true'`, `'on'` and `'yes'` enable privacy, anything else disables it. The result reports the parsed boolean. `reason` is accepted for compatibility but not sent: the gateway has no such field.
510
+
511
+ ```ruby
512
+ dna.modify_privacy_protection_status('example.com', true, 'Owner request')
513
+ ```
514
+
515
+ <details>
516
+ <summary>Sample output</summary>
517
+
518
+ ```ruby
519
+ { 'data' => { 'PrivacyProtectionStatus' => true }, 'result' => 'OK' }
520
+ ```
521
+
522
+ </details>
523
+
524
+ #### Get domain contacts
525
+
526
+ Contact types are matched case-insensitively (`admin`/`administrative`, `tech`/`technical`, `registrant`/`owner`, `billing`); other types the registry returns are kept after the four standard ones.
527
+
528
+ ```ruby
529
+ dna.get_contacts('example.com')
530
+ ```
531
+
532
+ <details>
533
+ <summary>Sample output</summary>
534
+
535
+ ```ruby
536
+ {
537
+ 'data' => {
538
+ 'contacts' => {
539
+ 'Administrative' => {
540
+ 'ID' => 'DNA-A-2', 'Status' => 'Active', 'AuthCode' => '',
541
+ 'FirstName' => 'Grace', 'LastName' => 'Hopper', 'Company' => '', 'EMail' => 'grace@example.com',
542
+ 'Type' => 'Administrative',
543
+ 'Address' => { 'Line1' => 'Example Street 2', 'Line2' => '', 'Line3' => '', 'State' => 'DC',
544
+ 'City' => 'Washington', 'Country' => 'US', 'ZipCode' => '20001' },
545
+ 'Phone' => { 'Phone' => { 'Number' => '2025550100', 'CountryCode' => '1' },
546
+ 'Fax' => { 'Number' => '2025550101', 'CountryCode' => '1' } },
547
+ 'Additional' => {}
548
+ },
549
+ 'Billing' => { ... }, 'Registrant' => { ... }, 'Technical' => { ... }
550
+ # any other contact type the registry returns follows these four
551
+ }
552
+ },
553
+ 'result' => 'OK'
554
+ }
555
+ ```
556
+
557
+ </details>
558
+
559
+ #### Save domain contacts
560
+
561
+ ```ruby
562
+ dna.save_contacts('example.com', {
563
+ 'Administrative' => contact,
564
+ 'Billing' => contact,
565
+ 'Technical' => contact,
566
+ 'Registrant' => contact
567
+ })
568
+ ```
569
+
570
+ <details>
571
+ <summary>Sample output</summary>
572
+
573
+ ```ruby
574
+ { 'result' => 'OK', 'data' => { 'contacts' => {} } }
575
+ ```
576
+
577
+ </details>
578
+
579
+ <hr style="border: 4px solid #000; border-style: dashed;">
580
+
581
+ ### Child nameservers
582
+
583
+ #### Add child nameserver
584
+
585
+ ```ruby
586
+ dna.add_child_name_server('example.com', 'ns1.example.com', '1.2.3.4')
587
+ ```
588
+
589
+ <details>
590
+ <summary>Sample output</summary>
591
+
592
+ ```ruby
593
+ { 'result' => 'OK', 'data' => { 'NameServer' => 'ns1.example.com', 'IPAdresses' => ['1.2.3.4'] } }
594
+ ```
595
+
596
+ </details>
597
+
598
+ #### Delete child nameserver
599
+
600
+ ```ruby
601
+ dna.delete_child_name_server('example.com', 'ns1.example.com')
602
+ ```
603
+
604
+ <details>
605
+ <summary>Sample output</summary>
606
+
607
+ ```ruby
608
+ { 'result' => 'OK', 'data' => { 'NameServer' => 'ns1.example.com' } }
609
+ ```
610
+
611
+ </details>
612
+
613
+ #### Update child nameserver
614
+
615
+ ```ruby
616
+ dna.modify_child_name_server('example.com', 'ns1.example.com', '1.2.3.5')
617
+ ```
618
+
619
+ <details>
620
+ <summary>Sample output</summary>
621
+
622
+ ```ruby
623
+ { 'result' => 'OK', 'data' => { 'NameServer' => 'ns1.example.com', 'IPAdresses' => ['1.2.3.5'] } }
624
+ ```
625
+
626
+ </details>
627
+
628
+ <hr style="border: 4px solid #000; border-style: dashed;">
629
+
630
+ ### Transfers
631
+
632
+ #### Domain transfer
633
+
634
+ `transfer(domain, epp_code, period)` — a transfer never sends contacts: the registrar handles them, so the request always carries an empty contact list and the method takes no contacts argument. Update contacts with `save_contacts` once the transfer has completed.
635
+
636
+ ```ruby
637
+ dna.transfer('example.com', 'Xy9#mK2$', 3)
638
+ ```
639
+
640
+ <details>
641
+ <summary>Sample output</summary>
642
+
643
+ ```ruby
644
+ {
645
+ 'data' => { 'ID' => 0, 'Status' => 'WaitingForIncomingTransfer', 'DomainName' => 'example.com', ... },
646
+ 'result' => 'OK'
647
+ }
648
+ ```
649
+
650
+ </details>
651
+
652
+ #### Check transfer
653
+
654
+ `result` is `'OK'` only when the registry reports the domain as transferable.
655
+
656
+ ```ruby
657
+ dna.check_transfer('example.com', 'Xy9#mK2$')
658
+ ```
659
+
660
+ <details>
661
+ <summary>Sample output</summary>
662
+
663
+ ```ruby
664
+ {
665
+ 'result' => 'OK',
666
+ 'data' => {
667
+ 'TransferAvailabilityStatus' => true, 'AuthCodeIsRequired' => true, 'AuthCodeIsValid' => true,
668
+ 'UserTransferRequired' => false, 'TransferLock' => false, 'Message' => '', 'MessageKey' => '',
669
+ 'Additional' => {}
670
+ }
671
+ }
672
+ ```
673
+
674
+ </details>
675
+
676
+ #### Cancel, approve or reject a transfer
677
+
678
+ `cancel_transfer` cancels a pending incoming transfer; `approve_transfer` and `reject_transfer` answer a pending outgoing one.
679
+
680
+ ```ruby
681
+ dna.cancel_transfer('example.com')
682
+ dna.approve_transfer('example.com')
683
+ dna.reject_transfer('example.com')
684
+ ```
685
+
686
+ <details>
687
+ <summary>Sample output</summary>
688
+
689
+ ```ruby
690
+ { 'result' => 'OK', 'data' => { 'DomainName' => 'example.com', 'Status' => 'Cancelled' } } # 'Approved' / 'Rejected'
691
+ ```
692
+
693
+ </details>
694
+
695
+ <hr style="border: 4px solid #000; border-style: dashed;">
696
+
697
+ ### DNS zone
698
+
699
+ Supported record types: `A`, `AAAA`, `ALIAS`, `CAA`, `CNAME`, `DS`, `MX`, `NAPTR`, `PTR`, `SRV`, `TLSA`, `TXT` (other types are rejected before any call). Names are relative (`'www'`); `''` or `'@'` is the zone apex, and a full name such as `'www.example.com'` is reduced to its label. Changes are applied immediately; pass `apply = false` to stage several and call `apply_resource_records` once. TTL is 1–86400 seconds.
700
+
701
+ If the automatic apply fails, the method returns the apply error (the change stays staged, not live) instead of `OK`. The result reports the upper-cased type and the TTL that were actually sent.
702
+
703
+ The gateway stores a name + type as one **set**: writing a set replaces all of its values, and deleting one value deletes the whole set. The `*_resource_record_value` methods do the read-merge-write for you.
704
+
705
+ #### List records
706
+
707
+ ```ruby
708
+ dna.get_resource_records('example.com')
709
+ ```
710
+
711
+ <details>
712
+ <summary>Sample output</summary>
713
+
714
+ ```ruby
715
+ {
716
+ 'result' => 'OK',
717
+ 'data' => {
718
+ 'records' => [
719
+ { 'Name' => 'example.com.', 'Type' => 'MX', 'TTL' => 3600, 'Content' => '10 mail.example.com.', 'Disabled' => false },
720
+ { 'Name' => 'www.example.com.', 'Type' => 'A', 'TTL' => 3600, 'Content' => '192.0.2.10', 'Disabled' => false }
721
+ ]
722
+ }
723
+ }
724
+ ```
725
+
726
+ </details>
727
+
728
+ #### Replace a record set
729
+
730
+ `set_resource_record_set(domain, name, type, contents, ttl = 3600, apply = true)` — an empty list removes the set.
731
+
732
+ ```ruby
733
+ dna.set_resource_record_set('example.com', 'www', 'A', ['192.0.2.10', '192.0.2.11'], 3600)
734
+ ```
735
+
736
+ <details>
737
+ <summary>Sample output</summary>
738
+
739
+ ```ruby
740
+ { 'result' => 'OK', 'data' => { 'Name' => 'www', 'Type' => 'A', 'TTL' => 3600, 'Contents' => ['192.0.2.10', '192.0.2.11'] } }
741
+ ```
742
+
743
+ </details>
744
+
745
+ #### Add, replace or remove one value
746
+
747
+ `add_resource_record_value(domain, name, type, value, ttl = nil, apply = true)`, `replace_resource_record_value(domain, name, type, old_value, new_value, ttl = nil, apply = true)`, `remove_resource_record_value(domain, name, type, value, apply = true)`. A `nil` TTL keeps the set's current TTL; adding a value that exists or removing one that does not is a no-op (`'Changed' => false`).
748
+
749
+ ```ruby
750
+ dna.add_resource_record_value('example.com', '', 'MX', '10 mail.example.com.')
751
+ dna.replace_resource_record_value('example.com', '', 'MX', '10 mail.example.com.', '20 mail.example.com.')
752
+ dna.remove_resource_record_value('example.com', '', 'MX', '20 mail.example.com.')
753
+ ```
754
+
755
+ <details>
756
+ <summary>Sample output</summary>
757
+
758
+ ```ruby
759
+ { 'result' => 'OK', 'data' => { 'Name' => '', 'Type' => 'MX', 'TTL' => 3600, 'Contents' => ['10 mail.example.com.'] } }
760
+ # value already present:
761
+ { 'result' => 'OK', 'data' => { 'Name' => '', 'Type' => 'MX', 'Contents' => ['10 mail.example.com.'], 'Changed' => false } }
762
+ ```
763
+
764
+ </details>
765
+
766
+ #### Delete a record set
767
+
768
+ `delete_resource_record_set(domain, name, type, apply = true, known_value = '')` — removes every value of the set; nothing to remove is a success with `'Removed' => false`.
769
+
770
+ ```ruby
771
+ dna.delete_resource_record_set('example.com', 'www', 'A')
772
+ ```
773
+
774
+ <details>
775
+ <summary>Sample output</summary>
776
+
777
+ ```ruby
778
+ { 'result' => 'OK', 'data' => { 'Name' => 'www.example.com.', 'Type' => 'A', 'Content' => '192.0.2.10' } }
779
+ ```
780
+
781
+ </details>
782
+
783
+ #### Low-level record calls
784
+
785
+ ```ruby
786
+ dna.add_resource_record('example.com', 'www', 'A', '192.0.2.10', 3600, true)
787
+ dna.edit_resource_record('example.com', 'www', 'www', 'A', '192.0.2.20', 3600, true)
788
+ dna.delete_resource_record('example.com', 'www.example.com.', '192.0.2.20', 'A', true)
789
+ dna.apply_resource_records('example.com')
790
+ ```
791
+
792
+ <details>
793
+ <summary>Sample output</summary>
794
+
795
+ ```ruby
796
+ { 'result' => 'OK', 'data' => { 'Name' => 'www', 'Type' => 'A', 'TTL' => 3600, 'Content' => '192.0.2.10' } } # add / edit
797
+ { 'result' => 'OK', 'data' => { 'Name' => 'www.example.com.', 'Type' => 'A', 'Content' => '192.0.2.20' } } # delete
798
+ { 'result' => 'OK', 'data' => { 'DomainName' => 'example.com' } } # apply
799
+ ```
800
+
801
+ </details>
802
+
803
+ <hr style="border: 4px solid #000; border-style: dashed;">
804
+
805
+ ### URL forwarding
806
+
807
+ Forward types: `'Standard'` (plain redirect) and `'Frame'` (masked). The gateway answers `Redirect information not found` when nothing is configured: `get_forwarding` then reports `'Enabled' => false` and `delete_forwarding` succeeds with `'Removed' => false`. Any other error, such as an unknown domain, is returned as an error.
808
+
809
+ ```ruby
810
+ dna.set_forwarding('example.com', 'https://www.example.org', 'Standard')
811
+ dna.get_forwarding('example.com')
812
+ dna.delete_forwarding('example.com')
813
+ ```
814
+
815
+ <details>
816
+ <summary>Sample output</summary>
817
+
818
+ ```ruby
819
+ # set_forwarding
820
+ { 'result' => 'OK', 'data' => { 'RedirectAddress' => 'https://www.example.org', 'ForwardType' => 'Standard' } }
821
+ # get_forwarding (nothing configured gives Enabled => false, not an error)
822
+ { 'result' => 'OK', 'data' => { 'Enabled' => true, 'RedirectAddress' => 'https://www.example.org', 'ForwardType' => 'Standard' } }
823
+ # delete_forwarding (Removed => false when nothing was configured)
824
+ { 'result' => 'OK', 'data' => { 'DomainName' => 'example.com', 'Removed' => true } }
825
+ ```
826
+
827
+ </details>
828
+
829
+ <hr style="border: 4px solid #000; border-style: dashed;">
830
+
831
+ ### Utilities (no network)
832
+
833
+ #### Is it a .tr domain?
834
+
835
+ ```ruby
836
+ dna.is_tr_tld('example.com.tr') # => true
837
+ dna.is_tr_tld('example.com') # => false
838
+ ```
839
+
840
+ #### Validate a contact
841
+
842
+ Completes and normalizes a contact the way registration does: digits and punctuation are removed from names, a missing state becomes the city, country names become ISO codes and phone numbers are split into number and country code. A number written with its own `+` or `00` calling code keeps that code even when another `PhoneCountryCode` was given (`'+1 202 555 0100'` → `'2025550100'` / `'1'`).
843
+
844
+ ```ruby
845
+ dna.validate_contact('FirstName' => 'J0hn', 'Phone' => '+90 532 374 33 88', 'Country' => 'Turkey')
846
+ ```
847
+
848
+ <details>
849
+ <summary>Sample output</summary>
850
+
851
+ ```ruby
852
+ {
853
+ 'FirstName' => 'Jhn', 'Phone' => '5323743388', 'Country' => 'TR', 'LastName' => 'Isimyok',
854
+ 'AddressLine1' => 'Addres yok', 'City' => 'ISTANBUL', 'State' => 'ISTANBUL', 'ZipCode' => '34000',
855
+ 'PhoneCountryCode' => '90', 'Fax' => '', 'FaxCountryCode' => ''
856
+ }
857
+ ```
858
+
859
+ </details>
860
+
861
+ <hr style="border: 4px solid #000; border-style: dashed;">
862
+
863
+ ### Settings
864
+
865
+ | Method | Effect |
866
+ |---|---|
867
+ | `set_error_reporting_enabled(false)` | Turn telemetry off |
868
+ | `set_telemetry_minimal(true)` | Send the minimal telemetry payload |
869
+ | `set_ssl_verification(false)` | Skip TLS certificate verification (not recommended) |
870
+ | `set_service_url(url)` | Use another gateway URL |
871
+ | `set_transport(callable)` | Replace the HTTP transport (see below) |
872
+
873
+ The HTTP transport is any object that responds to `call(request)`, where `request` is a Hash with `:method`, `:url`, `:headers`, `:body`, `:timeout`, `:connect_timeout` and `:verify_ssl`, and returns a `DomainNameApi::Response` (or a Hash with `:status`, `:body`, `:headers`). The default is `DomainNameApi::NetHttpTransport` (Net::HTTP). This is how the tests run without a network:
874
+
875
+ ```ruby
876
+ fake = ->(request) { DomainNameApi::Response.new(status: 200, body: '{"resellerId":"abc"}', headers: {}) }
877
+ dna = DomainNameApi::Client.new(reseller_id, api_key, false, transport: fake)
878
+ ```
879
+
880
+ <hr style="border: 4px solid #000; border-style: dashed;">
881
+
882
+ ## Testing
883
+
884
+ The test suite uses only Minitest and never touches the network:
885
+
886
+ ```bash
887
+ bundle install
888
+ bundle exec rake test
889
+ ```
890
+
891
+ `test/conformance_test.rb` runs php-dna's 126 conformance fixtures (`test/fixtures/conformance/`): for each scenario it checks the requests sent (method, path, query, JSON body, auth headers) and the returned value against what php-dna produced. The other tests cover the HTTP layer, error mapping, telemetry masking, the Net::HTTP transport and the utilities.
892
+
893
+ <hr style="border: 4px solid #000; border-style: dashed;">
894
+
895
+ ## Return and Error Codes with Descriptions
896
+
897
+ | Code | Description | Detail |
898
+ |------|------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
899
+ | 1000 | Command completed successfully | Operation successful. |
900
+ | 1001 | Command completed successfully; action pending | Operation successful. However, the operation is currently queued for completion. |
901
+ | 2003 | Required parameter missing | Parameter missing error. For example, no phone entry in contact information. |
902
+ | 2105 | Object is not eligible for renewal | Domain status is not suitable for renewal, locked for update operations. Status should not be "clientupdateprohibited". May be due to other status conditions. |
903
+ | 2200 | Authentication error | Authorization error, security code is incorrect or domain is registered with another registrar. |
904
+ | 2302 | Object exists | Domain name or nameserver information already exists in database. Cannot be registered. |
905
+ | 2303 | Object does not exist | Domain name or nameserver information does not exist in database. New registration required. |
906
+ | 2304 | Object status prohibits operation | Domain status is not suitable for updates, locked for update operations. Status should not be "clientupdateprohibited". May be due to other status conditions. |
907
+
908
+ | CODE | DETAIL |
909
+ |------|--------------------------------------------------------------------------------------------------------------------------------------|
910
+ | 101 | System Error Detailed For ({0})! |
911
+ | 102 | Multiple Errors Detailed For ({0})! |
912
+ | 103 | Unknown Errors Detailed For ({0})! |
913
+ | 200 | API command not found ({0})! |
914
+ | 210 | API service not found ({0})! |
915
+ | 211 | API service provider not set! |
916
+ | 300 | Reseller not found! |
917
+ | 301 | Your current IP address is not authorized to access. Please make sure you are connecting from an authorized IP address and try again |
918
+ | 310 | TLD is not supported! |
919
+ | 320 | API not found! |
920
+ | 321 | Currency is not supported! |
921
+ | 330 | Required parameter(s) not set ({0}). |
922
+ | – | Make sure you send all contact objects full |
923
+ | 340 | Price definition not found ({0}[{1}]-{2}{3}). |
924
+ | 350 | Insufficient reseller balance. (Reseller Id : {0} - Current Balance : {1} {2}). |
925
+ | 350 | Accounting currency does not match or the balance is not sufficient. |
926
+ | 360 | Invalid API request for field ({0}). |
927
+ | 360 | API quota exceeded! |
928
+ | 361 | Throttled error! |
929
+ | 362 | Premium domain is not available to register right now. |
930
+ | 363 | Operation cancelled because domain in auto-renewal period |
931
+ | 364 | This domain is currently unavailable for transaction due to a problem with the registry |
932
+
933
+ | CODE | DETAIL |
934
+ |------|---------------------------------------------------------------------------------------------|
935
+ | 400 | Invalid contact ({0}). |
936
+ | 401 | Contact info can not sync. |
937
+ | 402 | No access to contact information from registry. |
938
+ | 403 | THE SYSTEM HAS MISSING INFORMATION. ENTER YOUR DEFAULT INFORMATION OR CONTACT SUPPORT TEAM. |
939
+ | 404 | DOMAIN CONTACT CANNOT UPDATE. PERMISSION FROM THE REGISTRY. |
940
+ | 410 | Contact not found. |
941
+ | 410 | Contact not found ({0}). |
942
+ | 420 | Invalid Api command for contact {0}. |
943
+ | 430 | Contact api not found. |
944
+ | 440 | Contact is not synced. |
945
+ | 450 | Too many domain contacts! |
946
+ | 451 | Failed to proceed with contact update. |
947
+
948
+ | CODE | DETAIL |
949
+ |------|--------------------------------------------------------------------------------------------------------------------------------------|
950
+ | 500 | Invalid domain id. |
951
+ | 500 | Invalid domain id ({0}). |
952
+ | 501 | Domain could not synchronized({0}) |
953
+ | 502 | Internal transfer failed |
954
+ | 503 | Domain registration is not available. |
955
+ | 504 | Domain information does not match. Before :{0} |
956
+ | 505 | Enter the Ip Address. |
957
+ | 506 | Domain Transfer Has Been Started But Contact Info Not Read. |
958
+ | 507 | Authorization error. |
959
+ | 510 | Domain not found. |
960
+ | 511 | Expired domains cannot be found. |
961
+ | 512 | Domain is not renewable. |
962
+ | 513 | Domain is not in updateable status. It must be active for to be updated |
963
+ | 514 | Redemption Period Expected. |
964
+ | 520 | Invalid Api command for domain "{0}". |
965
+ | 530 | Invalid domain period. Period must be {0} to {1} years. Requested period is {2} years. |
966
+ | 540 | Domain cannot be extended beyond {0} years from current date. |
967
+ | 550 | Invalid domain name. Domain must be {0} to {1} characters length. Requested domain name is {2} characters. |
968
+ | 560 | Invalid name server count. Domain must have {0} to {1} name servers. Requested name server count is {2}. |
969
+ | 561 | No name server information found in the incoming request |
970
+ | 570 | Idn not supported for tld "{0}". |
971
+ | 571 | Period invalid. |
972
+ | 572 | Command invalid. |
973
+ | 573 | Domain names not found. |
974
+ | 574 | TLD names not found. |
975
+ | 575 | Domain is not in updateable status. It must be active for name servers to be updated |
976
+ | 576 | Domain can be renewed in the last 1 month before the expire date |
977
+ | 580 | Transfer not supported for tld "{0}". |
978
+ | 581 | Child name server not found |
979
+ | 582 | Transfer started by other reseller. |
980
+ | 583 | Transfer not initialized. Please contact the support team. |
981
+ | 584 | Object status prohibits operation |
982
+ | 590 | Auth code is required for this transfer. |
983
+ | 591 | Auth code is not valid. |
984
+ | 592 | Transfer lock exists on domain. |
985
+ | 593 | Domain cannot be transferred, Status Information and Transfer Lock must comply with the required criteria. (Status Information: #ok) |
986
+ | 594 | Domain name server address could not resolved ({0}) |
987
+ | 595 | Contact information can not be read (whois.registrar.tld) Please make sure that privacy protection status open |
988
+ | 596 | Contact information could not verified |
989
+ | 597 | Tld Not Found |
990
+ | 598 | Unknown error occurred |
991
+ | 599 | Domain Forward Not Found |
992
+
993
+ ## License
994
+
995
+ Apache License 2.0 — see [LICENSE](LICENSE).