misarseo 1.0.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 +7 -0
- data/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.md +257 -0
- data/lib/misarseo/client.rb +29 -0
- data/lib/misarseo/errors.rb +100 -0
- data/lib/misarseo/generated/account.rb +30 -0
- data/lib/misarseo/generated/activity.rb +22 -0
- data/lib/misarseo/generated/ahrefs.rb +35 -0
- data/lib/misarseo/generated/ai_search.rb +87 -0
- data/lib/misarseo/generated/backlinks.rb +34 -0
- data/lib/misarseo/generated/baidu.rb +35 -0
- data/lib/misarseo/generated/bing.rb +95 -0
- data/lib/misarseo/generated/brave_search.rb +20 -0
- data/lib/misarseo/generated/competitor_monitor.rb +37 -0
- data/lib/misarseo/generated/competitors.rb +20 -0
- data/lib/misarseo/generated/content.rb +25 -0
- data/lib/misarseo/generated/crawl.rb +140 -0
- data/lib/misarseo/generated/dashboard.rb +20 -0
- data/lib/misarseo/generated/direct_vendor_ai.rb +20 -0
- data/lib/misarseo/generated/disavow.rb +35 -0
- data/lib/misarseo/generated/domain.rb +37 -0
- data/lib/misarseo/generated/ga4.rb +50 -0
- data/lib/misarseo/generated/google_business_profile.rb +77 -0
- data/lib/misarseo/generated/gsc.rb +70 -0
- data/lib/misarseo/generated/index_now.rb +20 -0
- data/lib/misarseo/generated/keywords.rb +71 -0
- data/lib/misarseo/generated/link_building.rb +60 -0
- data/lib/misarseo/generated/local.rb +30 -0
- data/lib/misarseo/generated/local_pack_rank.rb +40 -0
- data/lib/misarseo/generated/local_rank_grid.rb +40 -0
- data/lib/misarseo/generated/log_analyzer.rb +42 -0
- data/lib/misarseo/generated/mentions.rb +25 -0
- data/lib/misarseo/generated/patches.rb +55 -0
- data/lib/misarseo/generated/portfolios.rb +50 -0
- data/lib/misarseo/generated/projects.rb +40 -0
- data/lib/misarseo/generated/rank_tracking.rb +144 -0
- data/lib/misarseo/generated/report_builder.rb +67 -0
- data/lib/misarseo/generated/resources.rb +85 -0
- data/lib/misarseo/generated/team.rb +20 -0
- data/lib/misarseo/generated/usage.rb +20 -0
- data/lib/misarseo/generated/web_analytics.rb +35 -0
- data/lib/misarseo/generated/yandex.rb +45 -0
- data/lib/misarseo/pagination.rb +38 -0
- data/lib/misarseo/transport.rb +192 -0
- data/lib/misarseo/version.rb +5 -0
- data/lib/misarseo.rb +13 -0
- data/manifest.json +2850 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 31f9863c4b7f4723163ddbca2c716dd97cc8b9ede908e30569397d492feb5453
|
|
4
|
+
data.tar.gz: f33667cb53a6b0b683d1c412714c1c124cf33c9c507be0635c3b3e9243002f9a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6ccc2a49b94d5bb5fc479e7ff5d9f731af485c805dad52c54e53d2a92d8e0063521646be5e241643efef18946b1913976afc8e8d8289ae1c70d707bd6d40b23b
|
|
7
|
+
data.tar.gz: 5a77b689e14b3a3b214544cfd6cfa7539f6a028b444700358a4efbce44fd11dbcd6ea3485fe37a2d653d31058473e8add6859096226f8c47b3abe3657ffe1df7
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this SDK are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] — 2026-10-16
|
|
8
|
+
|
|
9
|
+
First release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The full MisarSEO API: 221 methods in 36 resource groups, generated from
|
|
14
|
+
`manifest.json` (itself generated from the API's OpenAPI route registry),
|
|
15
|
+
plus the crawl progress Server-Sent Events stream.
|
|
16
|
+
- Bearer API-key authentication (`mseo_…`), with the key read from
|
|
17
|
+
`MISARSEO_API_KEY` where the platform has an environment.
|
|
18
|
+
- A plan-limit error carrying the server's upgrade offer (`code:
|
|
19
|
+
"plan_limit_exceeded"`; 402 for a locked feature or a point-in-time cap,
|
|
20
|
+
429 with `Retry-After` for a spent quota), detected by code and never
|
|
21
|
+
retried.
|
|
22
|
+
- The shared retry policy: 429 for every method, 502/503/504 and network
|
|
23
|
+
failures for GET only, exponential backoff with jitter honouring
|
|
24
|
+
`Retry-After` up to 60 s.
|
|
25
|
+
- Offset and cursor pagination helpers.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Misar.AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# MisarSEO Ruby SDK
|
|
2
|
+
|
|
3
|
+
> Keyword research, site audits, rank tracking and AI search visibility from Ruby.
|
|
4
|
+
|
|
5
|
+
[](https://rubygems.org/gems/misarseo)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
**36 resource groups · 221 methods · SSE crawl streaming · standard library only**
|
|
9
|
+
|
|
10
|
+
MisarSEO is one API for the SEO work around a site: keyword and competitor research, crawls and audits, rank tracking (organic, local and AI Overviews), backlinks, AI-search brand visibility, and the data Google Search Console, GA4, Bing, Yandex and Baidu hold about it. Ruby 2.6+. Every method returns the parsed JSON (Hash/Array).
|
|
11
|
+
|
|
12
|
+
Package: `misarseo` on RubyGems. Source: [github.com/Misar-AI/misarseo-sdks](https://github.com/Misar-AI/misarseo-sdks/tree/main/ruby).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
### Bundler
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem "misarseo"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### gem
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gem install misarseo
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Authentication
|
|
33
|
+
|
|
34
|
+
Create an API key as described at https://seo.misar.io/ai. It starts with `mseo_` and is sent as `Authorization: Bearer mseo_…` — the only credential the SDK uses.
|
|
35
|
+
|
|
36
|
+
Every call is decided server-side against the plan of the organization that owns the key; there is no client-side limit checking. A plan refusal carries `code: "plan_limit_exceeded"` and an upgrade offer, and is never retried (see [Plan limits](#plan-limits)).
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "misarseo"
|
|
40
|
+
|
|
41
|
+
seo = MisarSEO::Client.new(api_key: ENV["MISARSEO_API_KEY"])
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Resources
|
|
47
|
+
|
|
48
|
+
Every group the client exposes, and every method on it — generated from [`manifest.json`](./manifest.json), the same file every MisarSEO SDK is built from. Positional arguments: path parameters then required query parameters, then the JSON body (when the endpoint takes one); `query:` takes optional query parameters.
|
|
49
|
+
|
|
50
|
+
### Workspace
|
|
51
|
+
|
|
52
|
+
| Resource | Methods | What it covers |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `seo.projects` | `create`, `delete`, `get`, `list`, `update` | Projects in the organization: list, create, rename, archive. |
|
|
55
|
+
| `seo.team` | `list_members` | Members, pending invitations and the caller's role (read-only). |
|
|
56
|
+
| `seo.account` | `delete`, `delete_via_post`, `export` | Data-subject rights: full JSON export and account erasure. |
|
|
57
|
+
| `seo.activity` | `list` | The organization's activity log, newest first (cursor paged). |
|
|
58
|
+
| `seo.usage` | `get` | Plan tier, credits and quota usage for the organization. |
|
|
59
|
+
| `seo.dashboard` | `get` | Cross-project overview — one card per active project. |
|
|
60
|
+
| `seo.portfolios` | `add_projects`, `create`, `delete`, `get`, `list`, `remove_projects`, `rename` | Group projects and read their aggregate summary. |
|
|
61
|
+
| `seo.report_builder` | `create_definition`, `delete_definition`, `delete_schedule`, `get_definition`, `get_schedule`, `list_definitions`, `rename_definition`, `render_definition`, `set_widgets`, `upsert_schedule` | User-composed report definitions, widgets, rendering and schedules. |
|
|
62
|
+
|
|
63
|
+
### Keyword and competitive research
|
|
64
|
+
|
|
65
|
+
| Resource | Methods | What it covers |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `seo.keywords` | `classify_intent`, `gap`, `list_saved`, `list_share_of_voice`, `potential`, `research`, `save`, `serp_overview`, `strategy`, `topic_research` | Keyword research, saved keywords, intent, SERP overview, gap, topic research, strategy, share of voice. |
|
|
68
|
+
| `seo.domain` | `authority_history`, `batch_overview`, `capture_authority_snapshot`, `overview` | Domain overview, batch overview and authority history. |
|
|
69
|
+
| `seo.backlinks` | `anchors`, `intersect`, `overview` | Backlink profile, anchors (offset paged) and link intersect. |
|
|
70
|
+
| `seo.competitors` | `find` | SERP competitor discovery. |
|
|
71
|
+
| `seo.content` | `optimizer`, `page_metrics` | On-page content optimizer and page metrics. |
|
|
72
|
+
| `seo.mentions` | `media_monitoring`, `unlinked_mentions` | Unlinked brand mentions and media monitoring. |
|
|
73
|
+
| `seo.competitor_monitor` | `add_target`, `list_changes`, `list_targets`, `remove_target` | Competitor page change detection. |
|
|
74
|
+
| `seo.link_building` | `add`, `add_note`, `export_csv`, `import_csv`, `list`, `list_activity`, `remove`, `update`, `update_status` | Prospect list, outreach status, activity log, CSV import/export. |
|
|
75
|
+
| `seo.disavow` | `add`, `export`, `list`, `remove` | Disavow entries and Google's disavow-file export. |
|
|
76
|
+
|
|
77
|
+
### Site audit
|
|
78
|
+
|
|
79
|
+
| Resource | Methods | What it covers |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `seo.crawl` | `clear_issue_status`, `comparison`, `core_web_vitals`, `create_audit_segment`, `create_custom_issue_rule`, `create_issue_mute_rule`, `create_issue_severity_override`, `delete_audit_segment`, `delete_custom_issue_rule`, `delete_issue_mute_rule`, `delete_issue_severity_override`, `https_report`, `internal_links`, `list_audit_segments`, `list_custom_issue_rules`, `list_issue_mute_rules`, `list_issue_severity_overrides`, `list_issue_statuses`, `segment`, `set_issue_status`, `start`, `status`, `structured_data_report`, `stream_events` (SSE) | Start a crawl, read status and comparisons, Core Web Vitals, triage rules, audit reports, and the live SSE progress stream. |
|
|
82
|
+
| `seo.patches` | `batch_draft`, `disable_auto_publish`, `draft`, `enable_auto_publish`, `list`, `list_auto_publish_rules`, `publish`, `revert` | AI-drafted title/meta/canonical patches, publish/revert and auto-publish rules. |
|
|
83
|
+
| `seo.log_analyzer` | `daily_summary`, `get_upload`, `list_uploads`, `top_paths`, `upload` | Server-log uploads and bot-traffic reports. |
|
|
84
|
+
| `seo.web_analytics` | `devices`, `sources`, `summary`, `top_pages` | First-party, cookieless traffic and AI-referral reports. |
|
|
85
|
+
|
|
86
|
+
### Rank tracking
|
|
87
|
+
|
|
88
|
+
| Resource | Methods | What it covers |
|
|
89
|
+
| --- | --- | --- |
|
|
90
|
+
| `seo.rank_tracking` | `add_annotation`, `add_competitor`, `add_keywords`, `create`, `delete`, `get`, `get_ai_overview_trend`, `get_config_trend`, `get_keyword_history`, `get_latest_run`, `get_rank_movers`, `get_volatility_index`, `list_annotations`, `list_cannibalisation`, `list_competitors`, `list_configs`, `list_keyword_tags`, `remove_annotation`, `remove_competitor`, `remove_keywords`, `set_keyword_target_url`, `update`, `update_keyword_tag` | Trackers, keywords, tags, runs, trends, movers, annotations, cannibalisation, competitors. |
|
|
91
|
+
| `seo.local_rank_grid` | `create_config`, `delete_config`, `get_latest_run`, `list_configs`, `run_check` | Geo-grid organic rank tracking. |
|
|
92
|
+
| `seo.local_pack_rank` | `create_config`, `delete_config`, `get_latest_check`, `list_configs`, `run_check` | Local-pack position of a named business. |
|
|
93
|
+
| `seo.brave_search` | `rank_check` | Rank checks against Brave Search's independent index. |
|
|
94
|
+
|
|
95
|
+
### Local
|
|
96
|
+
|
|
97
|
+
| Resource | Methods | What it covers |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `seo.local` | `business_listings`, `nap_audit`, `questions_answers` | Business listings, Q&A and NAP audit. |
|
|
100
|
+
| `seo.google_business_profile` | `available_locations`, `create_post`, `delete_review_reply`, `get_connection`, `get_optimization`, `get_performance`, `get_profile`, `link`, `list_posts`, `list_reviews`, `reply_to_review`, `unlink` | Business Profile connection, profile, performance, reviews, posts, optimization. |
|
|
101
|
+
|
|
102
|
+
### AI search visibility
|
|
103
|
+
|
|
104
|
+
| Resource | Methods | What it covers |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| `seo.ai_search` | `brand_lookup`, `brand_lookup_status`, `brand_scan_trend`, `citations`, `competitor_positioning`, `delete_brand_monitor`, `delete_tracked_prompt`, `list_brand_monitors`, `list_tracked_prompts`, `prompt_explorer`, `prompt_research`, `tracked_prompt_trend`, `upsert_brand_monitor`, `upsert_tracked_prompt` | Brand lookup, prompt explorer, brand monitors, tracked prompts, competitor positioning, prompt research, citations. |
|
|
107
|
+
| `seo.direct_vendor_ai` | `answer` | Answers from vendors' own APIs (You.com, DeepSeek, xAI, Meta AI, MiniMax). |
|
|
108
|
+
|
|
109
|
+
### Search engines and analytics
|
|
110
|
+
|
|
111
|
+
| Resource | Methods | What it covers |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| `seo.gsc` | `anonymous_queries`, `archive`, `cannibalisation`, `connect`, `content_decay`, `coverage`, `ctr_by_position`, `hourly`, `inspect_urls`, `performance`, `sitemaps` | Search Console performance, archive, coverage, URL inspection, sitemaps, reports. |
|
|
114
|
+
| `seo.ga4` | `channels`, `connect`, `daily`, `disconnect`, `landing_pages`, `list_properties`, `set_property` | Google Analytics 4 connection and reports. |
|
|
115
|
+
| `seo.bing` | `backlinks`, `connect`, `detail_stats`, `disconnect`, `get_feed_details`, `keyword_research`, `list_sites`, `save_crawl_settings`, `set_site`, `site_overview`, `submit_content`, `submit_feed`, `update_blocked_url`, `update_query_parameter`, `url_info`, `url_links` | Bing Webmaster: site overview, backlinks, keyword research, submissions, settings. |
|
|
116
|
+
| `seo.yandex` | `connect`, `disconnect`, `list_hosts`, `reindex_url`, `set_host`, `site_overview` | Yandex Webmaster: site overview, re-index, connection. |
|
|
117
|
+
| `seo.baidu` | `connection`, `disconnect`, `set_connection`, `submit_urls` | Baidu push API connection and URL submission. |
|
|
118
|
+
| `seo.index_now` | `submit` | IndexNow URL submission. |
|
|
119
|
+
| `seo.ahrefs` | `connect`, `disconnect`, `list_projects`, `set_project` | Ahrefs Connect project selection (inert until partner approval). |
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Client
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
MisarSEO::Client.new(
|
|
128
|
+
api_key: "mseo_…",
|
|
129
|
+
base_url: "https://api.misar.io/seo", # default
|
|
130
|
+
timeout: 30,
|
|
131
|
+
max_retries: 2
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| Option | Default | Meaning |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| base URL | `https://api.misar.io/seo` | API gateway. |
|
|
138
|
+
| timeout | 30 s | Per request; a stream's wait covers only the response headers. |
|
|
139
|
+
| max retries | 2 | Extra attempts for retryable failures (see [Retries](#retries)). |
|
|
140
|
+
| retry delays | 250 ms base, 60 s cap | Exponential backoff with jitter; `Retry-After` honoured up to the cap. |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Quick start
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
project_id = seo.projects.list["projects"].first["id"]
|
|
148
|
+
|
|
149
|
+
ideas = seo.keywords.research("projectId" => project_id, "keywords" => ["seo tools"])
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Primary functions
|
|
155
|
+
|
|
156
|
+
### Run a site audit and follow it live
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
job = seo.crawl.start("projectId" => project_id, "startUrl" => "https://example.com")
|
|
160
|
+
|
|
161
|
+
seo.crawl.stream_events(job["jobId"], project_id) do |frame|
|
|
162
|
+
puts "#{frame.event} #{frame.data}" # progress … then complete or error
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Read a backlink profile
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
profile = seo.backlinks.overview(project_id, "example.com")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Errors
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
begin
|
|
178
|
+
seo.projects.get("nope")
|
|
179
|
+
rescue MisarSEO::NotFoundError => e
|
|
180
|
+
puts e.code # "NOT_FOUND"
|
|
181
|
+
rescue MisarSEO::Error => e
|
|
182
|
+
puts "#{e.status} #{e.code}"
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| Type | When |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `MisarSEO::Error` | Base: `status`, `code`, `body`, `retry_after`, `not_found?`, `rate_limited?`. |
|
|
189
|
+
| `MisarSEO::PlanLimitError` | `offer`, `reason`, `feature`, `plan`, `upgrade_url`. |
|
|
190
|
+
| `ValidationError` … `NotFoundError`, `RateLimitError`, `UpstreamError`, `NetworkError` | 400–404, 429, 502–504, no response. |
|
|
191
|
+
|
|
192
|
+
### Plan limits
|
|
193
|
+
|
|
194
|
+
A call the organization's plan does not cover answers with
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{ "error": "...", "code": "plan_limit_exceeded", "upgrade": { "reason", "feature", "headline", "plans": [...], "urls": { "pricing" }, "cta" } }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- **402** — the feature is not in the plan, or a point-in-time cap (projects, API keys, tracked keywords) is reached.
|
|
201
|
+
- **429** with `Retry-After` — a day/month quota is spent; it resets at the period rollover.
|
|
202
|
+
|
|
203
|
+
Detection is by `code`, never by status: a quota block is a 429 and would otherwise look like an ordinary throttle. Every piece of copy — plan names, prices, what each plan unlocks, URLs — is in the offer, so render it rather than hard-coding any of it. `x-misar-plan` and `x-misar-upgrade-url` response headers carry the current plan and upgrade link.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
begin
|
|
207
|
+
seo.keywords.research("projectId" => project_id, "keywords" => ["seo"])
|
|
208
|
+
rescue MisarSEO::PlanLimitError => e
|
|
209
|
+
puts e.offer["headline"], e.upgrade_url
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Retries
|
|
216
|
+
|
|
217
|
+
- **429** is retried for every method (the limiter refuses before any work is done) — except a `plan_limit_exceeded` body, which is never retried.
|
|
218
|
+
- **502/503/504 and network failures** are retried for GET only: a POST may already have started a billed crawl.
|
|
219
|
+
- Exponential backoff from 250 ms with jitter; a `Retry-After` longer than the cap raises immediately instead of parking the caller.
|
|
220
|
+
- 400/401/402/403/404/409 and 500/501 are never retried — they fail the same way every time.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Pagination
|
|
225
|
+
|
|
226
|
+
The API pages two ways: offset pages (`{ rows, hasMore, page }`, e.g. backlink anchors) and keyset pages (`{ rows, hasMore, nextCursor }`, e.g. the activity log, whose rows are under `entries`). The helpers stop on `hasMore: false`, on an empty page, or on a repeated cursor, so a shrinking result set cannot loop forever against a billed endpoint.
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
anchors = MisarSEO::Pagination.collect_offset_pages do |page|
|
|
230
|
+
seo.backlinks.anchors(project_id, "example.com", query: { "page" => page })
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Streaming
|
|
237
|
+
|
|
238
|
+
`crawl` progress is a Server-Sent Events stream (`GET /crawl/:jobId/stream`, `Accept: text/event-stream`): `progress` frames roughly every 1.5 s, then one terminal `complete` or `error`, after which the SDK ends the stream. It is never retried — replaying a stream would duplicate what was already consumed. See [Run a site audit](#run-a-site-audit-and-follow-it-live).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Versioning
|
|
243
|
+
|
|
244
|
+
Semantic versioning; see [CHANGELOG.md](./CHANGELOG.md). Every SDK is generated from the same `manifest.json` (API 1.1.0), so a method added to the API appears in all thirteen at once.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Links
|
|
249
|
+
|
|
250
|
+
- Product: https://seo.misar.io
|
|
251
|
+
- API docs: https://docs.misar.io/seo
|
|
252
|
+
- Issues: https://github.com/Misar-AI/misarseo-sdks/issues
|
|
253
|
+
- Status: https://status.misar.io
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MisarSEO
|
|
4
|
+
# MisarSEO API client. Every resource is a method named after it —
|
|
5
|
+
# +client.projects+, +client.rank_tracking+, +client.ai_search+ …
|
|
6
|
+
#
|
|
7
|
+
# client = MisarSEO::Client.new(api_key: ENV["MISARSEO_API_KEY"])
|
|
8
|
+
# client.projects.list
|
|
9
|
+
class Client
|
|
10
|
+
Generated::RESOURCES.each do |name, klass|
|
|
11
|
+
define_method(name) { @resources[name] ||= klass.new(@transport) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# +api_key+ falls back to MISARSEO_API_KEY. +adapter+ replaces Net::HTTP (tests).
|
|
15
|
+
def initialize(api_key: nil, base_url: Transport::DEFAULT_BASE_URL, timeout: 30, max_retries: 2,
|
|
16
|
+
retry_base_delay: 0.25, max_retry_delay: 60, adapter: nil)
|
|
17
|
+
key = api_key.to_s.empty? ? ENV["MISARSEO_API_KEY"].to_s : api_key
|
|
18
|
+
if key.empty?
|
|
19
|
+
raise ArgumentError, "A MisarSEO API key (mseo_...) is required: pass api_key: or set " \
|
|
20
|
+
"MISARSEO_API_KEY; see https://seo.misar.io/ai"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@transport = Transport.new(api_key: key, base_url: base_url, timeout: timeout, max_retries: max_retries,
|
|
24
|
+
retry_base_delay: retry_base_delay, max_retry_delay: max_retry_delay,
|
|
25
|
+
adapter: adapter)
|
|
26
|
+
@resources = {}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MisarSEO
|
|
4
|
+
# The +code+ every Misar product sends on a plan-limit rejection.
|
|
5
|
+
PLAN_LIMIT_CODE = "plan_limit_exceeded"
|
|
6
|
+
|
|
7
|
+
# A non-2xx response, or (status 0) a request that never got one. +code+ is
|
|
8
|
+
# the API's own code — NOT_FOUND, VALIDATION_ERROR, RATE_LIMITED …
|
|
9
|
+
class Error < StandardError
|
|
10
|
+
attr_reader :status, :code, :body, :retry_after
|
|
11
|
+
|
|
12
|
+
def initialize(status, code, message, body = nil, retry_after = nil)
|
|
13
|
+
@status = status
|
|
14
|
+
@code = code
|
|
15
|
+
@body = body
|
|
16
|
+
@retry_after = retry_after
|
|
17
|
+
super(status.zero? ? "misarseo: #{message}" : "misarseo: HTTP #{status} (#{code}): #{message}")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def not_found?
|
|
21
|
+
status == 404
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def unauthorized?
|
|
25
|
+
status == 401
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# An ordinary throttle — never true for a plan limit.
|
|
29
|
+
def rate_limited?
|
|
30
|
+
status == 429 && !is_a?(PlanLimitError)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class ValidationError < Error; end
|
|
35
|
+
class AuthenticationError < Error; end
|
|
36
|
+
class PaymentRequiredError < Error; end
|
|
37
|
+
class PermissionError < Error; end
|
|
38
|
+
class NotFoundError < Error; end
|
|
39
|
+
class RateLimitError < Error; end
|
|
40
|
+
class UpstreamError < Error; end
|
|
41
|
+
class NetworkError < Error; end
|
|
42
|
+
|
|
43
|
+
# The organization's plan does not cover the call: a feature its tier locks
|
|
44
|
+
# (402), a spent day/month quota (429 with Retry-After at the rollover) or a
|
|
45
|
+
# point-in-time cap such as projects (402). Never retried — show +offer+, or
|
|
46
|
+
# at least +upgrade_url+, to the user.
|
|
47
|
+
class PlanLimitError < Error
|
|
48
|
+
# The server's +upgrade+ Hash: plans, prices, URLs — all server-side copy.
|
|
49
|
+
attr_reader :offer
|
|
50
|
+
# Current plan slug (x-misar-plan, else the offer).
|
|
51
|
+
attr_reader :plan
|
|
52
|
+
# Where to upgrade (x-misar-upgrade-url, else the offer).
|
|
53
|
+
attr_reader :upgrade_url
|
|
54
|
+
|
|
55
|
+
def initialize(status, message, body, retry_after, plan_header, upgrade_url_header)
|
|
56
|
+
upgrade = body.is_a?(Hash) ? body["upgrade"] : nil
|
|
57
|
+
@offer = upgrade.is_a?(Hash) ? upgrade : nil
|
|
58
|
+
@plan = plan_header || dig(@offer, "current_plan", "slug")
|
|
59
|
+
@upgrade_url = upgrade_url_header || dig(@offer, "urls", "pricing")
|
|
60
|
+
super(status, PLAN_LIMIT_CODE, message, body, retry_after)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# quota_exhausted, feature_locked …
|
|
64
|
+
def reason
|
|
65
|
+
dig(@offer, "reason")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Machine key of the limit that was hit.
|
|
69
|
+
def feature
|
|
70
|
+
dig(@offer, "feature")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def dig(hash, *path)
|
|
76
|
+
path.reduce(hash) { |cur, key| cur.is_a?(Hash) ? cur[key] : nil }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
STATUS_ERRORS = {
|
|
81
|
+
400 => ValidationError, 401 => AuthenticationError, 402 => PaymentRequiredError,
|
|
82
|
+
403 => PermissionError, 404 => NotFoundError, 429 => RateLimitError,
|
|
83
|
+
502 => UpstreamError, 503 => UpstreamError, 504 => UpstreamError
|
|
84
|
+
}.freeze
|
|
85
|
+
|
|
86
|
+
# The error for a non-2xx response. The plan-limit code is checked BEFORE the
|
|
87
|
+
# status: a spent quota is a 429 that otherwise looks like an ordinary throttle.
|
|
88
|
+
def self.error_from(status, headers, body)
|
|
89
|
+
data = body.is_a?(Hash) ? body : {}
|
|
90
|
+
message = (data["error"] || data["message"] || "HTTP #{status}").to_s
|
|
91
|
+
code = (data["code"] || "API_ERROR").to_s
|
|
92
|
+
h = headers.each_with_object({}) { |(k, v), acc| acc[k.to_s.downcase] = v.is_a?(Array) ? v.first : v }
|
|
93
|
+
retry_after = h["retry-after"].to_s.strip =~ /\A\d+\z/ ? h["retry-after"].to_i : nil
|
|
94
|
+
if code == PLAN_LIMIT_CODE
|
|
95
|
+
return PlanLimitError.new(status, message, body, retry_after, h["x-misar-plan"], h["x-misar-upgrade-url"])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
STATUS_ERRORS.fetch(status, Error).new(status, code, message, body, retry_after)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
|
|
2
|
+
# Do not edit by hand: changes are overwritten. To change this surface,
|
|
3
|
+
# change the API route and its registry entry, run `pnpm sdk:sync`, then
|
|
4
|
+
# `pnpm sdk:generate`.
|
|
5
|
+
|
|
6
|
+
module MisarSEO
|
|
7
|
+
module Generated
|
|
8
|
+
# +client.account+ — 3 endpoint(s).
|
|
9
|
+
class AccountResource
|
|
10
|
+
def initialize(transport)
|
|
11
|
+
@transport = transport
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# DELETE /account/delete
|
|
15
|
+
def delete(body = nil, query: nil, **fields)
|
|
16
|
+
@transport.request("DELETE", "/account/delete", query: query, body: Transport.body(body, fields))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# POST /account/delete
|
|
20
|
+
def delete_via_post(body = nil, query: nil, **fields)
|
|
21
|
+
@transport.request("POST", "/account/delete", query: query, body: Transport.body(body, fields))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# GET /account/export
|
|
25
|
+
def export(query: nil)
|
|
26
|
+
@transport.request("GET", "/account/export", query: query, body: nil)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
|
|
2
|
+
# Do not edit by hand: changes are overwritten. To change this surface,
|
|
3
|
+
# change the API route and its registry entry, run `pnpm sdk:sync`, then
|
|
4
|
+
# `pnpm sdk:generate`.
|
|
5
|
+
|
|
6
|
+
module MisarSEO
|
|
7
|
+
module Generated
|
|
8
|
+
# +client.activity+ — 1 endpoint(s).
|
|
9
|
+
class ActivityResource
|
|
10
|
+
def initialize(transport)
|
|
11
|
+
@transport = transport
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# GET /activity
|
|
15
|
+
#
|
|
16
|
+
# Optional query parameters: +before+, +pageSize+, +projectId+.
|
|
17
|
+
def list(query: nil)
|
|
18
|
+
@transport.request("GET", "/activity", query: query, body: nil)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
|
|
2
|
+
# Do not edit by hand: changes are overwritten. To change this surface,
|
|
3
|
+
# change the API route and its registry entry, run `pnpm sdk:sync`, then
|
|
4
|
+
# `pnpm sdk:generate`.
|
|
5
|
+
|
|
6
|
+
module MisarSEO
|
|
7
|
+
module Generated
|
|
8
|
+
# +client.ahrefs+ — 4 endpoint(s).
|
|
9
|
+
class AhrefsResource
|
|
10
|
+
def initialize(transport)
|
|
11
|
+
@transport = transport
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# GET /ahrefs/connection
|
|
15
|
+
def connect(project_id, query: nil)
|
|
16
|
+
@transport.request("GET", "/ahrefs/connection", query: Transport.merge(query, "projectId" => project_id), body: nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# DELETE /ahrefs/connection
|
|
20
|
+
def disconnect(body = nil, query: nil, **fields)
|
|
21
|
+
@transport.request("DELETE", "/ahrefs/connection", query: query, body: Transport.body(body, fields))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# GET /ahrefs/projects
|
|
25
|
+
def list_projects(project_id, query: nil)
|
|
26
|
+
@transport.request("GET", "/ahrefs/projects", query: Transport.merge(query, "projectId" => project_id), body: nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# POST /ahrefs/connection
|
|
30
|
+
def set_project(body = nil, query: nil, **fields)
|
|
31
|
+
@transport.request("POST", "/ahrefs/connection", query: query, body: Transport.body(body, fields))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
|
|
2
|
+
# Do not edit by hand: changes are overwritten. To change this surface,
|
|
3
|
+
# change the API route and its registry entry, run `pnpm sdk:sync`, then
|
|
4
|
+
# `pnpm sdk:generate`.
|
|
5
|
+
|
|
6
|
+
module MisarSEO
|
|
7
|
+
module Generated
|
|
8
|
+
# +client.ai_search+ — 14 endpoint(s).
|
|
9
|
+
class AiSearchResource
|
|
10
|
+
def initialize(transport)
|
|
11
|
+
@transport = transport
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# POST /ai/brand-lookup
|
|
15
|
+
def brand_lookup(body = nil, query: nil, **fields)
|
|
16
|
+
@transport.request("POST", "/ai/brand-lookup", query: query, body: Transport.body(body, fields))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# GET /ai/brand-lookup/:jobId/status
|
|
20
|
+
def brand_lookup_status(job_id, project_id, query: nil)
|
|
21
|
+
@transport.request("GET", "/ai/brand-lookup/#{Transport.seg(job_id)}/status", query: Transport.merge(query, "projectId" => project_id), body: nil)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# GET /ai/brand-scan-trend
|
|
25
|
+
#
|
|
26
|
+
# Optional query parameters: +since+.
|
|
27
|
+
def brand_scan_trend(project_id, target, query: nil)
|
|
28
|
+
@transport.request("GET", "/ai/brand-scan-trend", query: Transport.merge(query, "projectId" => project_id, "target" => target), body: nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# POST /citations
|
|
32
|
+
def citations(body = nil, query: nil, **fields)
|
|
33
|
+
@transport.request("POST", "/citations", query: query, body: Transport.body(body, fields))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# POST /ai/competitor-positioning
|
|
37
|
+
def competitor_positioning(body = nil, query: nil, **fields)
|
|
38
|
+
@transport.request("POST", "/ai/competitor-positioning", query: query, body: Transport.body(body, fields))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# DELETE /ai/brand-monitors/:monitorId
|
|
42
|
+
def delete_brand_monitor(monitor_id, project_id, query: nil)
|
|
43
|
+
@transport.request("DELETE", "/ai/brand-monitors/#{Transport.seg(monitor_id)}", query: Transport.merge(query, "projectId" => project_id), body: nil)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# DELETE /ai/prompt-tracking/:trackedPromptId
|
|
47
|
+
def delete_tracked_prompt(tracked_prompt_id, project_id, query: nil)
|
|
48
|
+
@transport.request("DELETE", "/ai/prompt-tracking/#{Transport.seg(tracked_prompt_id)}", query: Transport.merge(query, "projectId" => project_id), body: nil)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# GET /ai/brand-monitors
|
|
52
|
+
def list_brand_monitors(project_id, query: nil)
|
|
53
|
+
@transport.request("GET", "/ai/brand-monitors", query: Transport.merge(query, "projectId" => project_id), body: nil)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# GET /ai/prompt-tracking
|
|
57
|
+
def list_tracked_prompts(project_id, query: nil)
|
|
58
|
+
@transport.request("GET", "/ai/prompt-tracking", query: Transport.merge(query, "projectId" => project_id), body: nil)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# POST /ai/prompt-explorer
|
|
62
|
+
def prompt_explorer(body = nil, query: nil, **fields)
|
|
63
|
+
@transport.request("POST", "/ai/prompt-explorer", query: query, body: Transport.body(body, fields))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# POST /ai/prompt-research
|
|
67
|
+
def prompt_research(body = nil, query: nil, **fields)
|
|
68
|
+
@transport.request("POST", "/ai/prompt-research", query: query, body: Transport.body(body, fields))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# GET /ai/prompt-tracking/:trackedPromptId/trend
|
|
72
|
+
def tracked_prompt_trend(tracked_prompt_id, project_id, query: nil)
|
|
73
|
+
@transport.request("GET", "/ai/prompt-tracking/#{Transport.seg(tracked_prompt_id)}/trend", query: Transport.merge(query, "projectId" => project_id), body: nil)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# POST /ai/brand-monitors
|
|
77
|
+
def upsert_brand_monitor(body = nil, query: nil, **fields)
|
|
78
|
+
@transport.request("POST", "/ai/brand-monitors", query: query, body: Transport.body(body, fields))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# POST /ai/prompt-tracking
|
|
82
|
+
def upsert_tracked_prompt(body = nil, query: nil, **fields)
|
|
83
|
+
@transport.request("POST", "/ai/prompt-tracking", query: query, body: Transport.body(body, fields))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# AUTO-GENERATED by scripts/generate-sdks.mjs from sdks/manifest.json.
|
|
2
|
+
# Do not edit by hand: changes are overwritten. To change this surface,
|
|
3
|
+
# change the API route and its registry entry, run `pnpm sdk:sync`, then
|
|
4
|
+
# `pnpm sdk:generate`.
|
|
5
|
+
|
|
6
|
+
module MisarSEO
|
|
7
|
+
module Generated
|
|
8
|
+
# +client.backlinks+ — 3 endpoint(s).
|
|
9
|
+
class BacklinksResource
|
|
10
|
+
def initialize(transport)
|
|
11
|
+
@transport = transport
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# GET /backlinks/anchors
|
|
15
|
+
#
|
|
16
|
+
# Optional query parameters: +page+, +pageSize+, +scope+, +sortField+, +sortOrder+.
|
|
17
|
+
def anchors(project_id, target, query: nil)
|
|
18
|
+
@transport.request("GET", "/backlinks/anchors", query: Transport.merge(query, "projectId" => project_id, "target" => target), body: nil)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# POST /backlinks/intersect
|
|
22
|
+
def intersect(body = nil, query: nil, **fields)
|
|
23
|
+
@transport.request("POST", "/backlinks/intersect", query: query, body: Transport.body(body, fields))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# GET /backlinks
|
|
27
|
+
#
|
|
28
|
+
# Optional query parameters: +scope+.
|
|
29
|
+
def overview(project_id, target, query: nil)
|
|
30
|
+
@transport.request("GET", "/backlinks", query: Transport.merge(query, "projectId" => project_id, "target" => target), body: nil)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|