sendly 3.40.0 → 4.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -1
- data/Gemfile.lock +3 -3
- data/README.md +153 -0
- data/lib/sendly/account_resource.rb +4 -4
- data/lib/sendly/calls_resource.rb +409 -0
- data/lib/sendly/campaigns_resource.rb +8 -8
- data/lib/sendly/client.rb +7 -0
- data/lib/sendly/contacts_resource.rb +9 -9
- data/lib/sendly/enterprise.rb +26 -26
- data/lib/sendly/numbers_resource.rb +22 -2
- data/lib/sendly/templates_resource.rb +6 -6
- data/lib/sendly/verify.rb +3 -3
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/webhooks.rb +4 -0
- data/lib/sendly/webhooks_resource.rb +10 -10
- data/lib/sendly.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84b10285655be846bb857f5e11388a893d916838d480accfc46a646c16dd68a3
|
|
4
|
+
data.tar.gz: cfe12c76eeea0a6db834c09d97c7546260c4a313f5f6d9c64a549d467884ae3b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c528f37943ed1c30eb8b6929097cb167ba9be070e0a8b3ec6d69949e9710a38ccbf1bdb04ce57d92a569fabdb68e56df3e50104e3ffffb620d9c6962d3e4e73
|
|
7
|
+
data.tar.gz: 73c7ba0af6ac53f75b33759b2566292f64ee5199c13f088da4ebb3b8bf68afea4b00fdddb76a0e7f3a7a1fa1547162ab94fa81a119d0b18004bce650ca6ed434
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
# sendly (Ruby)
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 4.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **Voice calls: `client.calls`.** Place phone calls handled by your AI agents, list and inspect them, end one early and fetch recordings, over the new `/api/v1/calls` routes. `create(to:, agent_id:, from: nil, context: nil, metadata: nil)` returns a `Sendly::Call` that is `ringing`; `list` takes `limit:`, `offset:`, `status:`, `direction:`, `kind:`, `agent_id:`, `to:` and `from:` and returns an Enumerable `Sendly::CallList` with `total`, `limit`, `offset` and `has_more?`; `get(id)` adds the `transcript` (an array of `Sendly::CallTranscriptLine`) on agent-handled calls and leaves it `nil` otherwise; `hangup(id)` cancels a ringing call or completes an active one and returns an already-ended call unchanged; `recording(id)` returns a `Sendly::CallRecording` whose signed `url` is set only while `ready?` and expires after five minutes. `create` and `hangup` send the client's usual `Idempotency-Key` and accept `idempotency_key:`. `Sendly::Call` also reads the snake_case object carried by the `call.started`, `call.completed` and `call.recording.ready` webhooks, including the new `billing` and `metadata` keys. Vocabularies are published as `Sendly::Call::STATUSES`, `::HANGUP_CLASSES` and `::ERROR_CODES`. `Sendly::PhoneNumber` gains `voice_enabled` (with `voice_enabled?`), `voice_mode` (`Sendly::PhoneNumber::VOICE_MODES`) and `raw`, so `client.numbers.list` can pick the `from` number for a call. Reads need the `calls:read` scope; writes need `calls:write` and a live key. Voice is enabled workspace by workspace: until it is on for yours the routes answer 404 `voice_not_enabled`, which raises `Sendly::NotFoundError`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
call = client.calls.create(
|
|
11
|
+
to: "+15555550123",
|
|
12
|
+
agent_id: "3c4d5e6f-7081-4293-a4b5-c6d7e8f90a1b",
|
|
13
|
+
context: "Confirm the 3pm appointment on Tuesday."
|
|
14
|
+
)
|
|
15
|
+
call = client.calls.get(call.id)
|
|
16
|
+
call.transcript&.each { |line| puts "#{line.speaker}: #{line.text}" }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 4.0.0
|
|
20
|
+
|
|
21
|
+
**Upgrading from 3.40.0:** that release already contained the breaking changes below, published by mistake as a minor version. 4.0.0 carries them under the correct major. Relative to 3.40.0, the only new changes are under **Security**.
|
|
4
22
|
|
|
5
23
|
### Breaking Changes
|
|
6
24
|
|
|
@@ -22,6 +40,120 @@
|
|
|
22
40
|
|
|
23
41
|
- **`id` is no longer filled from an unrelated `id` key.** `contact.auto_flagged` carries the contact under `id` and the message that failed under `message_id`, so `event.data.message_id` returned the *contact* id — a handler that marked that message failed acted on the wrong row. Contact events have no message view at all now; read the message with `event.data[:message_id]`.
|
|
24
42
|
|
|
43
|
+
### Migrating from 3.x
|
|
44
|
+
|
|
45
|
+
Nothing outside webhook handling changed. The client, every resource and every
|
|
46
|
+
`message.*` handler you already have keep working as written; the list below is
|
|
47
|
+
the whole edit.
|
|
48
|
+
|
|
49
|
+
**1. Reading a message field off a lifecycle event now raises.** 3.x decoded
|
|
50
|
+
every `data.object` into a `Sendly::WebhookMessageData`, so an `rcs_agent.live`
|
|
51
|
+
handler that asked for `event.data.from` was handed `""`, `event.data.segments`
|
|
52
|
+
`1` and `event.data.credits_used` `0` — values the event never carried, not
|
|
53
|
+
distinguishable from real ones, and no error was raised. The same calls raise
|
|
54
|
+
`NoMethodError` in 4.0, naming the keys that did arrive. That failure is the
|
|
55
|
+
point of this release rather than an accident of it: the value it replaces was
|
|
56
|
+
wrong, and silently so.
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
# 3.x — silently wrong, on every RCS event
|
|
60
|
+
event.data.from # => ""
|
|
61
|
+
event.data.segments # => 1
|
|
62
|
+
|
|
63
|
+
# 4.0 — the same call
|
|
64
|
+
event.data.from
|
|
65
|
+
# => NoMethodError: undefined method 'from' for Sendly::WebhookObject:
|
|
66
|
+
# this event's data.object carries agent_id, name, stage, organization_id
|
|
67
|
+
|
|
68
|
+
# 4.0 — the edit: read the object the event actually carries
|
|
69
|
+
event.data[:agent_id] # => "bb22cc33-dd44-4e55-9f66-001122334455"
|
|
70
|
+
event.data.stage # => "live"
|
|
71
|
+
event.raw_object # => the whole data.object Hash, untouched
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**2. `event.data` is a message view only on `message.*` events.** In 3.x it was
|
|
75
|
+
a `WebhookMessageData` whatever the event was, so `event.data.id` and
|
|
76
|
+
`event.data.status` answered for anything — with the payload's value when the
|
|
77
|
+
key happened to exist, and with `''` or `nil` when it did not. In 4.0 a
|
|
78
|
+
lifecycle event's `event.data` is a `Sendly::WebhookObject`: the keys the
|
|
79
|
+
payload carried are readable and nothing else is. `event.message` is new in 4.0
|
|
80
|
+
and is the message view; it is `nil` outside `message.*`, so guard it with
|
|
81
|
+
`event.message?` rather than calling into it unconditionally.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# 3.x — answered for every event type; "" when the payload had no id
|
|
85
|
+
mark_delivered(event.data.id)
|
|
86
|
+
|
|
87
|
+
# 4.0 — split the branches
|
|
88
|
+
case event.type
|
|
89
|
+
when Sendly::Webhooks::EVENT_MESSAGE_DELIVERED
|
|
90
|
+
mark_delivered(event.message.id) # same value as 3.x
|
|
91
|
+
when Sendly::Webhooks::EVENT_NUMBER_ACTIVATED
|
|
92
|
+
number_active(event.data[:id]) # the number's id, as it always was
|
|
93
|
+
when Sendly::Webhooks::EVENT_RCS_AGENT_LIVE
|
|
94
|
+
agent_went_live(event.data[:agent_id]) # unreachable in 3.x
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**3. `contact.auto_flagged` no longer reports the contact as the message.** The
|
|
99
|
+
payload carries the contact under `id` and the message that failed under
|
|
100
|
+
`message_id`; 3.x filled the message view's `id` from the contact, so a handler
|
|
101
|
+
that marked "the message" failed acted on the wrong record.
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# 3.x — the CONTACT id, presented as a message id
|
|
105
|
+
event.data.message_id # => "ct_9"
|
|
106
|
+
|
|
107
|
+
# 4.0
|
|
108
|
+
event.data[:id] # => "ct_9" the contact
|
|
109
|
+
event.data[:message_id] # => "msg_77" the message that failed
|
|
110
|
+
event.message # => nil
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**4. Message fields the payload omitted are `nil`, not a default.** On a
|
|
114
|
+
`message.*` event the readers still exist, so this raises nothing — it changes
|
|
115
|
+
what you get. `key?` separates "absent" from "arrived as null".
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# message.delivered, on a payload that carried no segments
|
|
119
|
+
event.data.segments # 3.x => 1 4.0 => nil
|
|
120
|
+
event.data.credits_used # 3.x => 0 4.0 => nil
|
|
121
|
+
event.data.direction # 3.x => "outbound" 4.0 => nil
|
|
122
|
+
event.data.key?(:segments) # => false when absent, true when it arrived as null
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**5. `event.data.to_h` is now `data.object` as it arrived.** It used to be a
|
|
126
|
+
compacted subset of the typed message fields, dropping `text`, `metadata`,
|
|
127
|
+
`media_urls`, `message_format` and `organization_id` and renaming `message_id`
|
|
128
|
+
to `id`. Code that persisted `to_h` will start seeing the full object.
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
event.data.to_h == event.raw_object # => true, for every event type
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**6. If you want a typed object for a lifecycle event, ask for one.**
|
|
135
|
+
`#object_as` fills a Struct or Data class from the members it declares and
|
|
136
|
+
ignores the rest of the payload, so a field added to the event later cannot
|
|
137
|
+
break the call.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
AgentLive = Struct.new(:agent_id, :name, :stage)
|
|
141
|
+
agent = event.object_as(AgentLive)
|
|
142
|
+
agent.stage # => "live"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Deprecations
|
|
146
|
+
|
|
147
|
+
- **`Sendly::Webhooks::EVENT_MESSAGE_QUEUED` is deprecated.** The API has never
|
|
148
|
+
emitted `message.queued`, and subscribing to it fails: `client.webhooks.create`
|
|
149
|
+
and `client.webhooks.update` reject any event outside the API's list with a
|
|
150
|
+
400, raised here as `Sendly::ValidationError`. The constant stays exported in
|
|
151
|
+
4.0 so existing code still loads, and will be removed in the next major.
|
|
152
|
+
`message.undelivered` is rejected on subscribe the same way and has never had
|
|
153
|
+
a constant in this SDK. Subscribe to `message.sent`, `message.failed` and
|
|
154
|
+
`message.bounced` instead. Every other `EVENT_*` constant matches the API's
|
|
155
|
+
list exactly.
|
|
156
|
+
|
|
25
157
|
### Minor Changes
|
|
26
158
|
|
|
27
159
|
- **`Sendly::WebhookEvent#raw_object`** carries `data.object` exactly as it arrived, for every event type, and **`#object_as(klass)`** reads it as a type of your choosing (`event.object_as(AgentLive)`). `#object` is an alias for `raw_object`.
|
|
@@ -48,6 +180,11 @@
|
|
|
48
180
|
|
|
49
181
|
- **`Sendly::ValidationError#field_errors` carries the API's `errors` list** (`[{ "path", "message" }, ...]`) when a 400 or 422 response includes one, instead of always being `nil`. RCS registration uses it to say which brand, agent, campaign or device field needs attention.
|
|
50
182
|
|
|
183
|
+
|
|
184
|
+
### Security
|
|
185
|
+
|
|
186
|
+
- **Path parameters are percent-encoded.** Every id you pass is now encoded (`URI.encode_www_form_component`) before it goes into the request path. An id containing `/`, `?` or `#` used to change which endpoint the request reached: an id of `../../account/keys` left its collection and hit another endpoint carrying your API key. Ordinary ids are sent byte-for-byte as before.
|
|
187
|
+
|
|
51
188
|
## 3.38.0
|
|
52
189
|
|
|
53
190
|
### Minor Changes
|
data/Gemfile.lock
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
sendly (
|
|
4
|
+
sendly (4.1.0)
|
|
5
5
|
faraday (~> 2.0)
|
|
6
6
|
faraday-retry (~> 2.0)
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
-
addressable (2.
|
|
11
|
+
addressable (2.8.8)
|
|
12
12
|
public_suffix (>= 2.0.2, < 8.0)
|
|
13
13
|
ast (2.4.3)
|
|
14
14
|
bigdecimal (3.3.1)
|
|
@@ -74,7 +74,7 @@ GEM
|
|
|
74
74
|
unicode-emoji (~> 4.1)
|
|
75
75
|
unicode-emoji (4.1.0)
|
|
76
76
|
uri (1.1.1)
|
|
77
|
-
webmock (3.26.
|
|
77
|
+
webmock (3.26.1)
|
|
78
78
|
addressable (>= 2.8.0)
|
|
79
79
|
crack (>= 0.3.2)
|
|
80
80
|
hashdiff (>= 0.4.0, < 2.0.0)
|
data/README.md
CHANGED
|
@@ -342,6 +342,16 @@ rotation = client.webhooks.rotate_secret("whk_xxx")
|
|
|
342
342
|
client.webhooks.delete("whk_xxx")
|
|
343
343
|
```
|
|
344
344
|
|
|
345
|
+
Subscribe with the `Sendly::Webhooks::EVENT_*` constants rather than string
|
|
346
|
+
literals — a typo then fails at load time instead of in a 400. Every constant
|
|
347
|
+
but one names an event the API accepts on subscribe. The exception is
|
|
348
|
+
`EVENT_MESSAGE_QUEUED`: `message.queued` has never been emitted and is
|
|
349
|
+
rejected on subscribe with a 400 (`Sendly::ValidationError`). It is
|
|
350
|
+
deprecated, kept only so existing code still loads, and will be removed in the
|
|
351
|
+
next major. `message.undelivered` is rejected in the same way and has never had
|
|
352
|
+
a constant here. Subscribe to `message.sent`, `message.failed` and
|
|
353
|
+
`message.bounced` instead.
|
|
354
|
+
|
|
345
355
|
### Receiving events
|
|
346
356
|
|
|
347
357
|
`Sendly::Webhooks.parse_event` verifies the signature and returns a
|
|
@@ -403,6 +413,66 @@ Note that `contact.auto_flagged` carries the contact under `id` and the message
|
|
|
403
413
|
that failed under `message_id`; read the message with
|
|
404
414
|
`event.data[:message_id]`.
|
|
405
415
|
|
|
416
|
+
### Handling a lifecycle event
|
|
417
|
+
|
|
418
|
+
Only `message.*` events carry a message. A lifecycle event — `rcs_*`,
|
|
419
|
+
`whatsapp_*`, `call.*`, `brand.*`, `campaign.*`, `assignment.*`, `number.*`,
|
|
420
|
+
`port*`, `contact*`, `conversation.*`, `draft.*` — carries a different object,
|
|
421
|
+
so `event.message` is `nil` and you read `data.object` off `event.data` or
|
|
422
|
+
`event.raw_object`.
|
|
423
|
+
|
|
424
|
+
```ruby
|
|
425
|
+
require "sendly"
|
|
426
|
+
|
|
427
|
+
# Framework-neutral: hand it the raw request body and a headers Hash.
|
|
428
|
+
def handle_sendly_webhook(raw_body, headers)
|
|
429
|
+
event = Sendly::Webhooks.parse_event(
|
|
430
|
+
raw_body,
|
|
431
|
+
headers["X-Sendly-Signature"],
|
|
432
|
+
ENV.fetch("SENDLY_WEBHOOK_SECRET"),
|
|
433
|
+
timestamp: headers["X-Sendly-Timestamp"]
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
case event.type
|
|
437
|
+
when Sendly::Webhooks::EVENT_RCS_AGENT_LIVE
|
|
438
|
+
# data.object is the agent: agent_id, name, stage
|
|
439
|
+
puts "RCS agent #{event.data[:name]} is #{event.data.stage}"
|
|
440
|
+
puts event.data[:agent_id]
|
|
441
|
+
when Sendly::Webhooks::EVENT_NUMBER_ACTIVATED
|
|
442
|
+
# data.object is the number: id, phone, status, country_code, source
|
|
443
|
+
puts "#{event.data[:phone]} active in #{event.data[:country_code]}"
|
|
444
|
+
when Sendly::Webhooks::EVENT_CONTACT_AUTO_FLAGGED
|
|
445
|
+
# the contact is `id`; the message that failed is `message_id`
|
|
446
|
+
puts "flagged #{event.data[:id]} (#{event.data[:invalid_reason]})"
|
|
447
|
+
puts "from message #{event.data[:message_id]}"
|
|
448
|
+
when Sendly::Webhooks::EVENT_MESSAGE_DELIVERED
|
|
449
|
+
# message.* events, and only these, also get the typed message view
|
|
450
|
+
puts "#{event.message.id} delivered to #{event.message.to}"
|
|
451
|
+
else
|
|
452
|
+
# An event type this SDK predates still parses; raw_object holds all of it.
|
|
453
|
+
puts "unhandled #{event.type}: #{event.raw_object.inspect}"
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
:ok
|
|
457
|
+
rescue Sendly::WebhookSignatureError
|
|
458
|
+
:unauthorized
|
|
459
|
+
end
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Reading a message field off a lifecycle event fails loudly rather than
|
|
463
|
+
answering with a value the event never carried:
|
|
464
|
+
|
|
465
|
+
```ruby
|
|
466
|
+
event.data.from
|
|
467
|
+
# => NoMethodError: undefined method 'from' for Sendly::WebhookObject:
|
|
468
|
+
# this event's data.object carries agent_id, name, stage, organization_id
|
|
469
|
+
event.message # => nil
|
|
470
|
+
event.message.id # => NoMethodError — nil has no #id
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Branch on `event.type`, or on `event.message?` / `event.verification?`, before
|
|
474
|
+
reaching for a typed view.
|
|
475
|
+
|
|
406
476
|
## Account & Credits
|
|
407
477
|
|
|
408
478
|
```ruby
|
|
@@ -732,6 +802,7 @@ end
|
|
|
732
802
|
# Get one number you own (includes is_default, which the list omits)
|
|
733
803
|
number = client.numbers.get('num_abc123')
|
|
734
804
|
puts "#{number.phone_number} — default sender: #{number.is_default}"
|
|
805
|
+
puts "voice: #{number.voice_enabled?} (#{number.voice_mode})"
|
|
735
806
|
|
|
736
807
|
# Update a number — make it the default sender (must be active),
|
|
737
808
|
# and/or cancel a scheduled release ("keep this number")
|
|
@@ -1101,6 +1172,88 @@ client.messages.send(
|
|
|
1101
1172
|
)
|
|
1102
1173
|
```
|
|
1103
1174
|
|
|
1175
|
+
## Voice Calls
|
|
1176
|
+
|
|
1177
|
+
Place phone calls that one of your AI agents handles, list and inspect
|
|
1178
|
+
calls, end a call early, and download recordings. Agents are configured in
|
|
1179
|
+
the dashboard under Calls, then Agents; the number you call from must have
|
|
1180
|
+
voice switched on there (Calls, then Settings) and an emergency address
|
|
1181
|
+
registered before it can place outbound calls. Each `Sendly::PhoneNumber`
|
|
1182
|
+
from `client.numbers.list` carries `voice_enabled?` and `voice_mode`
|
|
1183
|
+
(`"none"`, `"ring_dashboard"` or `"agent"`) so you can pick a `from`:
|
|
1184
|
+
|
|
1185
|
+
```ruby
|
|
1186
|
+
from = client.numbers.list[:numbers].find(&:voice_enabled?)&.phone_number
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
Calls are prepaid from your credit balance per started minute: 2 credits a
|
|
1190
|
+
minute outbound, plus 8 a minute while an AI agent is on the call (10 in
|
|
1191
|
+
total for an API-placed call). Unanswered calls cost nothing. Destinations
|
|
1192
|
+
are US and Canadian numbers. Reads need the `calls:read` scope; `create`
|
|
1193
|
+
and `hangup` need `calls:write` and a live key.
|
|
1194
|
+
|
|
1195
|
+
> **Rolling out.** Voice is enabled workspace by workspace. Until it is on
|
|
1196
|
+
> for yours, every call method raises `Sendly::NotFoundError`
|
|
1197
|
+
> (`voice_not_enabled`).
|
|
1198
|
+
|
|
1199
|
+
```ruby
|
|
1200
|
+
# Place a call. Returns at once with the call ringing; the agent greets the
|
|
1201
|
+
# callee when they answer and uses `context` for this call only.
|
|
1202
|
+
call = client.calls.create(
|
|
1203
|
+
to: "+15555550123",
|
|
1204
|
+
agent_id: "3c4d5e6f-7081-4293-a4b5-c6d7e8f90a1b",
|
|
1205
|
+
from: "+15555550188", # optional when you have one voice number
|
|
1206
|
+
context: "Confirm the 3pm appointment on Tuesday.",
|
|
1207
|
+
metadata: { "crmId" => "lead_8812" } # up to 20 string pairs, echoed everywhere
|
|
1208
|
+
)
|
|
1209
|
+
puts call.id
|
|
1210
|
+
puts call.status # "ringing"
|
|
1211
|
+
puts call.handled_by # "agent"
|
|
1212
|
+
|
|
1213
|
+
# Follow it. Agent-handled calls include a transcript once fetched by id.
|
|
1214
|
+
call = client.calls.get(call.id)
|
|
1215
|
+
puts call.status # "ringing" -> "active" -> "completed" (or no_answer, busy, ...)
|
|
1216
|
+
puts call.hangup_class # why it ended, e.g. "agent_agent_hangup"
|
|
1217
|
+
puts call.credits_charged
|
|
1218
|
+
call.transcript&.each { |line| puts "#{line.speaker}: #{line.text}" }
|
|
1219
|
+
|
|
1220
|
+
# List (newest first; limit 1-100, default 50)
|
|
1221
|
+
page = client.calls.list(status: "completed", direction: "outbound", agent_id: call.agent_id, limit: 20)
|
|
1222
|
+
page.each { |c| puts "#{c.to} #{c.duration_secs}s #{c.credits_charged} credits" }
|
|
1223
|
+
puts page.total
|
|
1224
|
+
puts page.has_more?
|
|
1225
|
+
|
|
1226
|
+
# End a call. Ringing -> "cancelled", active -> "completed"; a call that
|
|
1227
|
+
# has already ended comes back unchanged.
|
|
1228
|
+
client.calls.hangup(call.id)
|
|
1229
|
+
|
|
1230
|
+
# Recording. The URL is signed and valid for five minutes; it is nil until
|
|
1231
|
+
# the recording is ready. Ogg/Opus, dual channel on agent calls.
|
|
1232
|
+
rec = client.calls.recording(call.id)
|
|
1233
|
+
if rec.ready?
|
|
1234
|
+
File.binwrite("#{call.id}.ogg", Net::HTTP.get(URI(rec.url)))
|
|
1235
|
+
end
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
Call errors map onto the usual classes: `Sendly::InsufficientCreditsError`
|
|
1239
|
+
when the balance cannot cover one minute at the agent rate;
|
|
1240
|
+
`Sendly::NotFoundError` for `voice_not_enabled`, `outbound_calls_not_enabled`,
|
|
1241
|
+
`agent_not_found`, `number_not_found` and `call_not_found`;
|
|
1242
|
+
`Sendly::ValidationError` for `invalid_number`, `destination_not_supported`,
|
|
1243
|
+
`agent_required`, `invalid_metadata` and `from_number_required`;
|
|
1244
|
+
`Sendly::RateLimitError` for `daily_call_limit`; `Sendly::APIError` with
|
|
1245
|
+
`status_code` 428 for `e911_required` (register an emergency address for the
|
|
1246
|
+
number), 409 for `agent_disabled`, `no_voice_number` and `lines_busy` (retry
|
|
1247
|
+
shortly), or 403 for `live_key_required` and a key missing the scope; and
|
|
1248
|
+
`Sendly::ServerError` (a sibling of `APIError`, not a subclass) for 503
|
|
1249
|
+
`voice_unavailable` / `agents_unavailable` and 500 `voice_internal_error`.
|
|
1250
|
+
The full list is `Sendly::Call::ERROR_CODES`; the hangup vocabulary is
|
|
1251
|
+
`Sendly::Call::HANGUP_CLASSES`.
|
|
1252
|
+
|
|
1253
|
+
`call.started`, `call.completed` and `call.recording.ready` webhooks carry
|
|
1254
|
+
the same object in snake_case (`handled_by`, `hangup_class`, `billing`,
|
|
1255
|
+
`metadata`, ...); `Sendly::Call.new(event.raw_object)` reads it.
|
|
1256
|
+
|
|
1104
1257
|
## Error Handling
|
|
1105
1258
|
|
|
1106
1259
|
```ruby
|
|
@@ -55,7 +55,7 @@ module Sendly
|
|
|
55
55
|
# @param key_id [String] API key ID
|
|
56
56
|
# @return [Sendly::ApiKey]
|
|
57
57
|
def api_key(key_id)
|
|
58
|
-
response = @client.get("/account/keys/#{key_id}")
|
|
58
|
+
response = @client.get("/account/keys/#{URI.encode_www_form_component(key_id)}")
|
|
59
59
|
ApiKey.new(response)
|
|
60
60
|
end
|
|
61
61
|
|
|
@@ -64,7 +64,7 @@ module Sendly
|
|
|
64
64
|
# @param key_id [String] API key ID
|
|
65
65
|
# @return [Hash] Usage statistics
|
|
66
66
|
def api_key_usage(key_id)
|
|
67
|
-
@client.get("/account/keys/#{key_id}/usage")
|
|
67
|
+
@client.get("/account/keys/#{URI.encode_www_form_component(key_id)}/usage")
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Create a new API key
|
|
@@ -96,7 +96,7 @@ module Sendly
|
|
|
96
96
|
body = {}
|
|
97
97
|
body[:reason] = reason if reason
|
|
98
98
|
|
|
99
|
-
@client.patch("/account/keys/#{key_id}/revoke", body)
|
|
99
|
+
@client.patch("/account/keys/#{URI.encode_www_form_component(key_id)}/revoke", body)
|
|
100
100
|
end
|
|
101
101
|
|
|
102
102
|
# Rotate an API key.
|
|
@@ -127,7 +127,7 @@ module Sendly
|
|
|
127
127
|
body = {}
|
|
128
128
|
body[:gracePeriodHours] = grace_period_hours unless grace_period_hours.nil?
|
|
129
129
|
|
|
130
|
-
@client.post("/account/keys/#{key_id}/rotate", body)
|
|
130
|
+
@client.post("/account/keys/#{URI.encode_www_form_component(key_id)}/rotate", body)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def transfer_credits(target_organization_id:, amount:)
|