sendly 3.38.0 → 3.40.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 +48 -0
- data/Gemfile.lock +1 -1
- data/README.md +191 -3
- data/lib/sendly/client.rb +12 -4
- data/lib/sendly/errors.rb +4 -2
- data/lib/sendly/rcs_resource.rb +944 -11
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/webhooks.rb +369 -78
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 91dd6ee6db6b506dea8d70b085fc6a069330e9ddca10b1de8e097b3db07889cd
|
|
4
|
+
data.tar.gz: 3114725bc7d45361b625ba7c7cfc88b6a6eee0672154b3b3daf46237cb475b53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8728f247769f3bbd8ad1713a86814eb419c944d64f06797ed94f858b543185d4c1baced35ba9aa910871b04a15e0348aece222a4e88de691e717df9b5542c01
|
|
7
|
+
data.tar.gz: ba2ad16077e383a0e37b7dee97ebada69cd3c1e815f6775afb677ab331a031da2f643666da3e95cc2b90e0dbff3478a6d35bc9a0364efd9b865c1ff5302f25fd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# sendly (Ruby)
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- **A webhook that is not a message is no longer presented as one.** `parse_event` built a `Sendly::WebhookMessageData` out of every `data.object`, whatever the event was. For an `rcs_*`, `whatsapp_*`, `call.*`, `brand.*`, `campaign.*`, `assignment.*`, `number.*`, `port*`, `contact*`, `conversation.*` or `draft.*` payload that dropped every field the event actually carried — `agent_id`, `stage`, `port_request_id`, `duration_secs` and the rest were unreachable — and filled the gaps with message fields that were never sent. `event.data` is now a `Sendly::WebhookObject`, a hash-like view of `data.object`: read a key with `[]` (String or Symbol), a reader method of the same name, or `to_h`. `event.message` is the message view and is `nil` for all of the above; `message.*` events are unchanged, and `event.data` is still the `WebhookMessageData` there.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# before — "" for every RCS event, and the agent was unreachable
|
|
11
|
+
event.data.from
|
|
12
|
+
# after
|
|
13
|
+
event.data[:agent_id] # => "bb22cc33-..."
|
|
14
|
+
event.message # => nil
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **Absent fields are `nil` instead of a plausible-looking default.** `WebhookMessageData` defaulted `segments` to `1`, `credits_used` to `0`, `direction` to `"outbound"`, `from` to `""` and `id` to `""`, none of which a handler could tell from a real value. They are now `nil` when the payload did not carry them, and `data.key?(:segments)` says which case you are in. `WebhookVerificationData` loses the same kind of defaults (`delivery_status` `"queued"`, `attempts` `0`, `max_attempts` `3`), though nothing could reach that class before this release.
|
|
18
|
+
|
|
19
|
+
- **JSON `null` survives as `nil`.** `from` and `to` on a `call.*` event are `null` for every in-app call; `from` used to arrive as `""`. Code branching on `from.empty?` should branch on `nil` now.
|
|
20
|
+
|
|
21
|
+
- **`event.data.to_h` returns `data.object` as it arrived.** It used to return a compacted subset of the typed message fields, which dropped `text`, `metadata`, `media_urls`, `message_format` and `organization_id`, renamed `message_id` to `id`, and emitted the invented `segments`/`credits_used` defaults. `event.to_h[:data]` is the same hash. For a current-shape payload the familiar keys are all still there.
|
|
22
|
+
|
|
23
|
+
- **`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
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- **`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`.
|
|
28
|
+
|
|
29
|
+
- **`Sendly::WebhookVerificationData` is reachable.** Nothing ever constructed it, and it read String keys while `parse_event` symbolizes names, so it could not have worked if anything had. `verification.*` events now build one, as `event.verification` and `event.data`, and every reader takes String or Symbol keys.
|
|
30
|
+
|
|
31
|
+
- **`Sendly::WebhookEvent` gains `#message?` and `#verification?`** for the two cases that have a typed view.
|
|
32
|
+
|
|
33
|
+
- **`Sendly::ValidationError#field_errors` is now populated.** It was always `nil` before, because the API path never passed it. It now carries the response body's `errors` array on any 400 or 422, on every resource rather than just RCS: `client.contacts.import` already returns one, for example. Each entry is a Hash. Code that treats a truthy `field_errors` as "this only happens for X" should be rechecked.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
- **RCS agent registration is self-serve from the SDK.** `client.rcs` gains `registration.get`, `dossier.get`, `brands.create` / `brands.update`, and `agents.create` / `get` / `update` / `set_test_devices` / `submit` / `request_launch`, mirroring the dashboard: draft the brand and agent, submit them for Sendly's review, invite test devices once the agent is in testing, then request launch. Reads need the `rcs:read` scope and writes `rcs:write`; test and live keys both work. Nested hashes (address, contact, basics, campaign, testing) accept snake_case or camelCase keys. Logo, hero and call-to-action media must be public `https://` URLs; assets cannot be uploaded over the API. New models: `Sendly::RcsRegistration` (with `CUSTOMER_STAGES`, `REVIEW_STATUSES` and `ERROR_CODES`), `Sendly::RcsDossier`, `Sendly::RcsBrand`, `Sendly::RcsAddress`, `Sendly::RcsContact`, `Sendly::RcsAgentRegistration`, `Sendly::RcsAgentBasics`, `Sendly::RcsAgentCampaign`, `Sendly::RcsCampaignInteraction`, `Sendly::RcsConsentSettings`, `Sendly::RcsOptInMethod`, `Sendly::RcsAgentTesting` and `Sendly::RcsTestDevice`. `Sendly::RcsAgent` (from `agents.list`) gains `stage`. Every route stays behind the RCS rollout: while it is off for your account these calls raise `Sendly::NotFoundError` with `rcs_not_enabled`.
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
dossier = client.rcs.dossier.get
|
|
40
|
+
brand = client.rcs.brands.create(**dossier.brand)
|
|
41
|
+
agent = client.rcs.agents.create(brand_id: brand.id, display_name: "Acme Coffee",
|
|
42
|
+
use_case: "MULTI_USE",
|
|
43
|
+
basics: { logo_url: "https://acme.example/rcs/logo.png" })
|
|
44
|
+
client.rcs.agents.submit(agent.id, idempotency_key: "rcs-submit-#{agent.id}")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **`client.patch` and `client.put` accept `idempotency_key:`.** Neither generates a key on its own (unchanged), but a key you pass is now sent, so the RCS `update` and `set_test_devices` calls can be replayed safely.
|
|
48
|
+
|
|
49
|
+
- **`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
|
+
|
|
3
51
|
## 3.38.0
|
|
4
52
|
|
|
5
53
|
### Minor Changes
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -280,8 +280,36 @@ puts result.explanation # what changed and why
|
|
|
280
280
|
puts result.model # model used (when available)
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
+
## Idempotency
|
|
284
|
+
|
|
285
|
+
Every POST carries an automatically generated `Idempotency-Key` header, held
|
|
286
|
+
across the client's own rate-limit retries, so a retry of a request that
|
|
287
|
+
already reached the API returns the original result instead of sending and
|
|
288
|
+
charging again. Pass your own key (1-255 printable ASCII characters) when the
|
|
289
|
+
guarantee needs to outlive the process, such as a job queue that re-runs after
|
|
290
|
+
a crash or your own retry loop; `idempotency_key:` is accepted on
|
|
291
|
+
`messages.send`, `send_group`, `schedule`, and `send_batch`.
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
message = client.messages.send(
|
|
295
|
+
to: "+15551234567",
|
|
296
|
+
text: "Your order has shipped!",
|
|
297
|
+
idempotency_key: "order-4821-shipped"
|
|
298
|
+
)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Repeating a request with the same key within 24 hours returns the original
|
|
302
|
+
response; `send_batch` sends no automatic key, because the API already
|
|
303
|
+
deduplicates identical batches by their contents. Note this client raises
|
|
304
|
+
`Sendly::TimeoutError` instead of retrying a timeout, so a timeout is exactly
|
|
305
|
+
when to retry with your own key.
|
|
306
|
+
|
|
307
|
+
Full details: https://sendly.live/docs/idempotency
|
|
308
|
+
|
|
283
309
|
## Webhooks
|
|
284
310
|
|
|
311
|
+
### Managing endpoints
|
|
312
|
+
|
|
285
313
|
```ruby
|
|
286
314
|
# Create a webhook endpoint
|
|
287
315
|
webhook = client.webhooks.create(
|
|
@@ -314,6 +342,67 @@ rotation = client.webhooks.rotate_secret("whk_xxx")
|
|
|
314
342
|
client.webhooks.delete("whk_xxx")
|
|
315
343
|
```
|
|
316
344
|
|
|
345
|
+
### Receiving events
|
|
346
|
+
|
|
347
|
+
`Sendly::Webhooks.parse_event` verifies the signature and returns a
|
|
348
|
+
`Sendly::WebhookEvent`. Pass the raw request body — not a re-serialized hash,
|
|
349
|
+
which would no longer match the signature.
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
event = Sendly::Webhooks.parse_event(
|
|
353
|
+
request.raw_post,
|
|
354
|
+
request.headers["X-Sendly-Signature"],
|
|
355
|
+
ENV.fetch("SENDLY_WEBHOOK_SECRET"),
|
|
356
|
+
timestamp: request.headers["X-Sendly-Timestamp"]
|
|
357
|
+
)
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
`event.raw_object` is `data.object` exactly as it arrived, for every event
|
|
361
|
+
type. `event.data` is a hash-like view of the same object: read a key with
|
|
362
|
+
`[]` (String or Symbol), a reader method of the same name, or `to_h`.
|
|
363
|
+
|
|
364
|
+
```ruby
|
|
365
|
+
case event.type
|
|
366
|
+
when Sendly::Webhooks::EVENT_MESSAGE_DELIVERED
|
|
367
|
+
# message.* events also get a typed message view
|
|
368
|
+
puts "#{event.message.id} -> #{event.message.to}"
|
|
369
|
+
when Sendly::Webhooks::EVENT_RCS_AGENT_LIVE
|
|
370
|
+
puts event.data[:agent_id]
|
|
371
|
+
puts event.data.stage
|
|
372
|
+
when Sendly::Webhooks::EVENT_CALL_COMPLETED
|
|
373
|
+
puts event.data[:duration_secs]
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Or read data.object as a type of your own. A Struct or Data class is filled
|
|
377
|
+
# from the members it declares and ignores the rest of the payload, so a field
|
|
378
|
+
# added to the event later cannot break the call.
|
|
379
|
+
AgentLive = Struct.new(:agent_id, :name, :stage)
|
|
380
|
+
agent = event.object_as(AgentLive)
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Two things the SDK will not do, because both make a handler act on data that
|
|
384
|
+
was never sent:
|
|
385
|
+
|
|
386
|
+
- **Nothing is invented.** A field the payload did not carry is `nil`, and
|
|
387
|
+
`event.data.key?(:segments)` is `false`. `segments`, `credits_used`,
|
|
388
|
+
`direction`, `to` and `from` are no longer defaulted to `1`, `0`,
|
|
389
|
+
`"outbound"` and `""`.
|
|
390
|
+
- **`nil` stays `nil`.** An in-app `call.*` event carries `from` and `to` as
|
|
391
|
+
JSON `null`; they come back as `nil`, not `""`.
|
|
392
|
+
|
|
393
|
+
`event.message` is the message view and is `nil` for every event that is not a
|
|
394
|
+
message — `rcs_*`, `whatsapp_*`, `call.*`, `brand.*`, `campaign.*`,
|
|
395
|
+
`assignment.*`, `number.*`, `port*`, `contact*`, `conversation.*` and
|
|
396
|
+
`draft.*`, whose payloads are not message-shaped. `verification.*` events get
|
|
397
|
+
`event.verification`, a `Sendly::WebhookVerificationData`. `event.data` is the
|
|
398
|
+
typed view where one exists and a plain `Sendly::WebhookObject` otherwise, so
|
|
399
|
+
reading `data.object` works the same way for all of them, including an event
|
|
400
|
+
type this SDK predates.
|
|
401
|
+
|
|
402
|
+
Note that `contact.auto_flagged` carries the contact under `id` and the message
|
|
403
|
+
that failed under `message_id`; read the message with
|
|
404
|
+
`event.data[:message_id]`.
|
|
405
|
+
|
|
317
406
|
## Account & Credits
|
|
318
407
|
|
|
319
408
|
```ruby
|
|
@@ -846,9 +935,108 @@ recipients.
|
|
|
846
935
|
|
|
847
936
|
The RCS channel is being rolled out gradually and is not yet generally
|
|
848
937
|
available; until it is enabled for your account the endpoints read as
|
|
849
|
-
absent and calls raise `Sendly::NotFoundError` (HTTP 404
|
|
850
|
-
|
|
851
|
-
|
|
938
|
+
absent and calls raise `Sendly::NotFoundError` (HTTP 404: `not_found` on
|
|
939
|
+
sends and listing, `rcs_not_enabled` on registration). RCS sends and
|
|
940
|
+
capability checks require a live API key.
|
|
941
|
+
|
|
942
|
+
### Registering an agent
|
|
943
|
+
|
|
944
|
+
Registration is self-serve, from the dashboard or the API. Draft the
|
|
945
|
+
brand (the business) and the agent (what recipients see), submit them for
|
|
946
|
+
Sendly's review, and once approved they go on to the carrier network for
|
|
947
|
+
brand verification and agent review. The agent then reaches invited test
|
|
948
|
+
devices only; when you have tested it, request launch and Sendly reviews
|
|
949
|
+
the campaign before sending the launch on to the carrier network. Watch
|
|
950
|
+
`customer_stage` (one of `Sendly::RcsRegistration::CUSTOMER_STAGES`) to
|
|
951
|
+
see where a registration is.
|
|
952
|
+
|
|
953
|
+
Registration reads need the `rcs:read` scope and writes `rcs:write`; test
|
|
954
|
+
and live keys both work, since drafting is not carrier-backed. Logo, hero
|
|
955
|
+
and call-to-action media must be public `https://` URLs: assets cannot be
|
|
956
|
+
uploaded over the API, only from the dashboard. Nested hashes (address,
|
|
957
|
+
contact, basics, campaign, testing) accept snake_case or camelCase keys.
|
|
958
|
+
Every write accepts `idempotency_key:`; `POST`s get an auto-generated key
|
|
959
|
+
as usual, while `PATCH` and `PUT` send one only when you pass it.
|
|
960
|
+
|
|
961
|
+
```ruby
|
|
962
|
+
# Where is the registration at?
|
|
963
|
+
reg = client.rcs.registration.get
|
|
964
|
+
puts reg.stage # "draft" until something is submitted
|
|
965
|
+
|
|
966
|
+
# Seed the brand from details Sendly already holds (your 10DLC brand or
|
|
967
|
+
# toll-free verification), then fill in the rest
|
|
968
|
+
dossier = client.rcs.dossier.get
|
|
969
|
+
brand = client.rcs.brands.create(**dossier.brand) # dossier.source: "tendlc", "verification" or "none"
|
|
970
|
+
client.rcs.brands.update(brand.id,
|
|
971
|
+
display_name: "Acme Coffee",
|
|
972
|
+
legal_entity_type: "LIMITED_LIABILITY_COMPANY",
|
|
973
|
+
contact: { first_name: "Sam", last_name: "Lee", email: "sam@acme.example",
|
|
974
|
+
phone_number: "+15551234567" }
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
# Draft the agent. Media must be public https URLs.
|
|
978
|
+
agent = client.rcs.agents.create(
|
|
979
|
+
brand_id: brand.id,
|
|
980
|
+
display_name: "Acme Coffee",
|
|
981
|
+
use_case: "MULTI_USE",
|
|
982
|
+
basics: {
|
|
983
|
+
description: "Order updates and offers from Acme Coffee",
|
|
984
|
+
logo_url: "https://acme.example/rcs/logo.png",
|
|
985
|
+
hero_url: "https://acme.example/rcs/hero.png",
|
|
986
|
+
brand_color: "#5B3A29",
|
|
987
|
+
privacy_policy_url: "https://acme.example/privacy",
|
|
988
|
+
terms_and_conditions_url: "https://acme.example/terms",
|
|
989
|
+
phone_number: { number: "+15551234567", label: "Support" }
|
|
990
|
+
}
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
# Submit for Sendly's review. Pass your own key if you may retry: a replay
|
|
994
|
+
# returns the original response without submitting again.
|
|
995
|
+
agent = client.rcs.agents.submit(agent.id, idempotency_key: "rcs-submit-#{agent.id}")
|
|
996
|
+
puts agent.review_status # "awaiting_review"
|
|
997
|
+
|
|
998
|
+
# Later: poll, and act on a review note
|
|
999
|
+
agent = client.rcs.agents.get(agent.id)
|
|
1000
|
+
if agent.changes_requested?
|
|
1001
|
+
puts agent.review_note
|
|
1002
|
+
client.rcs.agents.update(agent.id, basics: { hero_url: "https://acme.example/rcs/hero-v2.png" })
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
# Once the agent is in testing, invite devices, describe the campaign,
|
|
1006
|
+
# then request launch
|
|
1007
|
+
client.rcs.agents.set_test_devices(agent.id, devices: [
|
|
1008
|
+
{ phone_number: "+15557654321", label: "Sam's Pixel" }
|
|
1009
|
+
])
|
|
1010
|
+
client.rcs.agents.update(agent.id,
|
|
1011
|
+
campaign: {
|
|
1012
|
+
company_overview: "Specialty coffee roaster with three cafes in Austin",
|
|
1013
|
+
agent_overview: "Order updates, pickup alerts and support replies",
|
|
1014
|
+
interactions: [{ interaction_type: "TRANSACTIONAL_UPDATES",
|
|
1015
|
+
description: "Order and pickup status" }],
|
|
1016
|
+
message_examples: ["Your order #4821 has shipped!",
|
|
1017
|
+
"Your latte is ready for pickup at 5th St",
|
|
1018
|
+
"Reply HELP for support or STOP to opt out"],
|
|
1019
|
+
consent_settings: {
|
|
1020
|
+
opt_in_methods: [{ method_type: "WEBSITE", description: "Checkbox at checkout" }],
|
|
1021
|
+
call_to_action: "Get order updates by RCS",
|
|
1022
|
+
call_to_action_url: "https://acme.example/updates",
|
|
1023
|
+
double_opt_in: false,
|
|
1024
|
+
help_response: "Acme Coffee: reply STOP to opt out, or email help@acme.example",
|
|
1025
|
+
opt_out_response: "You're opted out of Acme Coffee updates."
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
)
|
|
1029
|
+
client.rcs.agents.request_launch(agent.id, test_url: "https://acme.example/rcs-test")
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
Registration errors map onto the usual classes: `Sendly::NotFoundError`
|
|
1033
|
+
for `rcs_not_enabled` and `rcs_not_found`; `Sendly::ValidationError` for
|
|
1034
|
+
`rcs_us_only` and `rcs_invalid_content`, whose `field_errors` is the API's
|
|
1035
|
+
list of `{ "path", "message" }` hashes; and `Sendly::APIError` with
|
|
1036
|
+
`status_code` 409 for `rcs_field_locked`, `rcs_brand_not_verified` and
|
|
1037
|
+
`rcs_launch_not_ready`, or 403 for a key missing the scope.
|
|
1038
|
+
|
|
1039
|
+
### Sending
|
|
852
1040
|
|
|
853
1041
|
```ruby
|
|
854
1042
|
# Discover your RCS agents ("testing" reaches invited test devices only;
|
data/lib/sendly/client.rb
CHANGED
|
@@ -222,20 +222,28 @@ module Sendly
|
|
|
222
222
|
|
|
223
223
|
# Make a PATCH request
|
|
224
224
|
#
|
|
225
|
+
# No Idempotency-Key is generated for a PATCH; pass +idempotency_key+
|
|
226
|
+
# to send one (1-255 printable ASCII characters).
|
|
227
|
+
#
|
|
225
228
|
# @param path [String] API path
|
|
226
229
|
# @param body [Hash] Request body
|
|
230
|
+
# @param idempotency_key [String, nil] Caller-supplied idempotency key (optional)
|
|
227
231
|
# @return [Hash] Response body
|
|
228
|
-
def patch(path, body = {})
|
|
229
|
-
request(:patch, path, body: body)
|
|
232
|
+
def patch(path, body = {}, idempotency_key: nil)
|
|
233
|
+
request(:patch, path, body: body, idempotency_key: idempotency_key)
|
|
230
234
|
end
|
|
231
235
|
|
|
232
236
|
# Make a PUT request
|
|
233
237
|
#
|
|
238
|
+
# No Idempotency-Key is generated for a PUT; pass +idempotency_key+
|
|
239
|
+
# to send one (1-255 printable ASCII characters).
|
|
240
|
+
#
|
|
234
241
|
# @param path [String] API path
|
|
235
242
|
# @param body [Hash] Request body
|
|
243
|
+
# @param idempotency_key [String, nil] Caller-supplied idempotency key (optional)
|
|
236
244
|
# @return [Hash] Response body
|
|
237
|
-
def put(path, body = {})
|
|
238
|
-
request(:put, path, body: body)
|
|
245
|
+
def put(path, body = {}, idempotency_key: nil)
|
|
246
|
+
request(:put, path, body: body, idempotency_key: idempotency_key)
|
|
239
247
|
end
|
|
240
248
|
|
|
241
249
|
# Make a DELETE request
|
data/lib/sendly/errors.rb
CHANGED
|
@@ -47,7 +47,9 @@ module Sendly
|
|
|
47
47
|
|
|
48
48
|
# Raised when the request contains invalid parameters
|
|
49
49
|
class ValidationError < Error
|
|
50
|
-
# @return [Hash, nil] Field-specific validation errors
|
|
50
|
+
# @return [Array<Hash>, Hash, nil] Field-specific validation errors. For
|
|
51
|
+
# API responses this is the +errors+ list from the body when present,
|
|
52
|
+
# e.g. +[{ "path" => "brand.ein", "message" => "Enter a 9-digit EIN" }]+
|
|
51
53
|
attr_reader :field_errors
|
|
52
54
|
|
|
53
55
|
def initialize(message = "Validation failed", field_errors: nil, details: nil)
|
|
@@ -101,7 +103,7 @@ module Sendly
|
|
|
101
103
|
|
|
102
104
|
case status
|
|
103
105
|
when 400, 422
|
|
104
|
-
ValidationError.new(message, details: details)
|
|
106
|
+
ValidationError.new(message, details: details, field_errors: body["errors"])
|
|
105
107
|
when 401
|
|
106
108
|
AuthenticationError.new(message)
|
|
107
109
|
when 402
|