misarreach 1.0.0 → 5.0.3

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +509 -47
  4. data/lib/misar_reach/client.rb +3 -0
  5. metadata +17 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9daf8c016c2f6ba763cd1e7a2a0a76646dda1c42abe3cb39db1350a530301178
4
- data.tar.gz: f9cf3101b87c4b6cf4d27d8ff93804778e45b635973de23ba09c428b5fdf258d
3
+ metadata.gz: 624ea9b3d925e725ebbe1c056953d8deedb56fb007a00a565155a87c86e435c5
4
+ data.tar.gz: cbfb9882fcbfd55cb16937bee0fffaea0712c294476df4dfd6b5e8d25af5d488
5
5
  SHA512:
6
- metadata.gz: a459a3ba78a8f4e82e95e6f639cf7cc289d83f15e58269cfcadf9f011e72e23122c4d037feaa12ba9380735222fcee6833ab07c2b8a37f5573179ffc435bbb9b
7
- data.tar.gz: 84e40b7e482340465c7261e77f0ca4d31861a236f5cb46d9f44b24ec15c49d8d3b2b6afd0f150d792cde0f978964fe79c04dc8536a0e5bf6fe7dbdf03d5645bc
6
+ metadata.gz: bd4e3959ddccb3a772c2d60d3e837eeaf0d2fdcae23c682c67f4e1189b42a6efe38e9256687102f45a182ed75c6990f56e8e57dc03e538da115452ded6f35326
7
+ data.tar.gz: f187d012f71c3376a0709008bbc3fb3f8328b9c35cf33c95323ad50adf517025c0872bb1203ec3b3f0cea9f5aa4d163b881c03cde1b222314d8fc3336fe6d443
data/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ All notable changes to this SDK are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [5.0.3] — 2026-08-19
8
+
9
+ Republished so that every SDK, including the tag-versioned ones, ships through the same automated release pipeline. No API changes.
10
+
11
+ ## [5.0.2] — 2026-08-19
12
+
13
+ Republished so that every SDK, including the tag-versioned ones, ships through the same automated release pipeline. No API changes.
14
+
15
+ ## [5.0.1] — 2026-08-19
16
+
17
+ Republished so that every SDK, including the tag-versioned ones, ships through the same automated release pipeline. No API changes.
18
+
19
+ ## [5.0.0] — 2026-08-19
20
+
21
+ One version across every SDK in every Misar product, replacing the drift between separately-numbered clients.
22
+
23
+ ### Documentation
24
+
25
+ - A campaign step is flat — `{channel, delay_hours, subject, body}`, ordered by its position in the array — and `conversations.reply` takes `message`. Earlier examples showed shapes the API rejects.
26
+ - Rewritten README: every resource and method is listed with the endpoint it calls, the examples are verified against the API contract, and package links are consistent across all SDKs.
27
+ - Manifest metadata filled in — homepage, repository, issue tracker, documentation and author.
28
+
29
+ ### Fixed
30
+
31
+ - An error response with an empty body was reported as success. A bare 401, or any response stripped by a proxy, came back as an empty result instead of raising, so callers could not tell "no results" from "not authorised".
32
+
7
33
  ## [1.0.0] — 2026-08-17
8
34
 
9
35
  First release.
data/README.md CHANGED
@@ -1,80 +1,542 @@
1
1
  # MisarReach Ruby SDK
2
2
 
3
- Official Ruby client for the [MisarReach](https://misarreach.com) developer API
4
- lead finder (23 sources), multi-channel outreach, CRM (deals + pipeline),
5
- autopilot, and the AI sales agent.
3
+ > Ruby client for MisarReach find leads, enrich and verify them, then work them through a CRM pipeline and multi-channel outreach.
6
4
 
7
- - Base URL: `https://api.misar.io/reach/api`
8
- - Auth: `Authorization: Bearer mrk_...` (reach-only developer key)
9
- - Pure `Net::HTTP` — no runtime dependencies
10
- - Automatic retries with backoff on `429/5xx` (honours `Retry-After`)
11
- - Typed error classes + Server-Sent Events streaming for lead-finder jobs
5
+ [![Gem Version](https://img.shields.io/gem/v/misarreach.svg)](https://rubygems.org/gems/misarreach)
6
+ [![Downloads](https://img.shields.io/gem/dt/misarreach.svg)](https://rubygems.org/gems/misarreach)
7
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
8
+
9
+ **17 resource groups · 94 operations**
10
+
11
+ Works on Ruby 2.7+ with **no runtime dependencies** — pure `Net::HTTP`, hashes in and
12
+ hashes out — so it drops into a Rails app, a Sidekiq worker or a bare script without
13
+ pulling anything else onto the load path. Talks to `https://api.misar.io/reach/api`.
14
+
15
+ ---
12
16
 
13
17
  ## Install
14
18
 
19
+ ```bash
20
+ gem install misarreach
21
+ ```
22
+
23
+ Or in a `Gemfile`:
24
+
15
25
  ```ruby
16
- # Gemfile
17
- gem "misarreach"
26
+ gem "misarreach", "~> 1.0"
18
27
  ```
19
28
 
29
+ Requires Ruby >= 2.7.
30
+
31
+ **The gem name and the require path differ.** The gem is published as
32
+ `misarreach` (no underscore), but the file it installs is `misar_reach.rb` and
33
+ the namespace is `MisarReach`. So the true pairing is:
34
+
20
35
  ```bash
21
- gem build misar_reach.gemspec
36
+ gem install misarreach # ← no underscore
22
37
  ```
23
38
 
24
- ## Usage
39
+ ```ruby
40
+ require "misar_reach" # ← underscore. `require "misarreach"` raises LoadError.
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Authentication
46
+
47
+ Create a key in **Settings → API keys** in the MisarReach app. Reach keys start
48
+ with `mrk_` and are validated against the reach-owned key table only, so a key
49
+ from another Misar product is rejected. It travels as
50
+ `Authorization: Bearer mrk_…`.
25
51
 
26
52
  ```ruby
27
53
  require "misar_reach"
28
54
 
29
- reach = MisarReach.new(api_key: ENV["MISARREACH_API_KEY"]) # mrk_...
55
+ reach = MisarReach::Client.new(api_key: ENV.fetch("MISARREACH_API_KEY"))
56
+ ```
30
57
 
31
- # Lead Finder
32
- job = reach.leads.search(query: "Series A SaaS founders", location: "US")
33
- status = reach.leads.job(job["jobId"])
34
- reach.leads.enrich(email: "jane@acme.com")
35
- reach.leads.verify(emails: ["jane@acme.com"])
36
- reach.leads.score(leadIds: ["l_1", "l_2"])
58
+ `api_key:` is a required **keyword** and must be non-empty — a positional
59
+ argument raises `ArgumentError`. `MisarReach.new(api_key: …)` is shorthand for
60
+ the same constructor. `base_url:`, `timeout:` and `max_retries:` are optional.
37
61
 
38
- # Live progress via Server-Sent Events
39
- reach.leads.stream_job(job["jobId"]) do |evt|
40
- puts "#{evt[:event]}: #{evt[:data].inspect}"
41
- end
62
+ ---
42
63
 
43
- # CRM
44
- deal = reach.deals.create(leadEmail: "cto@acme.com", leadName: "Acme CTO", value: 12_000)
45
- reach.pipeline.get
46
- reach.deals.suggestions(deal["id"])
64
+ ## Quick start
47
65
 
48
- # Channels
49
- reach.channels.status
50
- reach.channels.connect_whatsapp(phoneNumberId: "...", token: "...")
66
+ ```ruby
67
+ require "misar_reach"
68
+
69
+ reach = MisarReach::Client.new(api_key: ENV.fetch("MISARREACH_API_KEY"))
70
+
71
+ job = reach.leads.search(query: "CTOs at Series A fintech", useAI: true)
72
+ snapshot = reach.leads.job(job["jobId"])
51
73
 
52
- # Autopilot & sales agent
53
- reach.autopilot.start(campaignId: "camp_1")
54
- reach.sales_agent.process(conversationId: "conv_1")
74
+ puts snapshot["job"]["status"], snapshot["results"].length
55
75
  ```
76
+ ---
77
+
78
+ ## What's in the package
79
+
80
+ - `MisarReach::Client` — constructed with **keyword arguments**
81
+ (`api_key:`, `base_url:`, `timeout:`, `max_retries:`); `api_key` is required
82
+ and a positional key raises `ArgumentError`. `MisarReach.new(api_key: …)` is a
83
+ shorthand for the same thing.
84
+ - **Resource accessors** hang off the client as readers: `leads`, `deals`,
85
+ `pipeline`, `channels`, `autopilot`, `sales_agent`, `campaigns`, `contacts`,
86
+ `conversations`, `workspaces`, `settings`, `ads`, `campaign_templates`,
87
+ `deliverability`, `notifications`, `webhooks`, `plan`.
88
+ - **Resource classes**, all public constants under `MisarReach`:
89
+ `LeadsResource`, `DealsResource`, `PipelineResource`, `ChannelsResource`,
90
+ `AutopilotResource`, `SalesAgentResource`, `CampaignsResource`,
91
+ `ContactsResource`, `ConversationsResource`, `WorkspacesResource`,
92
+ `SettingsResource`, `AdsResource`, `CampaignTemplatesResource`,
93
+ `DeliverabilityResource`, `NotificationsResource`, `WebhooksResource`,
94
+ `PlanResource`. Note there is **no separate lead-finder resource**: the whole
95
+ `/lead-finder` surface lives on `client.leads`.
96
+ - **No models.** Every call returns the parsed JSON as a plain `Hash` with
97
+ **string keys**, matching the open-shape API contract — so it is
98
+ `deal["deal"]["id"]`, never `deal[:deal]`. A non-object JSON body is wrapped
99
+ as `{ "data" => … }`, and a `204` returns `{}`.
100
+ - **Errors** — `MisarReach::ApiError` and the typed subclasses below.
101
+ - **Transport** — pure `Net::HTTP`, **no runtime dependencies**. `Bearer` auth,
102
+ and automatic retries with exponential back-off on 429/500/502/503/504
103
+ (`max_retries:`, default 3, honouring `Retry-After`). A 402 plan refusal is
104
+ never retried. A *legacy* 429 carrying `upgrade: true` is retried like any
105
+ other 429 before the typed error surfaces, so budget for the back-off.
106
+ Streams are never retried: replaying one that failed mid-flight would
107
+ duplicate whatever the caller already consumed.
108
+ - **SSE streaming** for lead-finder job progress, via a block-yielding
109
+ `leads.stream_job`. The server sends *named* events — `progress`, `found`,
110
+ `complete`, `error`, `timeout` — plus a `: keepalive` comment every 20
111
+ seconds, which the parser discards. There is no `[DONE]` sentinel; the block
112
+ simply stops being called when the server closes the stream. A job that has
113
+ **already finished** is answered with a JSON snapshot rather than a stream,
114
+ and the SDK synthesises the terminal `complete` — or `error` when the job
115
+ failed — so a caller that assumed a stream does not hang on nothing.
116
+
117
+ ---
56
118
 
57
119
  ## Resources
58
120
 
59
- `leads` · `deals` · `pipeline` · `channels` · `autopilot` · `sales_agent` ·
60
- `campaigns` · `contacts` · `conversations` · `workspaces` · `settings` · `ads`
121
+ Every public method, grouped the way the client groups them.
122
+
123
+ ### Lead finder
124
+
125
+ 23-source search, enrichment, verification, scoring, lists and the SSE job stream.
126
+
127
+ | Method | Description |
128
+ |--------|-------------|
129
+ | `account` | Lead-finder credit balance and provider account state. |
130
+ | `config` | Which sources, filters and AI options this workspace may use. |
131
+ | `list` | Saved leads, paginated, newest first. |
132
+ | `search` | Start an async search across 23 sources — returns a `jobId`, not leads. |
133
+ | `discover` | Company and lead discovery by firmographic filters. |
134
+ | `enrich` | Enrich a saved lead from external data (spends credits). |
135
+ | `verify` | Verify email deliverability for one address or a batch (spends credits). |
136
+ | `score` | AI-score leads by job id or an explicit lead-id list. |
137
+ | `export` | Export saved leads as CSV or JSON. |
138
+ | `search_history` | Past searches with their result counts. |
139
+ | `recommendations` | Suggested next leads based on what you have already saved. |
140
+ | `preview_message` | Draft the AI outreach message for a lead without sending it. |
141
+ | `send_to_campaign` | Push selected leads into an existing campaign. |
142
+ | `add_to_segment` | Add selected leads to a contact segment. |
143
+ | `company` | Company profile for a domain. |
144
+ | `company_people` | People found at that company. |
145
+ | `lists` | Lead lists in the workspace. |
146
+ | `create_list` | Create a lead list. |
147
+ | `sync_list` | Sync a lead list to its connected destination. |
148
+ | `saved_searches` | Saved search definitions. |
149
+ | `create_saved_search` | Save a search for reuse. |
150
+ | `delete_saved_search` | Delete a saved search. |
151
+ | `scoring_rules` | Lead-scoring rules for this workspace. |
152
+ | `create_scoring_rule` | Create a lead-scoring rule. |
153
+ | `update_scoring_rule` | Update a lead-scoring rule. |
154
+ | `delete_scoring_rule` | Delete a lead-scoring rule. |
155
+ | `job` | Poll a search job for status and the results so far. |
156
+ | `job_feedback` | Rate a job's results so scoring improves. |
157
+ | `stream_job` | SSE progress for a running job — named events, no `[DONE]` sentinel. |
158
+
159
+ ### Deals
160
+
161
+ CRM deals, their activity log and AI next steps.
162
+
163
+ | Method | Description |
164
+ |--------|-------------|
165
+ | `list` | List CRM deals with a workspace revenue summary. |
166
+ | `create` | Create a CRM deal. |
167
+ | `update` | Update a deal. |
168
+ | `delete` | Delete a deal. |
169
+ | `activity` | Activity log for a deal. |
170
+ | `bulk` | Delete, move or tag many deals at once; tag writes are atomic server-side. |
171
+ | `suggestions` | AI next-step suggestions for a deal. |
172
+
173
+ ### Pipeline
174
+
175
+ The Kanban board and stage moves.
176
+
177
+ | Method | Description |
178
+ |--------|-------------|
179
+ | `get` | Kanban board of deals grouped by stage, with revenue totals. |
180
+ | `update` | Move a deal to another stage. |
181
+
182
+ ### Campaigns
183
+
184
+ Multi-step sequences and recipient dispatch.
185
+
186
+ | Method | Description |
187
+ |--------|-------------|
188
+ | `list` | List campaigns with step counts and send-status summaries. |
189
+ | `create` | Create a campaign with an optional step sequence. |
190
+ | `get` | Read one campaign. |
191
+ | `update` | Update a campaign. |
192
+ | `delete` | Delete a campaign. |
193
+ | `enqueue` | Queue recipients; check `warnings` for steps that can never deliver. |
194
+
195
+ ### Campaign templates
196
+
197
+ Reusable starting points.
198
+
199
+ | Method | Description |
200
+ |--------|-------------|
201
+ | `list` | Built-in templates plus your saved ones. |
202
+ | `create` | Save a template from steps or by copying a campaign. |
203
+
204
+ ### Contacts
205
+
206
+ The audience behind outreach.
207
+
208
+ | Method | Description |
209
+ |--------|-------------|
210
+ | `list` | List contacts. |
211
+ | `create` | Create a contact. |
212
+ | `get` | Read one contact. |
213
+ | `update` | Update a contact. |
214
+ | `delete` | Delete a contact. |
215
+ | `bulk` | Bulk delete / unsubscribe / resubscribe, max 500. |
216
+ | `import_contacts` | Import up to 5000 contacts; `subscribed` requires consent evidence. |
217
+ | `segments` | Segments defined in the workspace. |
218
+ | `stats` | Audience counts and subscription health. |
219
+
220
+ ### Conversations
221
+
222
+ The unified inbox.
223
+
224
+ | Method | Description |
225
+ |--------|-------------|
226
+ | `list` | Unified inbox — one row per contact, across every channel. |
227
+ | `get` | One contact's full timeline. |
228
+ | `reply` | Send a human reply into a thread. |
229
+
230
+ ### Channels
231
+
232
+ Connectors, consent links and per-channel health.
233
+
234
+ | Method | Description |
235
+ |--------|-------------|
236
+ | `status` | Connection state, credential health and period usage per channel. |
237
+ | `update_status` | Enable or disable a channel. |
238
+ | `opt_in_links` | Double opt-in links to collect SMS/WhatsApp consent. |
239
+ | `connect_sms` | Connect BYO Twilio SMS. |
240
+ | `connect_whatsapp` | Connect WhatsApp Business. |
241
+ | `connect_telegram` | Connect a Telegram bot. |
242
+ | `connect_twitter` | Connect X (Twitter) DMs. |
243
+ | `connect_instagram` | Connect Instagram DMs. |
244
+ | `connect_facebook` | Connect Facebook Messenger. |
245
+ | `connect_discord` | Connect a Discord bot. |
246
+ | `subscribe_push` | Register a browser web-push subscription. |
247
+ | `unsubscribe_push` | Unsubscribe this browser from web push. |
248
+
249
+ ### AI sales agent
250
+
251
+ Config, today's actions, and running the agent over a thread.
252
+
253
+ | Method | Description |
254
+ |--------|-------------|
255
+ | `config` | Agent config — offer, booking link, daily reply cap, confidence threshold. |
256
+ | `update_config` | Update the agent config. |
257
+ | `actions` | What the agent did today. |
258
+ | `conversations` | Conversations the agent is handling. |
259
+ | `process` | Run the agent over one conversation. |
260
+
261
+ ### Autopilot
262
+
263
+ Goal-driven runs.
264
+
265
+ | Method | Description |
266
+ |--------|-------------|
267
+ | `start` | Start an autopilot run from a stated goal. |
268
+ | `runs` | List autopilot runs, with the caller's plan limits. |
269
+ | `get` | Read one autopilot run. |
270
+ | `status` | Poll a run's status. |
271
+ | `set_status` | Pause, resume or stop a run. |
272
+
273
+ ### Deliverability
274
+
275
+ Sender reputation.
276
+
277
+ | Method | Description |
278
+ |--------|-------------|
279
+ | `get` | Sender health: bounce and complaint rates against *attempted* sends, plus a verdict. |
280
+
281
+ ### Notifications
282
+
283
+ The in-app bell.
284
+
285
+ | Method | Description |
286
+ |--------|-------------|
287
+ | `list` | In-app notifications, newest first, with the unread count. |
288
+ | `mark_read` | Mark notifications read — `{ids: [...]}` or `{all: true}`. |
289
+
290
+ ### Webhooks
291
+
292
+ Signed outbound endpoints.
293
+
294
+ | Method | Description |
295
+ |--------|-------------|
296
+ | `list` | Registered endpoints and their delivery health. |
297
+ | `create` | Register an endpoint; the signing secret is returned once only. |
298
+
299
+ ### Workspaces
300
+
301
+ Teams and membership.
302
+
303
+ | Method | Description |
304
+ |--------|-------------|
305
+ | `list` | Workspaces you belong to. |
306
+ | `create` | Create a workspace. |
307
+ | `members` | List members. |
308
+ | `add_member` | Invite a member. |
309
+ | `remove_member` | Remove a member. |
310
+
311
+ ### Plan
312
+
313
+ Caps and usage.
314
+
315
+ | Method | Description |
316
+ |--------|-------------|
317
+ | `get` | Plan, caps, per-feature usage and the upgrade offer. |
318
+
319
+ ### Settings
320
+
321
+ Compliance settings.
322
+
323
+ | Method | Description |
324
+ |--------|-------------|
325
+ | `sender_address` | The CAN-SPAM postal address on file. |
326
+ | `set_sender_address` | Set the CAN-SPAM postal address — sends are blocked until this exists. |
327
+
328
+ ### Ads
329
+
330
+ Paid-audience export.
331
+
332
+ | Method | Description |
333
+ |--------|-------------|
334
+ | `linkedin_company_audience` | Build a LinkedIn company audience from your leads. |
335
+
336
+ ---
337
+
338
+ ## Usage
339
+
340
+ ### Find leads
341
+
342
+ `leads.search` returns a job id, not leads — the search runs asynchronously.
343
+ `location` and `companySize` are **nested under `filters`**, not top level.
344
+
345
+ ```ruby
346
+ job = reach.leads.search(
347
+ query: "heads of ops at logistics startups",
348
+ useAI: true,
349
+ filters: { location: "Berlin", companySize: "11-50" },
350
+ )
351
+ job_id = job["jobId"]
352
+ ```
353
+
354
+ ### Stream job progress
355
+
356
+ `stream_job` requires a block and blocks the calling thread until the server
357
+ closes the stream.
358
+
359
+ ```ruby
360
+ reach.leads.stream_job(job_id) do |evt|
361
+ case evt[:event]
362
+ when "progress" then puts "working #{evt[:data]["message"]}"
363
+ when "found" then puts "hit #{evt[:data]["email"]}"
364
+ when "complete" then puts "done, #{evt[:data]["total_found"]} found"
365
+ when "error", "timeout" then warn "#{evt[:event]}: #{evt[:data]}"
366
+ end
367
+ end
368
+ ```
369
+
370
+ Each event is a `Hash` with a **symbol** `:event` / `:data` pair — the only
371
+ symbol-keyed structure in the SDK — while `:data` is the decoded JSON payload
372
+ with string keys.
373
+
374
+ ### List saved leads
375
+
376
+ ```ruby
377
+ page = reach.leads.list(page: 1, limit: 50)
378
+ puts page["total"]
379
+ ```
380
+
381
+ ### Create a CRM contact and a deal
382
+
383
+ ```ruby
384
+ require "time" # Time#iso8601 lives in the stdlib, not in core
385
+
386
+ reach.contacts.create(
387
+ email: "cto@acme.com",
388
+ firstName: "Dana",
389
+ status: "subscribed",
390
+ consent: { source: "signup form /pricing", timestamp: Time.now.utc.iso8601 },
391
+ )
392
+
393
+ created = reach.deals.create(
394
+ leadEmail: "cto@acme.com",
395
+ leadName: "Dana Reyes",
396
+ value: 12_000,
397
+ currency: "USD",
398
+ )
399
+ deal_id = created["deal"]["id"]
400
+ ```
401
+
402
+ ### Read and move the pipeline
403
+
404
+ The board is keyed by stage; every stage in `stages` is present, possibly empty.
405
+ Moving a deal is `pipeline.update`, not `pipeline.move`.
406
+
407
+ ```ruby
408
+ board = reach.pipeline.get
409
+ puts board["stages"].inspect, board["revenue"]["pipeline"]
410
+
411
+ reach.pipeline.update(dealId: deal_id, newStage: "meeting")
412
+ ```
413
+
414
+ ### Run a campaign
415
+
416
+ ```ruby
417
+ campaign = reach.campaigns.create(
418
+ name: "Q3 fintech outbound",
419
+ steps: [
420
+ # Steps are flat and ORDERED BY ARRAY INDEX — the server assigns
421
+ # step_order and builds the template itself. There is no nested `template`.
422
+ { channel: "email", subject: "Quick question", body: "Hi {{name}} …" },
423
+ { channel: "email", delay_hours: 72, body: "Following up …" },
424
+ ],
425
+ )
426
+
427
+ result = reach.campaigns.enqueue(
428
+ campaign["id"],
429
+ recipients: [{ email: "cto@acme.com", name: "Dana Reyes", company: "Acme" }],
430
+ )
431
+
432
+ # Check `warnings`: a step whose channel has no inbound path in this deployment
433
+ # can never deliver, and the server reports that here rather than silently
434
+ # dropping every recipient at dispatch time.
435
+ puts result["queued"], result["skipped"], result["warnings"].inspect
436
+ ```
437
+
438
+ ### Check the plan before an expensive run
439
+
440
+ ```ruby
441
+ plan = reach.plan.get
442
+ searches = plan["usage"]["lead_searches"]
443
+
444
+ if searches["remaining"].nil?
445
+ puts "unlimited lead searches on #{plan["plan"]["name"]}"
446
+ elsif searches["remaining"].zero?
447
+ puts "no searches left; upgrade at #{plan["upgrade"]["url"]}"
448
+ else
449
+ puts "#{searches["remaining"]} of #{searches["limit"]} searches left"
450
+ end
451
+ ```
452
+
453
+ ### Reply into a conversation
454
+
455
+ The reply body field is `message`, and the thread's own channel decides the
456
+ transport — you do not pick it.
457
+
458
+ ```ruby
459
+ inbox = reach.conversations.list(limit: 25)
460
+ puts inbox["conversations"].length
461
+
462
+ reach.conversations.reply(
463
+ "cto@acme.com",
464
+ message: "Happy to walk you through it — does Thursday work?",
465
+ )
466
+ ```
467
+
468
+ ---
61
469
 
62
470
  ## Errors
63
471
 
64
- | Class | When |
65
- |-------|------|
66
- | `MisarReach::AuthError` | 401 / 403 missing or wrong-scope key |
472
+ Every non-2xx raises `MisarReach::ApiError` or one of its subclasses — all of
473
+ which descend from `ApiError`, and so from `StandardError`. Each carries
474
+ `status` and `code`; the HTTP ones also carry the decoded response `body`
475
+ (`NetworkError` has no response, so its `body` is `nil`).
476
+
477
+ | Class | Raised for |
478
+ |-------|-----------|
479
+ | `MisarReach::AuthError` | 401 and 403 — missing, invalid or out-of-scope `mrk_` key |
67
480
  | `MisarReach::NotFoundError` | 404 |
68
- | `MisarReach::RateLimitError` | 429 (`retry_after`, `balance`, `free_remaining`) |
69
- | `MisarReach::UpgradeRequiredError` | 429 with `upgrade: true` |
70
- | `MisarReach::ApiError` | any other non-2xx (`status`, `code`, `body`) |
71
- | `MisarReach::NetworkError` | connectivity failure |
481
+ | `MisarReach::RateLimitError` | 429 rate limiting; adds `retry_after`, `balance`, `free_remaining` |
482
+ | `MisarReach::UpgradeRequiredError` | a plan cap was hit — see below |
483
+ | `MisarReach::ApiError` | any other non-2xx; the base class of all of the above |
484
+ | `MisarReach::NetworkError` | transport failure or retries exhausted; `status` is `0` |
72
485
 
73
- ## Test
486
+ The same mapping applies to the SSE stream, so a plan refusal on `stream_job`
487
+ arrives as the same typed error rather than a bare exception.
74
488
 
75
- ```bash
76
- bundle install
77
- bundle exec rspec
489
+ ### The 402 upgrade case
490
+
491
+ A counted plan cap answers **402** with `upgrade: true` — not 403, and not 429.
492
+ Retrying cannot help until the cap resets or the plan changes, so the retry loop
493
+ skips it and the error surfaces immediately:
494
+
495
+ ```ruby
496
+ begin
497
+ reach.leads.search(query: "…")
498
+ rescue MisarReach::UpgradeRequiredError => e
499
+ # e.g. feature "lead_searches", current 50 of limit 50
500
+ warn "#{e.feature}: #{e.current}/#{e.limit}"
501
+ warn "upgrade at #{e.upgrade_url}" # resolved to an absolute URL
502
+ end
78
503
  ```
79
504
 
80
- See https://docs.misar.io/reach/api for the full reference.
505
+ 429 is still accepted as an upgrade refusal when `upgrade: true` is present, for
506
+ older deployments — though `e.status` then reports `402`, the canonical status
507
+ for the condition, rather than the wire status. This is distinct from the 503
508
+ `retry: true` the server sends when it could not *check* the quota — that one is
509
+ retried, so "we don't know" is never mistaken for "you're over your limit".
510
+
511
+ ### Reading `remaining`
512
+
513
+ In `plan.get`, a `usage` entry's `limit` is `nil` when the plan is unlimited for
514
+ that counter, and `remaining` is `nil` alongside it — deliberately **not** `0`,
515
+ which would read as exhausted. Test with `.nil?` before comparing, as the
516
+ example above does; `searches["remaining"].zero?` on a `nil` raises
517
+ `NoMethodError`.
518
+
519
+ ---
520
+
521
+ ## Compliance
522
+
523
+ Outreach is not uniformly permitted. Email is the only cold-capable channel; SMS
524
+ and WhatsApp require a consent record, and several social channels may only
525
+ reply inside a window the recipient opened. The API enforces this server-side
526
+ and will refuse a send rather than let you breach TCPA, CASL or GDPR — a refusal
527
+ is the SDK working correctly, not an error to retry around. Sends are also
528
+ blocked until a CAN-SPAM sender postal address is set via
529
+ `settings.set_sender_address`.
530
+
531
+ ---
532
+
533
+ ## Links
534
+
535
+ - **Website** — https://www.misarreach.com
536
+ - **App** — https://reach.misar.io
537
+ - **Parent** — https://misar.io
538
+ - **Documentation** — https://docs.misar.io/reach
539
+ - **Source** — https://github.com/Misar-AI/misarreach-sdks
540
+ - **RubyGems** — https://rubygems.org/gems/misarreach
541
+
542
+ MIT © [Misar AI](https://misar.io)
@@ -653,6 +653,9 @@ module MisarReach
653
653
  end
654
654
 
655
655
  def parse_response(resp, status)
656
+ # A refusal with an empty body — a bare 401/404, or a 5xx from a proxy —
657
+ # must still raise. Falling into the 204 guard below reported it as success.
658
+ raise_for_status(status, JSON.generate("error" => resp.message.to_s)) if status >= 400 && resp.body.to_s.empty?
656
659
  return {} if status == 204 || resp.body.nil? || resp.body.empty?
657
660
 
658
661
  decoded = begin
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: misarreach
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 5.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Misar AI
@@ -52,9 +52,14 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.22'
55
- description: Full-featured Ruby SDK for the MisarReach developer API (api.misar.io/reach/api).
56
- Covers all 12 resource groups and 84 operations, including the lead-finder SSE job
57
- stream. Pure Net::HTTP, no runtime dependencies.
55
+ description: 'Ruby client for the MisarReach outreach and lead-generation API (api.misar.io/reach/api).
56
+ 17 resource groups and 94 methods one for every operation in the published OpenAPI
57
+ spec: an asynchronous lead finder across 23 sources with Server-Sent Events job
58
+ streaming, CRM contacts, deals and a Kanban pipeline, multi-step campaigns over
59
+ email, SMS, WhatsApp, web push and social DMs, an AI sales agent, autopilot, deliverability,
60
+ and plan and usage reporting. Pure Net::HTTP with no runtime dependencies, typed
61
+ errors, and retries with exponential back-off. Installs as `misarreach`; requires
62
+ as `misar_reach`.'
58
63
  email:
59
64
  - hello@misar.io
60
65
  executables: []
@@ -67,15 +72,16 @@ files:
67
72
  - lib/misar_reach.rb
68
73
  - lib/misar_reach/client.rb
69
74
  - lib/misar_reach/errors.rb
70
- homepage: https://misarreach.com/docs
75
+ homepage: https://www.misarreach.com
71
76
  licenses:
72
77
  - MIT
73
78
  metadata:
74
- homepage_uri: https://misarreach.com/docs
75
- documentation_uri: https://misarreach.com/docs/sdks/ruby
76
- source_code_uri: https://github.com/Misar-AI/misarreach-sdks
77
- changelog_uri: https://github.com/Misar-AI/misarreach-sdks/blob/main/sdks/ruby/CHANGELOG.md
79
+ homepage_uri: https://www.misarreach.com
80
+ documentation_uri: https://docs.misar.io/reach
81
+ source_code_uri: https://github.com/Misar-AI/misarreach-sdks/tree/main/ruby
82
+ changelog_uri: https://github.com/Misar-AI/misarreach-sdks/blob/main/ruby/CHANGELOG.md
78
83
  bug_tracker_uri: https://github.com/Misar-AI/misarreach-sdks/issues
84
+ rubygems_mfa_required: 'true'
79
85
  post_install_message:
80
86
  rdoc_options: []
81
87
  require_paths:
@@ -94,5 +100,6 @@ requirements: []
94
100
  rubygems_version: 3.5.22
95
101
  signing_key:
96
102
  specification_version: 4
97
- summary: Official Ruby SDK for MisarReach lead finder, outreach channels, CRM, autopilot
103
+ summary: 'Ruby client for MisarReach: async lead finder with SSE streaming, CRM pipeline,
104
+ multi-channel campaigns, AI sales agent, autopilot'
98
105
  test_files: []