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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +509 -47
- data/lib/misar_reach/client.rb +3 -0
- metadata +17 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 624ea9b3d925e725ebbe1c056953d8deedb56fb007a00a565155a87c86e435c5
|
|
4
|
+
data.tar.gz: cbfb9882fcbfd55cb16937bee0fffaea0712c294476df4dfd6b5e8d25af5d488
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
[](https://rubygems.org/gems/misarreach)
|
|
6
|
+
[](https://rubygems.org/gems/misarreach)
|
|
7
|
+
[](./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
|
-
|
|
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
|
|
36
|
+
gem install misarreach # ← no underscore
|
|
22
37
|
```
|
|
23
38
|
|
|
24
|
-
|
|
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
|
|
55
|
+
reach = MisarReach::Client.new(api_key: ENV.fetch("MISARREACH_API_KEY"))
|
|
56
|
+
```
|
|
30
57
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
reach.leads.stream_job(job["jobId"]) do |evt|
|
|
40
|
-
puts "#{evt[:event]}: #{evt[:data].inspect}"
|
|
41
|
-
end
|
|
62
|
+
---
|
|
42
63
|
|
|
43
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
69
|
-
| `MisarReach::UpgradeRequiredError` |
|
|
70
|
-
| `MisarReach::ApiError` | any other non-2xx
|
|
71
|
-
| `MisarReach::NetworkError` |
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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)
|
data/lib/misar_reach/client.rb
CHANGED
|
@@ -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:
|
|
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:
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
75
|
+
homepage: https://www.misarreach.com
|
|
71
76
|
licenses:
|
|
72
77
|
- MIT
|
|
73
78
|
metadata:
|
|
74
|
-
homepage_uri: https://misarreach.com
|
|
75
|
-
documentation_uri: https://
|
|
76
|
-
source_code_uri: https://github.com/Misar-AI/misarreach-sdks
|
|
77
|
-
changelog_uri: https://github.com/Misar-AI/misarreach-sdks/blob/main/
|
|
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:
|
|
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: []
|