sendly 4.2.0 → 4.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e04f0698b7520abcb9cd17816198b7e728ab2ce6d2ed1d564195887334dca82
4
- data.tar.gz: 9f26320067a3fe83a7035364cdf6682e03f9b93bf26996faf728cbfb9899065c
3
+ metadata.gz: 3aee146f9046fd6558c2ec3af67cf715a780538d5f054a212d1fd27c36c52a59
4
+ data.tar.gz: f88147e73d75d0a731e2c8fd65634ac79ce5a162908e03d7c778122bb841914f
5
5
  SHA512:
6
- metadata.gz: 2b3ec03a21b0eae75affd32ee962e3e33a4481f1b5b23e656d5633ebeef18f0302168343943a5b74f45176609566229dcccead196d939f73e43a7c19aa676c77
7
- data.tar.gz: f7d9d7f44fd1fe7aa99d53dcfbfdba8be8fb25e15d0ccdefda67488d7ea5552c04199f5a91287bab73b703192b1ae5860bb6c80fe23085bc2d4bb98d28f5ba2a
6
+ metadata.gz: bd2892234c3a4b5dbd721fe37a0b4ecc8693ea20440fcfa04d724ce9d6ad94fd6f42319aefa9148d3a05e801ac0f2db225096c1b23e7897e54625b406ef1115e
7
+ data.tar.gz: 89476582a5b3026c6a07d54fb563425df47328d1ba5f00f5da9e17731dfe3e229ceb96b699efa219f9c9fcaa1907e7bc3e4174b5c52b6b3ce6f004553daead90
data/CHANGELOG.md CHANGED
@@ -1,5 +1,85 @@
1
1
  # sendly (Ruby)
2
2
 
3
+ ## 4.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Options the API already supported, now in the Ruby SDK.**
8
+ - `account.create_api_key(name, type: "test", scopes: nil, expires_at: nil)`: `type:` is `"test"` (the default) or `"live"`, and `scopes:` sets the new key's scopes, which cannot go beyond the calling key's own. A live key needs a verified business and a credit balance; the API answers 403 `verification_required` or 402 `credits_required` otherwise. Any other type raises `ArgumentError` before a request is sent.
9
+ - `webhooks.deliveries(id, limit: nil, offset: nil, status: nil)`.
10
+ - `conversations.reply(id, media_urls: [...])` sends a reply with media and no text. `text:` is optional; a reply needs text, media or both.
11
+ - `enterprise.workspaces.inherit_verification(id, source_workspace_id:, purchase_new_number: nil)`: by default the workspace shares the source's verification and sending number, and nothing is bought. Passing `purchase_new_number: true` copies only the business details and buys the workspace its own toll-free number; the response then has `"newNumber" => true`.
12
+ - `enterprise.workspaces.create_key(id, name:, scopes: [...])`.
13
+ - `enterprise.workspaces.provision_bulk` accepts up to 100 workspaces, the API's limit. It refused more than 50 before sending anything.
14
+ - **Readers for fields the API sends.** `Account` gains `organization`, `organization_id`, `credits`, `verification`, `api_key`, `limits` and `raw`, and `name` is the workspace's name. `ApiKey` gains `scopes`, `is_active` and `revoked_at`. `Message` gains `media_urls` and `message_format`, and `to_h` includes them. `WebhookSecretRotation` gains `id`, `new_secret_version`, `grace_period_hours`, `rotated_at` and `raw`. `WebhookTestResult` gains `message`, `delivery_id` and `raw`. `Campaign` gains `completed?`.
15
+ - **Vocabularies gain values the API returns.** `Campaign::STATUSES` gains `completed`, what a sent campaign becomes. `CreditTransaction::TYPES` gains `transfer`, `admin_grant` and `admin_seed`. `Sendly::Call::ERROR_CODES` gains `from_number_not_supported`, the 400 `calls.create` returns when `from` is not a US or Canadian number.
16
+ - **`Sendly::CampaignSendResult`**, a `Sendly::Campaign`, is what `campaigns.send_campaign` returns (see below).
17
+ - **WhatsApp: add a number to a connected account by code, sender photos, conversational components and calling.**
18
+ - `whatsapp.signup.create(phone_number:, business_account_id: nil, verification_method: nil, display_name: nil)`: with `business_account_id:` (a WhatsApp Business Account already connected in the workspace, as on `WhatsAppSender#business_account_id`) the number is added without the Facebook step. The same $19 fee is charged and refunded if it fails, Meta sends the number a 6-digit code by `"sms"` (the default) or `"voice"`, and the session is `"verifying"` with no `connect_url`. With a non-empty `business_account_id:` a 5xx, such as 502 `whatsapp_verification_start_failed` (the session failed and its fee is refunded), is raised at once and never retried by the client (nor is a timeout or dropped connection), since each retry would start a new charged session; a Facebook signup is retried as before. `WhatsAppSignupSession` gains `phone_number`, `business_account_id`, `failure_reasons`, `updated_at`, `verification_method`, `verification_attempts_remaining` and `verifying?`, and `to_h` includes the ones the API sends (a Facebook signup's `to_h` is unchanged).
19
+ - `whatsapp.signup.create` raises `Sendly::ValidationError` for a `business_account_id:` that is present but empty or whitespace-only, before anything is sent, instead of starting a paid Facebook signup.
20
+ - `whatsapp.signup.verify(id, code:)` submits the code and returns the `WhatsAppSignup`, `"active"` on success. A 5xx, a timeout or a dropped connection from it is raised at once and never retried by the client: each submission uses one of the 5 attempts, and after a 502 `whatsapp_activation_pending` WhatsApp has already accepted the code, so check with `signup.get`. `whatsapp.signup.resend(id, verification_method: nil)` asks for a new code, at most every 30 seconds (429 `whatsapp_verification_resend_too_soon` is raised at once with `e.retry_after`).
21
+ - `WhatsAppSignup` gains `verification_method`, `verification_attempts_remaining`, `verification_code` (the code once Meta's text has arrived on the number, or nil) and `verifying?`. Both `STATUSES` gain `verifying`, and `failure_reasons` can be `verification_start_failed`, `verification_failed` or `verification_expired`.
22
+ - `whatsapp.senders.upload_profile_photo(phone_number, file, content_type: "image/jpeg", filename: "profile.jpg")` (a JPEG or PNG of at most 5 MB, sent as the multipart field `file`) and `whatsapp.senders.delete_profile_photo(phone_number)` return the `WhatsAppSenderProfile`. A 5xx, a timeout or a dropped connection from the upload is raised at once and never retried by the client.
23
+ - `whatsapp.senders.get_conversational_components(phone_number)` and `whatsapp.senders.update_conversational_components(phone_number, ice_breakers: nil, commands: nil)` return a `Sendly::WhatsAppConversationalComponents` (`phone_number`, `ice_breakers`, and `commands` as `Sendly::WhatsAppCommand`). Each list passed replaces the stored one and `[]` clears it; passing neither raises `Sendly::ValidationError` before anything is sent.
24
+ - `whatsapp.senders.set_calling(phone_number, enabled:)` switches WhatsApp calling on or off and returns a `Sendly::WhatsAppCallingSettings` (`phone_number`, `calling_enabled`, `outbound_calling_allowed`). Turning it on needs voice on for the number (409 `voice_not_enabled` otherwise). There is no API for placing WhatsApp calls.
25
+ - `WhatsAppSender` gains `business_account_id`, `business_name`, `calling_enabled` and `outbound_calling_allowed` (false for +1, +20, +84 and +234 numbers), with `calling_enabled?` and `outbound_calling_allowed?`.
26
+ - `Sendly::Call` gains `channel` (`"phone"`, `"whatsapp"` or `"browser"`, listed in `Sendly::Call::CHANNELS`; any other value comes through unchanged), and `to_h` includes it. The `call.started`, `call.completed` and `call.recording.ready` webhooks carry `channel` too, read as `event.data[:channel]`.
27
+ - WhatsApp sends document the new 409 `whatsapp_send_unconfirmed`, a `Sendly::APIError` with `status_code` 409: the outcome is unknown, the message was marked failed and refunded but may still be delivered, so check before sending it again (it could arrive twice). It is not retried automatically.
28
+ - `Client#post` and `Client#post_multipart` take `retry_server_errors:` (default `true`); `false` raises a 5xx at once instead of retrying it.
29
+
30
+ ```ruby
31
+ added = client.whatsapp.signup.create(phone_number: "+14155550124", business_account_id: "102290129340398")
32
+ client.whatsapp.signup.verify(added.id, code: "482913")
33
+ client.whatsapp.senders.set_calling("+14155550124", enabled: true)
34
+ ```
35
+
36
+ ```ruby
37
+ key = client.account.create_api_key("Production", type: "live", scopes: ["sms:send"])
38
+ deliveries = client.webhooks.deliveries("whk_xxx", status: "failed", limit: 20)
39
+ ```
40
+
41
+ ### Patch Changes
42
+
43
+ - **A retried 5xx keeps its idempotency key.** After a 5xx the client sent the retry with a new auto-generated key, a leftover from when the API recorded server errors under the key. The API has not recorded a 5xx since August, so the retry runs again under the same key either way. A new key only lost protection in one case: when the API had finished the request and recorded its answer but a gateway returned the 5xx, a retry with a new key sent the message again. The retry now carries the same key, so that case returns the recorded answer instead. This applies to uploads too.
44
+ - **Uploads whose filename has quotes, line breaks or non-ASCII characters no longer fail.** `media.upload` and `whatsapp.senders.upload_profile_photo` raised `Encoding::CompatibilityError` before sending when `filename:` had a non-ASCII character (such as `café.png`), and a `filename:` with a `"`, CR or LF broke the header of the upload's `file` part, so the API saw no file and answered 400. The filename is now sent as UTF-8, with `"`, CR and LF written as `%22`, `%0D` and `%0A`. Every other filename is sent exactly as before.
45
+ - **A 429 is waited out only when waiting can help, and never for more than a minute.** The client waited out and retried every 429 that carried a `retryAfter`, for as long as it said. It now waits only for an ordinary `rate_limit_exceeded`, the per-minute `provision_rate_limit` from enterprise workspace provisioning (120 a minute), a 429 with no `error`, or `too_many_concurrent_verifications` (too many first-time API key checks at once), and only when `retryAfter` is 60 seconds or less. Every other 429 is raised at once as a `Sendly::RateLimitError`, with the API's code in `e.response_body["error"]` and the wait in `e.retry_after`:
46
+ - `too_many_failed_key_attempts`: repeated wrong API keys from one address locked the account out for up to 5 minutes, and the call waited that out before failing. Fix the key, then wait, since until the lockout ends the right key can be refused too.
47
+ - `rate_limit_exceeded` from `verify.send` and `verify.resend` against the per-phone limit (5 codes per 10 minutes) or the daily limit (20 per day): the call blocked for up to 10 minutes or a day, then sent a code nobody was waiting for.
48
+ - the hourly `provision_rate_limit` (1,000 workspaces an hour): the call blocked for up to an hour. It is now raised with its `retry_after` so you can pace provisioning.
49
+
50
+ The wait is read from the `Retry-After` header first, then from the body's `retryAfter`, so a 429 from a proxy that carries only the header is waited out too, under the same idempotency key.
51
+
52
+ `enterprise.upload_verification_document` and `business_upgrade.start` and `resubmit` send their uploads once, as before, so any 429 there, `too_many_concurrent_verifications` included, is raised at once.
53
+ - **An ID of `""`, `"."` or `".."` no longer reaches a different endpoint.** Dots are left as they are when an ID is put in a URL, and a `.` or `..` path segment is then resolved on the way to the API, so `enterprise.workspaces.revoke_key("ws_1", "..")` sent `DELETE /enterprise/workspaces/ws_1/`, which deletes the workspace. Every request now raises `Sendly::ValidationError` before anything is sent when a segment of its path is empty, `.` or `..` (also written as `%2e`). IDs that only contain dots among other characters are sent as before.
54
+ - **Methods that failed on every call now work.** Each was checked against the handler it calls.
55
+ - `webhooks.deliveries` raised `TypeError`: it mapped over the `{deliveries, pagination}` envelope as if it were the list.
56
+ - `webhooks.rotate_secret` raised `NoMethodError` after the API had rotated the secret, so the new secret, shown only once, was lost. `new_secret` is read from the body the API sends.
57
+ - `account.create_api_key` never sent `type`, which the API used to require, so it failed with a 400, and it could never create a live key. It always sends `type` now.
58
+ - **`enterprise.upload_verification_document` and `business_upgrade.start` and `resubmit` no longer fail on non-ASCII text or on a filename with a quote or line break.** A filename that is not ASCII (such as `café.pdf`), or a form field that is not ASCII (such as a business name with an accent) sent with an `ein_doc`, raised `Encoding::CompatibilityError` before anything was sent. A `"`, carriage return or line feed in the filename broke the file part's header, so the API saw no file. Those three characters are now written as `%22`, `%0D` and `%0A`, as browsers do, and every upload that worked before is sent byte for byte as it was.
59
+ - **Values that were wrong on every call.**
60
+ - `account.get` left `id`, `email`, `name` and `created_at` nil: the API nests the user under `user` and the workspace under `organization`.
61
+ - `account.api_key(id)` reported `permissions` `[]` and `revoked?` false for every key: that endpoint sends `scopes`, `isActive` and `revokedAt`. `permissions` is read from `scopes`, and `revoked?` from `isActive` (then `revokedAt`) when `isRevoked` is absent.
62
+ - `messages.list` reported the size of the page as `total` and `has_more` false, so `messages.each` stopped after the first page. Both are read from the response's `pagination`, and `each` walks every page.
63
+ - `messages.each` and `conversations.each` advanced by `batch_size`, although a page holds at most 100 rows, so a `batch_size` over 100 skipped rows. They advance by the rows each page returned.
64
+ - `webhooks.test` left `status_code` and `response_time_ms` nil: the API nests them under `delivery`.
65
+ - `drafts.list` reported `has_more` false, `limit` 20 and `offset` 0 whatever was asked for, because the API sends only the total. `has_more` is worked out from the total, `offset` is the one requested, and `limit` is the page size the API used: 50 unless `limit:` is given, and at most 100.
66
+ - Campaigns: `recipient_count` was always 0, `started_at` nil and `sent?` never true, because the API sends `totalRecipients` and `sentAt` and calls a sent campaign `completed`. `text` and `contact_list_ids` also fall back to `messageText` and `targetListId`. `campaigns.preview` left `breakdown` nil: the API calls it `byCountry`.
67
+ - `campaigns.send_campaign` returned a `Campaign` with `id` and `name` nil and every count 0, because the API returns the batch the campaign went out in. The `Sendly::CampaignSendResult` it returns has the campaign's `id` and the batch's `batch_id`, `status`, `recipient_count` (the batch's total), `sent_count`, `failed_count`, `credits_used`, `credits_refunded`, `messages` and `raw`.
68
+ - `Message` dropped the attachments of an MMS, because it read neither `media_urls` nor `message_format`.
69
+ - **Docs that described behaviour the API does not have.** `webhooks.backfill` no longer says to dedupe on `data.object.id`: synthesized events reuse the original event id, so dedupe on `event.id`. `webhooks.rotate_secret` says deliveries are signed with the new secret as soon as it returns and the old secret is not kept, and `WebhookSecretRotation#webhook` and `#old_secret_expires_at`, which the API never sends, say they are always nil. `webhooks.test` documents that a failed test delivery raises `Sendly::ValidationError` with the API's message. The `send_batch`, `list_batches` and `preview_batch` examples read `total`, `id` and `hasSufficientCredits` instead of `queued`, `batchId` and `canSend`, which those endpoints never send. `Campaign::STATUSES` says `sent` and `paused` are never returned, `CreditTransaction::TYPES` says `adjustment` is never recorded, `ApiKey#last_four` says it is always nil, and `CampaignPreview` says `id` and `estimated_segments` are not returned.
70
+ - **WhatsApp docs match the API.** The YARD docs now say that the API never sends the `expired` signup status (`STATUSES` and `expired?` stay for compatibility), that `WhatsAppSignup#business_account_id` is set only while the signup is `verifying` or `active` (nil while `registering` and after a failure), that a closed window returns its past `expires_at` rather than nil, that a media send returns its caption as `text`, how in-window replies are priced (1 credit for the first 1,000 per sending number each month, then the destination's utility price), which roles and scopes connecting and editing need, the `waba_mismatch` and `registration_timeout` failure reasons, `template_header_variable_unsupported`, `whatsapp_unavailable` (503), `whatsapp_signup_limit_reached` (429), and `whatsapp_send_failed` as a final 422 or a 502 for a message that provably never reached the carrier, so it was not sent and is safe to send again. Nothing changes at runtime.
71
+ - **The README no longer says `Sendly::Call.new(event.raw_object)` reads a call webhook.** Webhook payloads are parsed with symbol keys, which `Sendly::Call` does not read, so every field came back nil. Read the payload from `event.data` instead, for example `event.data[:channel]`.
72
+ - **Errors the SDK already raised are documented.** `webhooks.create` documents the `ArgumentError` it raises for a URL that is not HTTPS or an empty event list, and `webhooks.update` the one it raises for an ID that does not start with `whk_` or a URL that is not HTTPS.
73
+
74
+ **Worth knowing before you upgrade.**
75
+
76
+ - `messages.list(...).total` counts every matching message instead of the page, and `messages.each` walks every page, as documented. On a large account `each` makes more requests than before.
77
+ - `campaigns.send_campaign` returns a `Sendly::CampaignSendResult`. It is still a `Sendly::Campaign`, but its `status` is the batch's (`processing`, `completed`, `partial_failure` or `failed`); read the campaign itself with `campaigns.get`.
78
+ - `Campaign#sent?` is true for a `completed` campaign.
79
+ - `enterprise.workspaces.create_key` without a name, or with an empty one, raises `ArgumentError` before sending, instead of the `Sendly::ValidationError` the API's 400 raised. Any other name is sent as before.
80
+ - `conversations.reply` with neither text nor media raises `Sendly::ValidationError` ("Provide 'text' or 'media_urls'"); leaving out `text:` used to raise `ArgumentError`.
81
+ - `enterprise.workspaces.inherit_verification` sends the source as `sourceWorkspaceId` instead of `source_workspace_id`. The API reads both.
82
+
3
83
  ## 4.2.0
4
84
 
5
85
  ### Minor Changes
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sendly (4.2.0)
4
+ sendly (4.3.0)
5
5
  faraday (~> 2.0)
6
6
  faraday-retry (~> 2.0)
7
7