voicetel 2.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7c70dc67d349736fd88fd576459960103bcb4afbc7e9bb1512eb80569a7ad020
4
+ data.tar.gz: 6b3cb14e263751cb26e572d73cb511920af75047f84a9f4b2bb6f5bcef72550e
5
+ SHA512:
6
+ metadata.gz: 011706d8c9b695a96e2ad2e8b9594f6916b268565c81ffaf2b598bd5ce3e009c4205594e525bb8fe5501b169dd1679e4934bde97a2d1fb490ec948037c2c5578
7
+ data.tar.gz: a1349e176f37078243b4a295ea9180c031707e529ad8f109eb3a08567526d3d0817fde73efd4fd4da65c6c4629a118b7fa38e1e80c4e9857992767233a666278
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VoiceTel Communications
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # πŸ“ž VoiceTel Ruby SDK
2
+
3
+ The official Ruby client for the [VoiceTel REST API](https://voicetel.com/docs/api/v2.2/) β€” provision numbers, place orders, validate e911, send messages, and manage your account, all with idiomatic Ruby, structured errors, and zero codegen footprint.
4
+
5
+ ![Version](https://img.shields.io/badge/version-2.2.10-blue)
6
+ ![Ruby](https://img.shields.io/badge/ruby-3.1%2B-red)
7
+ ![License](https://img.shields.io/badge/license-MIT-green)
8
+ ![Coverage](https://img.shields.io/badge/coverage-%E2%89%A585%25-brightgreen)
9
+ ![Gem](https://img.shields.io/badge/gem-voicetel-blue)
10
+
11
+ ## πŸ“š Table of Contents
12
+
13
+ - [Features](#-features)
14
+ - [Installation](#-installation)
15
+ - [Quickstart](#-quickstart)
16
+ - [Authentication](#-authentication)
17
+ - [Resource Reference](#-resource-reference)
18
+ - [Error Handling](#-error-handling)
19
+ - [Rate Limits](#-rate-limits)
20
+ - [Development](#-development)
21
+ - [API Documentation](#-api-documentation)
22
+ - [Contributors](#-contributors)
23
+ - [Sponsors](#-sponsors)
24
+ - [License](#-license)
25
+
26
+ ## ✨ Features
27
+
28
+ ### 🧱 Idiomatic Ruby End-to-End
29
+ - **Resource-style API** β€” `client.numbers.list`, `client.messaging.send_message(...)`, just like the official SDKs in every other language.
30
+ - **snake_case everywhere**, automatically camelCased on the wire so the spec's `fromNumber` / `toNumber` / `messagingBrandId` look like plain Ruby.
31
+ - **RBS signatures** ship in `sig/` β€” opt into type-checking with Steep or Sorbet, or ignore them and stay dynamic.
32
+
33
+ ### πŸ” Production-Grade Transport
34
+ - **Faraday 2.x** with connection pooling, persistent HTTP, and pluggable adapters.
35
+ - **Automatic retry** on 429 / 5xx via `faraday-retry`, honoring `Retry-After`.
36
+ - **Configurable timeouts** per client.
37
+ - **Bearer auth** managed for you; password→key exchange handled by `client.login`.
38
+ - **Structured ApiError** β€” `:rate_limit`, `:not_found`, `:conflict`, ... pattern-match on a Symbol, or call `err.rate_limit?`.
39
+
40
+ ### πŸ“ž Complete API Coverage
41
+ - **Numbers** β€” list, get, add, remove, route, translate, CNAM, LIDB, fax, forward, SMS, messaging campaigns, port-out PIN, account moves.
42
+ - **Account** β€” profile, sub-accounts, CDRs, credits, payments, MRC, registration, password recovery.
43
+ - **e911** β€” record provisioning, address validation, lookup, removal.
44
+ - **Gateways** β€” list, create, update, delete, view bound numbers.
45
+ - **Messaging** β€” SMS & MMS sending, message history, 10DLC brand and campaign registration, per-number messaging state.
46
+ - **Lookups** β€” CNAM and LRN dips.
47
+ - **iNumbering** β€” inventory search, coverage queries, number orders, port-in submissions, port-out availability checks (v2.2.10 LRN + rate-center tier fields).
48
+ - **Support** β€” ticket create / read / update / delete, threaded messages, replies.
49
+ - **ACL** β€” IP allowlist management with structured 409 conflict bodies.
50
+ - **Authentication** β€” switch between Digest, IP-only, or hybrid modes; rotate passwords.
51
+
52
+ ### πŸ§ͺ Battle-Tested
53
+ - Unit tests against a mocked HTTP layer (`webmock`), every resource method covered.
54
+ - Read-only integration suite gated by env vars β€” safe for CI.
55
+ - β‰₯85% line coverage via `simplecov`.
56
+
57
+ ## πŸš€ Installation
58
+
59
+ ```bash
60
+ gem install voicetel
61
+ ```
62
+
63
+ Or in your `Gemfile`:
64
+
65
+ ```ruby
66
+ gem "voicetel", "~> 2.2.10"
67
+ ```
68
+
69
+ Requires Ruby 3.1 or later. Tested on 3.1, 3.2, 3.3, and 3.4.
70
+
71
+ ## 🏁 Quickstart
72
+
73
+ ```ruby
74
+ require "voicetel"
75
+
76
+ client = VoiceTel::Client.new
77
+
78
+ # Exchange username + password for an API key (one-time per session)
79
+ client.login(username: 1_000_000_001, password: "hunter2")
80
+
81
+ me = client.account.get
82
+ puts "Balance: $#{me['cash']} | Caller ID: #{me['callerId']}"
83
+
84
+ # List your numbers
85
+ client.numbers.list["numbers"].each do |n|
86
+ puts "#{n['number']} route=#{n['route']} cnam=#{n['cnam']} sms=#{n['smsEnabled']}"
87
+ end
88
+ ```
89
+
90
+ Or, if you already have an API key:
91
+
92
+ ```ruby
93
+ client = VoiceTel::Client.new(api_key: ENV.fetch("VOICETEL_API_KEY"))
94
+
95
+ coverage = client.i_numbering.coverage(state: "NJ")
96
+ coverage["coverage"].each do |bucket|
97
+ puts "#{bucket['npa']}-#{bucket['nxx']}: #{bucket['count']} TNs available"
98
+ end
99
+ ```
100
+
101
+ ## πŸ”‘ Authentication
102
+
103
+ Every endpoint requires `Authorization: Bearer <apikey>` **except** `POST /v2.2/account/api-key`, which exchanges username + password for a fresh key. `Client#login` handles the exchange and installs the returned key on the transport.
104
+
105
+ Re-fetch the API key after any password change β€” the old one is invalidated.
106
+
107
+ > Don't have credentials yet? Get them at **[voicetel.com/docs/api/v2.2/credentials](https://voicetel.com/docs/api/v2.2/credentials/)**.
108
+
109
+ ```ruby
110
+ client = VoiceTel::Client.new
111
+ key = client.login(username: 1_000_000_001, password: "hunter2")
112
+ # `key` is the freshly minted bearer; the client already has it installed.
113
+ ```
114
+
115
+ ## πŸ—ΊοΈ Resource Reference
116
+
117
+ | Resource | Operations | Example |
118
+ |---|---|---|
119
+ | `client.account` | Profile, CDR, credits, payments, MRC, signup, recovery, sub-accounts | `client.account.cdr(start: t1, end_at: t2)` |
120
+ | `client.acl` | IP allowlist (CIDR entries) | `client.acl.add(acl: [{ cidr: "1.2.3.0/24" }])` |
121
+ | `client.authentication` | SIP/HTTP auth mode + password | `client.authentication.update(auth_type: 1)` |
122
+ | `client.e911` | Records, address validation, provisioning | `client.e911.validate(address1: "...", city: "...", state: "NJ", zip: "07101")` |
123
+ | `client.gateways` | Termination routes | `client.gateways.list` |
124
+ | `client.i_numbering` | Inventory, orders, port-ins | `client.i_numbering.search_inventory(npa: 201)` |
125
+ | `client.lookups` | CNAM & LRN dips | `client.lookups.lrn("2015551234", "2012548000")` |
126
+ | `client.messaging` | SMS/MMS, 10DLC brands & campaigns | `client.messaging.send_message(from_number: "...", to_number: "...", text: "...")` |
127
+ | `client.numbers` | All operations on TNs on the account | `client.numbers.assign_campaign("2015551234", campaign_id: "CABC123")` |
128
+ | `client.support` | Tickets, replies, attachments | `client.support.create(subject: "...", message: "...")` |
129
+
130
+ Every method that takes a body accepts a plain Ruby Hash with `snake_case` keys; the SDK camelCases them when serializing to JSON:
131
+
132
+ ```ruby
133
+ client.messaging.send_message(
134
+ from_number: "2012548000",
135
+ to_number: "2015551234",
136
+ text: "Your code is 482917"
137
+ )
138
+ # β†’ POST /v2.2/messages { "fromNumber": "2012548000", "toNumber": "2015551234", "text": "Your code is 482917" }
139
+
140
+ client.numbers.assign_campaign("2015551234", campaign_id: "CABC123")
141
+ # β†’ PUT /v2.2/numbers/2015551234/messaging-campaign { "campaignId": "CABC123" }
142
+ ```
143
+
144
+ Responses come back as Hash/Array structures with the API's native (camelCase) keys β€” easy to pass straight to `to_json` or pretty-print.
145
+
146
+ ## 🚨 Error Handling
147
+
148
+ All HTTP errors raise `VoiceTel::ApiError`. The `kind` attribute is a Symbol; convenience predicates are exposed for readability:
149
+
150
+ | Status | `kind` | Predicate |
151
+ |--------|-----------------------|--------------------------|
152
+ | 400 | `:bad_request` | `err.bad_request?` |
153
+ | 401 | `:authentication` | `err.authentication?` |
154
+ | 403 | `:permission_denied` | `err.permission_denied?` |
155
+ | 404 | `:not_found` | `err.not_found?` |
156
+ | 409 | `:conflict` | `err.conflict?` |
157
+ | 429 | `:rate_limit` | `err.rate_limit?` |
158
+ | 5xx | `:server` | `err.server?` |
159
+ | other | `:unknown` | `err.unknown?` |
160
+
161
+ ```ruby
162
+ begin
163
+ client.numbers.get("9999999999")
164
+ rescue VoiceTel::ApiError => e
165
+ case e.kind
166
+ when :not_found then puts "Not on your account."
167
+ when :rate_limit then puts "Slow down β€” retry suggested."
168
+ when :authentication then puts "Re-login: #{e.message}"
169
+ else raise
170
+ end
171
+ end
172
+ ```
173
+
174
+ `ApiError#body` preserves the parsed response payload β€” useful on 409 ACL conflicts, where the server returns structured `{added, removed, failed}` detail.
175
+
176
+ ## ⏱️ Rate Limits
177
+
178
+ These endpoints are limited to **6 requests per hour per IP**:
179
+
180
+ - `account/info` (`client.account.get`)
181
+ - `account/cdr` (`client.account.cdr`)
182
+ - `account/recurring-charges` (`client.account.recurring_charges`)
183
+ - `account/payments` (`client.account.payments`)
184
+ - `account/registration` (`client.account.registration`)
185
+ - `account/api-key` (`client.login`)
186
+
187
+ The SDK automatically retries 429 responses with `Retry-After` honored, up to `max_retries` (default `2`). To raise it:
188
+
189
+ ```ruby
190
+ VoiceTel::Client.new(api_key: key, max_retries: 4, timeout: 60)
191
+ ```
192
+
193
+ ## πŸ› οΈ Development
194
+
195
+ ```bash
196
+ git clone https://github.com/voicetel/ruby-sdk
197
+ cd ruby-sdk
198
+ bundle install
199
+
200
+ # Unit tests (fast, no network)
201
+ bundle exec rspec
202
+
203
+ # Lint
204
+ bundle exec rubocop
205
+
206
+ # Integration tests (live api.voicetel.com, read-only)
207
+ cp .env.example .env # fill in VOICETEL_USERNAME / VOICETEL_PASSWORD
208
+ INTEGRATION=1 bundle exec rspec spec/integration
209
+
210
+ # Build the gem
211
+ gem build voicetel.gemspec
212
+ ```
213
+
214
+ ## πŸ“– API Documentation
215
+
216
+ - **Reference docs:** [voicetel.com/docs/api/v2.2/](https://voicetel.com/docs/api/v2.2/)
217
+ - **Interactive playground:** [voicetel.com/docs/api/v2.2/playground/](https://voicetel.com/docs/api/v2.2/playground/) β€” try the API in your browser without writing any code
218
+ - **API credentials:** [voicetel.com/docs/api/v2.2/credentials/](https://voicetel.com/docs/api/v2.2/credentials/)
219
+ - **Type signatures:** see `sig/voicetel.rbs`
220
+
221
+ ## πŸ™Œ Contributors
222
+
223
+ - [Michael Mavroudis](https://github.com/mavroudis) β€” Lead Developer
224
+
225
+ Contributions welcome. Open an issue describing the change you want to make, or send a pull request against `main`.
226
+
227
+ ## πŸ’– Sponsors
228
+
229
+ | Sponsor | Contribution |
230
+ |---------|--------------|
231
+ | [VoiceTel Communications](https://voicetel.com) | Primary development and production hosting |
232
+
233
+ ## πŸ“„ License
234
+
235
+ This project is licensed under the MIT License β€” see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VoiceTel
4
+ # ApiError is raised on every non-2xx response (and on transport failures).
5
+ #
6
+ # `kind` is a Symbol classifying the failure so callers can pattern-match
7
+ # without checking HTTP status codes directly. The convenience predicates
8
+ # (`rate_limit?`, `not_found?`, etc.) are exposed for readability.
9
+ class ApiError < StandardError
10
+ KINDS = %i[
11
+ bad_request
12
+ authentication
13
+ permission_denied
14
+ not_found
15
+ conflict
16
+ rate_limit
17
+ server
18
+ unknown
19
+ ].freeze
20
+
21
+ attr_reader :kind, :status_code, :code, :body
22
+
23
+ def initialize(message, kind: :unknown, status_code: nil, code: nil, body: nil)
24
+ super(message)
25
+ @kind = kind
26
+ @status_code = status_code
27
+ @code = code
28
+ @body = body
29
+ end
30
+
31
+ # Convenience predicates β€” one per kind.
32
+ KINDS.each do |k|
33
+ define_method("#{k}?") { @kind == k }
34
+ end
35
+
36
+ def self.kind_from_status(status)
37
+ case status
38
+ when 400 then :bad_request
39
+ when 401 then :authentication
40
+ when 403 then :permission_denied
41
+ when 404 then :not_found
42
+ when 409 then :conflict
43
+ when 429 then :rate_limit
44
+ when 500..599 then :server
45
+ else :unknown
46
+ end
47
+ end
48
+
49
+ def self.from_response(status, code, message, body)
50
+ new(
51
+ message,
52
+ kind: kind_from_status(status),
53
+ status_code: status,
54
+ code: code,
55
+ body: body
56
+ )
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+ require "faraday/net_http_persistent"
6
+ require "json"
7
+ require "securerandom"
8
+ require "stringio"
9
+ require "zlib"
10
+
11
+ require_relative "../api_error"
12
+ require_relative "../version"
13
+
14
+ module VoiceTel
15
+ module Internal
16
+ # Transport is the low-level Faraday wrapper used by every resource service.
17
+ # It owns the connection, installs the bearer token, retries 429/5xx with
18
+ # Retry-After honored, and strips the `{status, data}` envelope from
19
+ # responses before returning the inner payload.
20
+ class Transport
21
+ RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
22
+
23
+ attr_reader :base_url, :user_agent, :timeout, :max_retries
24
+ attr_accessor :api_key
25
+
26
+ def initialize(base_url:, api_key: nil, timeout: 30, max_retries: 2, user_agent: nil, adapter: nil)
27
+ @base_url = (base_url || VoiceTel::DEFAULT_BASE_URL).chomp("/")
28
+ @api_key = api_key
29
+ @timeout = timeout
30
+ @max_retries = max_retries
31
+ @user_agent = user_agent || VoiceTel::USER_AGENT
32
+ @adapter = adapter
33
+ @conn = build_connection
34
+ end
35
+
36
+ # Perform an HTTP request. Returns the parsed inner data (envelope
37
+ # stripped) on success, or raises ApiError on failure. Returns nil on
38
+ # 204 No Content.
39
+ #
40
+ # @param method [Symbol] one of :get, :post, :put, :patch, :delete
41
+ # @param path [String] absolute path including the /v2.2 prefix
42
+ # @param query [Hash, nil] query string params, snake_case keys
43
+ # @param body [Hash, nil] request body, snake_case keys (translated to camelCase)
44
+ # @param require_auth [Boolean] false skips the bearer header
45
+ def request(method, path, query: nil, body: nil, require_auth: true)
46
+ if require_auth && (@api_key.nil? || @api_key.empty?)
47
+ raise ApiError.new(
48
+ "no api key set; call client.login(...) or pass api_key: to Client.new",
49
+ kind: :authentication
50
+ )
51
+ end
52
+
53
+ headers = build_headers(require_auth)
54
+ headers["Idempotency-Key"] = SecureRandom.uuid if %i[post put patch].include?(method)
55
+ response = @conn.run_request(method, path, body ? JSON.generate(camelize_keys(body)) : nil, headers) do |req|
56
+ req.params.update(camelize_keys(query)) if query && !query.empty?
57
+ end
58
+
59
+ handle_response(response)
60
+ rescue Faraday::TimeoutError => e
61
+ raise ApiError.new("voicetel: request timed out: #{e.message}", kind: :unknown)
62
+ rescue Faraday::ConnectionFailed => e
63
+ raise ApiError.new("voicetel: connection failed: #{e.message}", kind: :unknown)
64
+ end
65
+
66
+ # Recursively transform a Ruby snake_case key Hash/Array structure into
67
+ # a Hash with camelCase keys, suitable for JSON serialization. Values
68
+ # that are already strings/numbers/bools/nil/symbols pass through.
69
+ def camelize_keys(obj)
70
+ case obj
71
+ when Hash
72
+ obj.each_with_object({}) do |(k, v), h|
73
+ h[snake_to_camel(k)] = camelize_keys(v)
74
+ end
75
+ when Array
76
+ obj.map { |v| camelize_keys(v) }
77
+ else
78
+ obj
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def snake_to_camel(key)
85
+ s = key.to_s
86
+ return s unless s.include?("_")
87
+
88
+ head, *rest = s.split("_")
89
+ head + rest.map(&:capitalize).join
90
+ end
91
+
92
+ def build_connection
93
+ Faraday.new(url: @base_url) do |f|
94
+ f.request :retry, retry_options
95
+ f.options.timeout = @timeout
96
+ f.options.open_timeout = @timeout
97
+ f.adapter(@adapter || :net_http_persistent)
98
+ end
99
+ end
100
+
101
+ def retry_options
102
+ {
103
+ max: @max_retries,
104
+ interval: 0.5,
105
+ interval_randomness: 0.0,
106
+ backoff_factor: 2,
107
+ max_interval: 8,
108
+ retry_statuses: RETRYABLE_STATUSES,
109
+ methods: %i[get post put patch delete],
110
+ retry_if: ->(env, _exception) { RETRYABLE_STATUSES.include?(env.status) }
111
+ }
112
+ end
113
+
114
+ def decode_body(response)
115
+ raw = response.body.to_s
116
+ encoding = response.headers["content-encoding"] || response.headers["Content-Encoding"]
117
+ return raw unless encoding&.downcase&.include?("gzip")
118
+
119
+ Zlib::GzipReader.new(StringIO.new(raw)).read
120
+ end
121
+
122
+ def build_headers(require_auth)
123
+ h = {
124
+ "User-Agent" => @user_agent,
125
+ "Accept" => "application/json",
126
+ "Accept-Encoding" => "gzip",
127
+ "Content-Type" => "application/json"
128
+ }
129
+ h["Authorization"] = "Bearer #{@api_key}" if require_auth && @api_key && !@api_key.empty?
130
+ h
131
+ end
132
+
133
+ def handle_response(response)
134
+ status = response.status
135
+ raw = decode_body(response)
136
+
137
+ if status >= 200 && status < 300
138
+ return nil if raw.empty? || status == 204
139
+
140
+ parsed = parse_json(raw)
141
+ return unwrap(parsed)
142
+ end
143
+
144
+ raise build_error(status, raw)
145
+ end
146
+
147
+ def parse_json(raw)
148
+ return nil if raw.nil? || raw.empty?
149
+
150
+ JSON.parse(raw)
151
+ rescue JSON::ParserError
152
+ raw
153
+ end
154
+
155
+ def unwrap(parsed)
156
+ if parsed.is_a?(Hash) && parsed.key?("status") && parsed.key?("data")
157
+ parsed["data"]
158
+ else
159
+ parsed
160
+ end
161
+ end
162
+
163
+ def build_error(status, raw)
164
+ parsed = parse_json(raw)
165
+ code = nil
166
+ message = nil
167
+
168
+ if parsed.is_a?(Hash)
169
+ code = parsed["code"] || parsed["error"]
170
+ message = parsed["message"] || parsed["error"]
171
+ end
172
+ message ||= "HTTP #{status}"
173
+
174
+ ApiError.from_response(status, code, "voicetel: HTTP #{status}: #{message}", parsed || raw)
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module VoiceTel
6
+ module Resources
7
+ # AccountService β€” Account tag in the OpenAPI spec.
8
+ #
9
+ # Rate-limited endpoints (6 req/hour/IP, shared with Client#login):
10
+ # `cdr`, `recurring_charges`, `payments`, `registration`, and `info`.
11
+ class Account < Base
12
+ # GET /v2.2/account β€” return the authenticated account profile.
13
+ def get
14
+ @transport.request(:get, "/v2.2/account")
15
+ end
16
+
17
+ # PUT /v2.2/account β€” partial-update account settings.
18
+ def update(body)
19
+ @transport.request(:put, "/v2.2/account", body: body)
20
+ end
21
+
22
+ # POST /v2.2/account β€” create a sub-account (admin-only).
23
+ def add(body)
24
+ @transport.request(:post, "/v2.2/account", body: body)
25
+ end
26
+
27
+ # POST /v2.2/accounts β€” public sign-up flow.
28
+ def signup(body)
29
+ @transport.request(:post, "/v2.2/accounts", body: body)
30
+ end
31
+
32
+ # GET /v2.2/account/cdr β€” call detail records. Rate-limited.
33
+ def cdr(start: nil, end_at: nil)
34
+ q = compact_query("start" => start, "end" => end_at)
35
+ @transport.request(:get, "/v2.2/account/cdr", query: q)
36
+ end
37
+
38
+ # GET /v2.2/account/credits β€” credit history.
39
+ def credits
40
+ @transport.request(:get, "/v2.2/account/credits")
41
+ end
42
+
43
+ # GET /v2.2/account/recurring-charges β€” active monthly recurring charges. Rate-limited.
44
+ def recurring_charges
45
+ @transport.request(:get, "/v2.2/account/recurring-charges")
46
+ end
47
+
48
+ # GET /v2.2/account/payments β€” payment history. Rate-limited.
49
+ def payments
50
+ @transport.request(:get, "/v2.2/account/payments")
51
+ end
52
+
53
+ # GET /v2.2/account/registration β€” current SIP registration. Rate-limited.
54
+ def registration
55
+ @transport.request(:get, "/v2.2/account/registration")
56
+ end
57
+
58
+ # POST /v2.2/account/recovery β€” start password recovery. No auth required.
59
+ def recover(body)
60
+ @transport.request(:post, "/v2.2/account/recovery", body: body, require_auth: false)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module VoiceTel
6
+ module Resources
7
+ # AclService β€” manages the IP allowlist (CIDR entries).
8
+ #
9
+ # The DELETE /v2.2/acl endpoint is unusual: it returns 200 with a body
10
+ # (not 204). 409 conflicts include partial success/failure detail in the
11
+ # body β€” surfaced through ApiError#body so callers can inspect it.
12
+ class Acl < Base
13
+ def list
14
+ @transport.request(:get, "/v2.2/acl")
15
+ end
16
+
17
+ # body example: { acl: [{ cidr: "1.2.3.0/24" }] }
18
+ def add(body)
19
+ @transport.request(:post, "/v2.2/acl", body: body)
20
+ end
21
+
22
+ def remove(body)
23
+ @transport.request(:delete, "/v2.2/acl", body: body)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module VoiceTel
6
+ module Resources
7
+ # AuthenticationService β€” SIP/HTTP authentication settings (mode + password).
8
+ #
9
+ # auth_type values: 0 = Digest, 1 = IP Auth, 2 = Digest OR IP, 3 = Digest AND IP.
10
+ class Authentication < Base
11
+ AUTH_TYPE_DIGEST = 0
12
+ AUTH_TYPE_IP = 1
13
+ AUTH_TYPE_DIGEST_OR_IP = 2
14
+ AUTH_TYPE_DIGEST_AND_IP = 3
15
+
16
+ def get
17
+ @transport.request(:get, "/v2.2/auth")
18
+ end
19
+
20
+ def update(body)
21
+ @transport.request(:put, "/v2.2/auth", body: body)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VoiceTel
4
+ module Resources
5
+ # Shared base for every resource service. Each service receives the
6
+ # client's transport on construction and uses it for HTTP calls. Keeping
7
+ # this thin lets us keep resource files focused on their endpoints.
8
+ class Base
9
+ def initialize(transport)
10
+ @transport = transport
11
+ end
12
+
13
+ # Compact a query hash, dropping nil and empty-string values, before
14
+ # handing it to the transport. Some endpoints take a lot of optional
15
+ # filters and writing `nil` everywhere at call sites would be noisy.
16
+ def compact_query(hash)
17
+ hash.reject { |_, v| v.nil? || v == "" }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module VoiceTel
6
+ module Resources
7
+ # E911Service β€” provisioning, validation, lookup, removal.
8
+ #
9
+ # Note: request bodies take a 10-digit TN in `dn`; responses return the
10
+ # 11-digit E.164 US form (leading 1).
11
+ class E911 < Base
12
+ def list
13
+ @transport.request(:get, "/v2.2/e911")
14
+ end
15
+
16
+ def create(body)
17
+ @transport.request(:post, "/v2.2/e911", body: body)
18
+ end
19
+
20
+ def validate(body)
21
+ @transport.request(:post, "/v2.2/e911/validations", body: body)
22
+ end
23
+
24
+ def get(dn)
25
+ @transport.request(:get, "/v2.2/e911/#{dn}")
26
+ end
27
+
28
+ def provision(dn, body)
29
+ @transport.request(:put, "/v2.2/e911/#{dn}", body: body)
30
+ end
31
+
32
+ # Returns nil on 204 No Content.
33
+ def remove(dn)
34
+ @transport.request(:delete, "/v2.2/e911/#{dn}")
35
+ end
36
+ end
37
+ end
38
+ end