surmado 0.1.0__tar.gz
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.
- surmado-0.1.0/LICENSE +22 -0
- surmado-0.1.0/PKG-INFO +308 -0
- surmado-0.1.0/README.md +279 -0
- surmado-0.1.0/pyproject.toml +61 -0
- surmado-0.1.0/surmado/__init__.py +47 -0
- surmado-0.1.0/surmado/client.py +571 -0
surmado-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Surmado, Inc.
|
|
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.
|
|
22
|
+
|
surmado-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: surmado
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for Surmado - AI marketing intelligence and SEO auditing
|
|
5
|
+
Project-URL: Homepage, https://surmado.com
|
|
6
|
+
Project-URL: Documentation, https://help.surmado.com/docs/api-reference/
|
|
7
|
+
Project-URL: Repository, https://github.com/surmado/surmado-python
|
|
8
|
+
Project-URL: Issues, https://github.com/surmado/surmado-python/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/surmado/surmado-python/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Surmado Engineering <engineering@surmado.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai,analytics,chatgpt,geo,llm,marketing,perplexity,seo,surmado,visibility
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Requires-Dist: requests>=2.25.0
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Surmado Python Client
|
|
31
|
+
|
|
32
|
+
Official Python SDK for [Surmado](https://surmado.com) — the anti-dashboard marketing intelligence engine.
|
|
33
|
+
|
|
34
|
+
**SEO audits, AI visibility testing, and strategic advisory. Reports cost $25–$50. No subscriptions. No dashboards.**
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install surmado
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from surmado import Surmado
|
|
46
|
+
|
|
47
|
+
# Initialize (or set SURMADO_API_KEY env var)
|
|
48
|
+
client = Surmado(api_key="sur_live_...")
|
|
49
|
+
|
|
50
|
+
# Run an AI Visibility Test
|
|
51
|
+
report = client.signal(
|
|
52
|
+
url="https://example.com",
|
|
53
|
+
brand_name="Example Brand",
|
|
54
|
+
email="you@example.com",
|
|
55
|
+
industry="E-commerce",
|
|
56
|
+
location="United States",
|
|
57
|
+
persona="Small business owners looking for affordable solutions",
|
|
58
|
+
pain_points="Finding reliable vendors, managing costs",
|
|
59
|
+
brand_details="Affordable solutions for growing businesses",
|
|
60
|
+
direct_competitors="Competitor A, Competitor B"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
print(f"Report queued: {report['report_id']}")
|
|
64
|
+
print(f"Token (save for Solutions): {report['token']}")
|
|
65
|
+
|
|
66
|
+
# Wait for completion (or use webhooks)
|
|
67
|
+
completed = client.wait_for_report(report["report_id"])
|
|
68
|
+
print(f"PDF ready: {completed['download_url']}")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Features
|
|
72
|
+
|
|
73
|
+
### Signal — AI Visibility Testing
|
|
74
|
+
|
|
75
|
+
Test how your brand appears across 7 AI platforms: ChatGPT, Perplexity, Google Gemini, Claude, Meta AI, Grok, DeepSeek.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
result = client.signal(
|
|
79
|
+
url="https://acme.com",
|
|
80
|
+
brand_name="Acme Corp", # max 100 chars
|
|
81
|
+
email="you@acme.com",
|
|
82
|
+
industry="B2B SaaS", # max 200 chars
|
|
83
|
+
location="United States", # max 200 chars
|
|
84
|
+
persona="CTOs at mid-market companies", # max 800 chars
|
|
85
|
+
pain_points="Integration challenges, lack of visibility", # max 1000 chars
|
|
86
|
+
brand_details="Modern, dev-focused tooling", # max 1200 chars
|
|
87
|
+
direct_competitors="Asana, Monday.com", # max 500 chars
|
|
88
|
+
tier="pro" # "basic" (1 credit) or "pro" (2 credits)
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Scan — SEO Auditing
|
|
93
|
+
|
|
94
|
+
Comprehensive technical SEO audits with prioritized recommendations.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
result = client.scan(
|
|
98
|
+
url="https://acme.com",
|
|
99
|
+
brand_name="Acme Corp",
|
|
100
|
+
email="you@acme.com",
|
|
101
|
+
tier="premium", # "basic" (1 credit) or "premium" (2 credits)
|
|
102
|
+
competitor_urls=["https://competitor1.com", "https://competitor2.com"]
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Solutions — Strategic Advisory
|
|
107
|
+
|
|
108
|
+
Multi-AI strategic recommendations from 6 specialized agents.
|
|
109
|
+
|
|
110
|
+
**Mode 1: With Signal Token** (recommended)
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# Run Signal first, then pass the token
|
|
114
|
+
signal_result = client.signal(...)
|
|
115
|
+
solutions_result = client.solutions(
|
|
116
|
+
email="you@acme.com",
|
|
117
|
+
signal_token=signal_result["token"]
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Mode 2: Standalone**
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
result = client.solutions(
|
|
125
|
+
email="you@acme.com",
|
|
126
|
+
brand_name="Acme Corp", # max 100 chars
|
|
127
|
+
business_story="We're a B2B SaaS company in project management...", # max 2000 chars
|
|
128
|
+
decision="Should we expand to enterprise market?", # max 1500 chars
|
|
129
|
+
success="$10M ARR in 18 months", # max 1000 chars
|
|
130
|
+
timeline="Q2 2025", # max 200 chars
|
|
131
|
+
scale_indicator="$2M ARR, 20 employees" # max 100 chars
|
|
132
|
+
)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Mode 3: With Financial Analysis**
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
result = client.solutions(
|
|
139
|
+
email="you@acme.com",
|
|
140
|
+
signal_token=signal_result["token"],
|
|
141
|
+
include_financial="yes",
|
|
142
|
+
financial_context="Growing but need to optimize costs",
|
|
143
|
+
monthly_revenue="$50K",
|
|
144
|
+
monthly_costs="$40K",
|
|
145
|
+
cash_available="$200K"
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Rerun Methods (Automation-Friendly)
|
|
150
|
+
|
|
151
|
+
Once you've set up a brand with personas in the Surmado dashboard, you can run reports with minimal code:
|
|
152
|
+
|
|
153
|
+
### Signal Rerun
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# Just 4 fields instead of 10+
|
|
157
|
+
result = client.signal_rerun(
|
|
158
|
+
brand_slug="acme_corp",
|
|
159
|
+
persona_slug="cto-enterprise",
|
|
160
|
+
email="you@acme.com",
|
|
161
|
+
tier="basic"
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Scan Rerun
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
# Just 3 fields
|
|
169
|
+
result = client.scan_rerun(
|
|
170
|
+
brand_slug="acme_corp",
|
|
171
|
+
email="you@acme.com",
|
|
172
|
+
tier="premium"
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Perfect for:
|
|
177
|
+
- **Zapier/Make/n8n workflows** — set up brand once, automate reports
|
|
178
|
+
- **Scheduled monitoring** — weekly SEO scans or monthly AI visibility checks
|
|
179
|
+
- **Dashboard "Run Again"** — one-click report refresh
|
|
180
|
+
|
|
181
|
+
## Async Reports
|
|
182
|
+
|
|
183
|
+
All reports are processed asynchronously (~15 minutes). Two ways to get results:
|
|
184
|
+
|
|
185
|
+
### Option 1: Polling
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
report = client.signal(...)
|
|
189
|
+
completed = client.wait_for_report(report["report_id"], timeout_minutes=20)
|
|
190
|
+
print(completed["download_url"])
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Option 2: Webhooks
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
report = client.signal(
|
|
197
|
+
...,
|
|
198
|
+
webhook_url="https://your-server.com/webhook"
|
|
199
|
+
)
|
|
200
|
+
# Your webhook receives POST when report completes
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Response Format
|
|
204
|
+
|
|
205
|
+
Report creation returns HTTP 202 Accepted:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
{
|
|
209
|
+
"report_id": "rpt_abc123def456",
|
|
210
|
+
"token": "tok_xyz789abc123", # Save this for Solutions Mode 1
|
|
211
|
+
"org_id": "org_xyz789",
|
|
212
|
+
"product": "signal",
|
|
213
|
+
"status": "queued",
|
|
214
|
+
"brand_slug": "example_brand",
|
|
215
|
+
"brand_name": "Example Brand",
|
|
216
|
+
"credits_used": 1,
|
|
217
|
+
"created_at": "2025-01-15T10:30:00Z"
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Completed reports include download URLs (expire in 15 minutes):
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
{
|
|
225
|
+
"status": "completed",
|
|
226
|
+
"download_url": "https://storage.googleapis.com/...", # PDF
|
|
227
|
+
"pptx_download_url": "https://storage.googleapis.com/...", # PPTX (Pro only)
|
|
228
|
+
"intelligence_download_url": "https://storage.googleapis.com/...", # Full JSON
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Error Handling
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from surmado import (
|
|
236
|
+
Surmado,
|
|
237
|
+
AuthenticationError,
|
|
238
|
+
InsufficientCreditsError,
|
|
239
|
+
NotFoundError,
|
|
240
|
+
ValidationError,
|
|
241
|
+
SurmadoError
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
client = Surmado()
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
result = client.signal(...)
|
|
248
|
+
except AuthenticationError:
|
|
249
|
+
print("Invalid API key")
|
|
250
|
+
except InsufficientCreditsError as e:
|
|
251
|
+
print(f"Need more credits: {e.response}")
|
|
252
|
+
except NotFoundError:
|
|
253
|
+
print("Brand or report not found")
|
|
254
|
+
except ValidationError as e:
|
|
255
|
+
print(f"Invalid request: {e}")
|
|
256
|
+
except SurmadoError as e:
|
|
257
|
+
print(f"API error: {e.status_code} - {e}")
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Field Length Limits
|
|
261
|
+
|
|
262
|
+
| Field | Max Length |
|
|
263
|
+
|-------|------------|
|
|
264
|
+
| brand_name | 100 chars |
|
|
265
|
+
| industry | 200 chars |
|
|
266
|
+
| location | 200 chars |
|
|
267
|
+
| persona | 800 chars |
|
|
268
|
+
| pain_points | 1000 chars |
|
|
269
|
+
| brand_details | 1200 chars |
|
|
270
|
+
| direct_competitors | 500 chars |
|
|
271
|
+
| indirect_competitors | 500 chars |
|
|
272
|
+
| keywords | 500 chars |
|
|
273
|
+
| product | 1000 chars |
|
|
274
|
+
| business_story | 2000 chars |
|
|
275
|
+
| decision | 1500 chars |
|
|
276
|
+
| success | 1000 chars |
|
|
277
|
+
| timeline | 200 chars |
|
|
278
|
+
| scale_indicator | 100 chars |
|
|
279
|
+
|
|
280
|
+
## Pricing
|
|
281
|
+
|
|
282
|
+
| Product | Price | Credits |
|
|
283
|
+
|---------|-------|---------|
|
|
284
|
+
| Scan Basic | $25 | 1 |
|
|
285
|
+
| Scan Premium | $50 | 2 |
|
|
286
|
+
| Signal Basic | $25 | 1 |
|
|
287
|
+
| Signal Pro | $50 | 2 |
|
|
288
|
+
| Solutions | $50 | 2 |
|
|
289
|
+
|
|
290
|
+
**Credits:** 1 credit = $25. No subscriptions. Credits don't expire.
|
|
291
|
+
|
|
292
|
+
## Links
|
|
293
|
+
|
|
294
|
+
- [Documentation](https://help.surmado.com/docs/api-reference/)
|
|
295
|
+
- [Get API Key](https://surmado.com/login)
|
|
296
|
+
- [API Examples](https://github.com/surmado/surmado-api-public)
|
|
297
|
+
|
|
298
|
+
## About Surmado
|
|
299
|
+
|
|
300
|
+
Surmado is an AI marketing intelligence company based in San Diego, California. Founded in October 2025, we build tools that help businesses understand their visibility in AI search results and traditional SEO.
|
|
301
|
+
|
|
302
|
+
- Website: [surmado.com](https://surmado.com)
|
|
303
|
+
- Help: [help.surmado.com](https://help.surmado.com)
|
|
304
|
+
- Contact: [hi@surmado.com](mailto:hi@surmado.com)
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT
|
surmado-0.1.0/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Surmado Python Client
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [Surmado](https://surmado.com) — the anti-dashboard marketing intelligence engine.
|
|
4
|
+
|
|
5
|
+
**SEO audits, AI visibility testing, and strategic advisory. Reports cost $25–$50. No subscriptions. No dashboards.**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install surmado
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from surmado import Surmado
|
|
17
|
+
|
|
18
|
+
# Initialize (or set SURMADO_API_KEY env var)
|
|
19
|
+
client = Surmado(api_key="sur_live_...")
|
|
20
|
+
|
|
21
|
+
# Run an AI Visibility Test
|
|
22
|
+
report = client.signal(
|
|
23
|
+
url="https://example.com",
|
|
24
|
+
brand_name="Example Brand",
|
|
25
|
+
email="you@example.com",
|
|
26
|
+
industry="E-commerce",
|
|
27
|
+
location="United States",
|
|
28
|
+
persona="Small business owners looking for affordable solutions",
|
|
29
|
+
pain_points="Finding reliable vendors, managing costs",
|
|
30
|
+
brand_details="Affordable solutions for growing businesses",
|
|
31
|
+
direct_competitors="Competitor A, Competitor B"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
print(f"Report queued: {report['report_id']}")
|
|
35
|
+
print(f"Token (save for Solutions): {report['token']}")
|
|
36
|
+
|
|
37
|
+
# Wait for completion (or use webhooks)
|
|
38
|
+
completed = client.wait_for_report(report["report_id"])
|
|
39
|
+
print(f"PDF ready: {completed['download_url']}")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
### Signal — AI Visibility Testing
|
|
45
|
+
|
|
46
|
+
Test how your brand appears across 7 AI platforms: ChatGPT, Perplexity, Google Gemini, Claude, Meta AI, Grok, DeepSeek.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
result = client.signal(
|
|
50
|
+
url="https://acme.com",
|
|
51
|
+
brand_name="Acme Corp", # max 100 chars
|
|
52
|
+
email="you@acme.com",
|
|
53
|
+
industry="B2B SaaS", # max 200 chars
|
|
54
|
+
location="United States", # max 200 chars
|
|
55
|
+
persona="CTOs at mid-market companies", # max 800 chars
|
|
56
|
+
pain_points="Integration challenges, lack of visibility", # max 1000 chars
|
|
57
|
+
brand_details="Modern, dev-focused tooling", # max 1200 chars
|
|
58
|
+
direct_competitors="Asana, Monday.com", # max 500 chars
|
|
59
|
+
tier="pro" # "basic" (1 credit) or "pro" (2 credits)
|
|
60
|
+
)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Scan — SEO Auditing
|
|
64
|
+
|
|
65
|
+
Comprehensive technical SEO audits with prioritized recommendations.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
result = client.scan(
|
|
69
|
+
url="https://acme.com",
|
|
70
|
+
brand_name="Acme Corp",
|
|
71
|
+
email="you@acme.com",
|
|
72
|
+
tier="premium", # "basic" (1 credit) or "premium" (2 credits)
|
|
73
|
+
competitor_urls=["https://competitor1.com", "https://competitor2.com"]
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Solutions — Strategic Advisory
|
|
78
|
+
|
|
79
|
+
Multi-AI strategic recommendations from 6 specialized agents.
|
|
80
|
+
|
|
81
|
+
**Mode 1: With Signal Token** (recommended)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# Run Signal first, then pass the token
|
|
85
|
+
signal_result = client.signal(...)
|
|
86
|
+
solutions_result = client.solutions(
|
|
87
|
+
email="you@acme.com",
|
|
88
|
+
signal_token=signal_result["token"]
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Mode 2: Standalone**
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
result = client.solutions(
|
|
96
|
+
email="you@acme.com",
|
|
97
|
+
brand_name="Acme Corp", # max 100 chars
|
|
98
|
+
business_story="We're a B2B SaaS company in project management...", # max 2000 chars
|
|
99
|
+
decision="Should we expand to enterprise market?", # max 1500 chars
|
|
100
|
+
success="$10M ARR in 18 months", # max 1000 chars
|
|
101
|
+
timeline="Q2 2025", # max 200 chars
|
|
102
|
+
scale_indicator="$2M ARR, 20 employees" # max 100 chars
|
|
103
|
+
)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Mode 3: With Financial Analysis**
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
result = client.solutions(
|
|
110
|
+
email="you@acme.com",
|
|
111
|
+
signal_token=signal_result["token"],
|
|
112
|
+
include_financial="yes",
|
|
113
|
+
financial_context="Growing but need to optimize costs",
|
|
114
|
+
monthly_revenue="$50K",
|
|
115
|
+
monthly_costs="$40K",
|
|
116
|
+
cash_available="$200K"
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Rerun Methods (Automation-Friendly)
|
|
121
|
+
|
|
122
|
+
Once you've set up a brand with personas in the Surmado dashboard, you can run reports with minimal code:
|
|
123
|
+
|
|
124
|
+
### Signal Rerun
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# Just 4 fields instead of 10+
|
|
128
|
+
result = client.signal_rerun(
|
|
129
|
+
brand_slug="acme_corp",
|
|
130
|
+
persona_slug="cto-enterprise",
|
|
131
|
+
email="you@acme.com",
|
|
132
|
+
tier="basic"
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Scan Rerun
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Just 3 fields
|
|
140
|
+
result = client.scan_rerun(
|
|
141
|
+
brand_slug="acme_corp",
|
|
142
|
+
email="you@acme.com",
|
|
143
|
+
tier="premium"
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Perfect for:
|
|
148
|
+
- **Zapier/Make/n8n workflows** — set up brand once, automate reports
|
|
149
|
+
- **Scheduled monitoring** — weekly SEO scans or monthly AI visibility checks
|
|
150
|
+
- **Dashboard "Run Again"** — one-click report refresh
|
|
151
|
+
|
|
152
|
+
## Async Reports
|
|
153
|
+
|
|
154
|
+
All reports are processed asynchronously (~15 minutes). Two ways to get results:
|
|
155
|
+
|
|
156
|
+
### Option 1: Polling
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
report = client.signal(...)
|
|
160
|
+
completed = client.wait_for_report(report["report_id"], timeout_minutes=20)
|
|
161
|
+
print(completed["download_url"])
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Option 2: Webhooks
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
report = client.signal(
|
|
168
|
+
...,
|
|
169
|
+
webhook_url="https://your-server.com/webhook"
|
|
170
|
+
)
|
|
171
|
+
# Your webhook receives POST when report completes
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Response Format
|
|
175
|
+
|
|
176
|
+
Report creation returns HTTP 202 Accepted:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
{
|
|
180
|
+
"report_id": "rpt_abc123def456",
|
|
181
|
+
"token": "tok_xyz789abc123", # Save this for Solutions Mode 1
|
|
182
|
+
"org_id": "org_xyz789",
|
|
183
|
+
"product": "signal",
|
|
184
|
+
"status": "queued",
|
|
185
|
+
"brand_slug": "example_brand",
|
|
186
|
+
"brand_name": "Example Brand",
|
|
187
|
+
"credits_used": 1,
|
|
188
|
+
"created_at": "2025-01-15T10:30:00Z"
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Completed reports include download URLs (expire in 15 minutes):
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
{
|
|
196
|
+
"status": "completed",
|
|
197
|
+
"download_url": "https://storage.googleapis.com/...", # PDF
|
|
198
|
+
"pptx_download_url": "https://storage.googleapis.com/...", # PPTX (Pro only)
|
|
199
|
+
"intelligence_download_url": "https://storage.googleapis.com/...", # Full JSON
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Error Handling
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from surmado import (
|
|
207
|
+
Surmado,
|
|
208
|
+
AuthenticationError,
|
|
209
|
+
InsufficientCreditsError,
|
|
210
|
+
NotFoundError,
|
|
211
|
+
ValidationError,
|
|
212
|
+
SurmadoError
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
client = Surmado()
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
result = client.signal(...)
|
|
219
|
+
except AuthenticationError:
|
|
220
|
+
print("Invalid API key")
|
|
221
|
+
except InsufficientCreditsError as e:
|
|
222
|
+
print(f"Need more credits: {e.response}")
|
|
223
|
+
except NotFoundError:
|
|
224
|
+
print("Brand or report not found")
|
|
225
|
+
except ValidationError as e:
|
|
226
|
+
print(f"Invalid request: {e}")
|
|
227
|
+
except SurmadoError as e:
|
|
228
|
+
print(f"API error: {e.status_code} - {e}")
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Field Length Limits
|
|
232
|
+
|
|
233
|
+
| Field | Max Length |
|
|
234
|
+
|-------|------------|
|
|
235
|
+
| brand_name | 100 chars |
|
|
236
|
+
| industry | 200 chars |
|
|
237
|
+
| location | 200 chars |
|
|
238
|
+
| persona | 800 chars |
|
|
239
|
+
| pain_points | 1000 chars |
|
|
240
|
+
| brand_details | 1200 chars |
|
|
241
|
+
| direct_competitors | 500 chars |
|
|
242
|
+
| indirect_competitors | 500 chars |
|
|
243
|
+
| keywords | 500 chars |
|
|
244
|
+
| product | 1000 chars |
|
|
245
|
+
| business_story | 2000 chars |
|
|
246
|
+
| decision | 1500 chars |
|
|
247
|
+
| success | 1000 chars |
|
|
248
|
+
| timeline | 200 chars |
|
|
249
|
+
| scale_indicator | 100 chars |
|
|
250
|
+
|
|
251
|
+
## Pricing
|
|
252
|
+
|
|
253
|
+
| Product | Price | Credits |
|
|
254
|
+
|---------|-------|---------|
|
|
255
|
+
| Scan Basic | $25 | 1 |
|
|
256
|
+
| Scan Premium | $50 | 2 |
|
|
257
|
+
| Signal Basic | $25 | 1 |
|
|
258
|
+
| Signal Pro | $50 | 2 |
|
|
259
|
+
| Solutions | $50 | 2 |
|
|
260
|
+
|
|
261
|
+
**Credits:** 1 credit = $25. No subscriptions. Credits don't expire.
|
|
262
|
+
|
|
263
|
+
## Links
|
|
264
|
+
|
|
265
|
+
- [Documentation](https://help.surmado.com/docs/api-reference/)
|
|
266
|
+
- [Get API Key](https://surmado.com/login)
|
|
267
|
+
- [API Examples](https://github.com/surmado/surmado-api-public)
|
|
268
|
+
|
|
269
|
+
## About Surmado
|
|
270
|
+
|
|
271
|
+
Surmado is an AI marketing intelligence company based in San Diego, California. Founded in October 2025, we build tools that help businesses understand their visibility in AI search results and traditional SEO.
|
|
272
|
+
|
|
273
|
+
- Website: [surmado.com](https://surmado.com)
|
|
274
|
+
- Help: [help.surmado.com](https://help.surmado.com)
|
|
275
|
+
- Contact: [hi@surmado.com](mailto:hi@surmado.com)
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "surmado"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client for Surmado - AI marketing intelligence and SEO auditing"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Surmado Engineering", email = "engineering@surmado.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"surmado",
|
|
17
|
+
"seo",
|
|
18
|
+
"ai",
|
|
19
|
+
"marketing",
|
|
20
|
+
"visibility",
|
|
21
|
+
"analytics",
|
|
22
|
+
"llm",
|
|
23
|
+
"chatgpt",
|
|
24
|
+
"perplexity",
|
|
25
|
+
"geo",
|
|
26
|
+
]
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 4 - Beta",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"License :: OSI Approved :: MIT License",
|
|
31
|
+
"Operating System :: OS Independent",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Programming Language :: Python :: 3.8",
|
|
34
|
+
"Programming Language :: Python :: 3.9",
|
|
35
|
+
"Programming Language :: Python :: 3.10",
|
|
36
|
+
"Programming Language :: Python :: 3.11",
|
|
37
|
+
"Programming Language :: Python :: 3.12",
|
|
38
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
39
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
40
|
+
]
|
|
41
|
+
dependencies = [
|
|
42
|
+
"requests>=2.25.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://surmado.com"
|
|
47
|
+
Documentation = "https://help.surmado.com/docs/api-reference/"
|
|
48
|
+
Repository = "https://github.com/surmado/surmado-python"
|
|
49
|
+
Issues = "https://github.com/surmado/surmado-python/issues"
|
|
50
|
+
Changelog = "https://github.com/surmado/surmado-python/blob/main/CHANGELOG.md"
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.sdist]
|
|
53
|
+
include = [
|
|
54
|
+
"/surmado",
|
|
55
|
+
"/README.md",
|
|
56
|
+
"/LICENSE",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.hatch.build.targets.wheel]
|
|
60
|
+
packages = ["surmado"]
|
|
61
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Surmado Python SDK
|
|
3
|
+
|
|
4
|
+
Official Python client for Surmado - the anti-dashboard marketing intelligence engine.
|
|
5
|
+
|
|
6
|
+
Installation:
|
|
7
|
+
pip install surmado
|
|
8
|
+
|
|
9
|
+
Quick Start:
|
|
10
|
+
>>> from surmado import Surmado
|
|
11
|
+
>>> client = Surmado() # reads SURMADO_API_KEY from env
|
|
12
|
+
>>> report = client.signal(
|
|
13
|
+
... url="https://example.com",
|
|
14
|
+
... brand_name="Example",
|
|
15
|
+
... email="you@example.com",
|
|
16
|
+
... industry="E-commerce",
|
|
17
|
+
... location="United States",
|
|
18
|
+
... persona="Small business owners",
|
|
19
|
+
... pain_points="Finding reliable vendors",
|
|
20
|
+
... brand_details="Affordable solutions",
|
|
21
|
+
... direct_competitors="Competitor A, Competitor B"
|
|
22
|
+
... )
|
|
23
|
+
|
|
24
|
+
Documentation: https://surmado.com/docs
|
|
25
|
+
API Reference: https://help.surmado.com/docs/api-reference/
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from .client import (
|
|
29
|
+
Surmado,
|
|
30
|
+
SurmadoError,
|
|
31
|
+
AuthenticationError,
|
|
32
|
+
InsufficientCreditsError,
|
|
33
|
+
NotFoundError,
|
|
34
|
+
ValidationError,
|
|
35
|
+
__version__,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"Surmado",
|
|
40
|
+
"SurmadoError",
|
|
41
|
+
"AuthenticationError",
|
|
42
|
+
"InsufficientCreditsError",
|
|
43
|
+
"NotFoundError",
|
|
44
|
+
"ValidationError",
|
|
45
|
+
"__version__",
|
|
46
|
+
]
|
|
47
|
+
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Surmado Python Client
|
|
3
|
+
|
|
4
|
+
Official Python SDK for Surmado - the anti-dashboard marketing intelligence engine.
|
|
5
|
+
https://surmado.com
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import requests
|
|
10
|
+
from typing import Dict, Optional, Any, List
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SurmadoError(Exception):
|
|
16
|
+
"""Base exception for Surmado SDK errors."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message: str, status_code: int = None, response: dict = None):
|
|
19
|
+
super().__init__(message)
|
|
20
|
+
self.status_code = status_code
|
|
21
|
+
self.response = response
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthenticationError(SurmadoError):
|
|
25
|
+
"""Raised when API key is invalid or missing."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InsufficientCreditsError(SurmadoError):
|
|
30
|
+
"""Raised when account doesn't have enough credits."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NotFoundError(SurmadoError):
|
|
35
|
+
"""Raised when a report or resource is not found."""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ValidationError(SurmadoError):
|
|
40
|
+
"""Raised when request data is invalid."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Surmado:
|
|
45
|
+
"""
|
|
46
|
+
Official Surmado API client.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
api_key: Your Surmado API key (starts with sur_live_ or sur_test_).
|
|
50
|
+
If not provided, reads from SURMADO_API_KEY env var.
|
|
51
|
+
base_url: API base URL. Defaults to https://api.surmado.com/v1
|
|
52
|
+
timeout: Request timeout in seconds. Defaults to 30.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> from surmado import Surmado
|
|
56
|
+
>>> client = Surmado() # reads SURMADO_API_KEY from env
|
|
57
|
+
>>> result = client.signal(
|
|
58
|
+
... url="https://example.com",
|
|
59
|
+
... brand_name="Example Brand",
|
|
60
|
+
... email="you@example.com",
|
|
61
|
+
... industry="E-commerce",
|
|
62
|
+
... location="United States",
|
|
63
|
+
... persona="Small business owners",
|
|
64
|
+
... pain_points="Finding reliable vendors",
|
|
65
|
+
... brand_details="Affordable solutions",
|
|
66
|
+
... direct_competitors="Competitor A, Competitor B"
|
|
67
|
+
... )
|
|
68
|
+
>>> print(result["report_id"])
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
api_key: str = None,
|
|
74
|
+
base_url: str = None,
|
|
75
|
+
timeout: int = 30
|
|
76
|
+
):
|
|
77
|
+
self.api_key = api_key or os.getenv("SURMADO_API_KEY")
|
|
78
|
+
if not self.api_key:
|
|
79
|
+
raise AuthenticationError(
|
|
80
|
+
"API key required. Set SURMADO_API_KEY environment variable "
|
|
81
|
+
"or pass api_key parameter."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.base_url = base_url or "https://api.surmado.com/v1"
|
|
85
|
+
self.timeout = timeout
|
|
86
|
+
|
|
87
|
+
# =========================================================================
|
|
88
|
+
# Full Report Methods (all fields provided)
|
|
89
|
+
# =========================================================================
|
|
90
|
+
|
|
91
|
+
def signal(
|
|
92
|
+
self,
|
|
93
|
+
url: str,
|
|
94
|
+
brand_name: str,
|
|
95
|
+
email: str,
|
|
96
|
+
industry: str,
|
|
97
|
+
location: str,
|
|
98
|
+
persona: str,
|
|
99
|
+
pain_points: str,
|
|
100
|
+
brand_details: str,
|
|
101
|
+
direct_competitors: str,
|
|
102
|
+
tier: str = "basic",
|
|
103
|
+
**kwargs
|
|
104
|
+
) -> Dict[str, Any]:
|
|
105
|
+
"""
|
|
106
|
+
Run an AI Visibility Test (Signal).
|
|
107
|
+
|
|
108
|
+
Tests how your brand appears across 7 AI platforms:
|
|
109
|
+
ChatGPT, Perplexity, Google Gemini, Claude, Meta AI, Grok, DeepSeek.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
url: Your website URL to analyze (required)
|
|
113
|
+
brand_name: Your brand name (max 100 chars, required)
|
|
114
|
+
email: Email for notifications (required)
|
|
115
|
+
industry: Your industry/sector (max 200 chars, required)
|
|
116
|
+
location: Primary market location (max 200 chars, required)
|
|
117
|
+
persona: Target customer description (max 800 chars, required)
|
|
118
|
+
pain_points: Problems your product solves as comma-separated string (max 1000 chars, required)
|
|
119
|
+
brand_details: Your brand positioning (max 1200 chars, required)
|
|
120
|
+
direct_competitors: Competitor names as comma-separated string (max 500 chars, required)
|
|
121
|
+
tier: "basic" (1 credit, $25) or "pro" (2 credits, $50)
|
|
122
|
+
|
|
123
|
+
Optional kwargs:
|
|
124
|
+
indirect_competitors: Alternative solutions (max 500 chars)
|
|
125
|
+
keywords: Target keywords as comma-separated string (max 500 chars)
|
|
126
|
+
product: Product/service description (max 1000 chars)
|
|
127
|
+
business_scale: "small", "medium", or "large" (default: "medium")
|
|
128
|
+
webhook_url: URL to receive POST when report completes (HTTPS required)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Report creation response with report_id, token, and status
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
>>> result = client.signal(
|
|
135
|
+
... url="https://acme.com",
|
|
136
|
+
... brand_name="Acme Corp",
|
|
137
|
+
... email="you@acme.com",
|
|
138
|
+
... industry="B2B SaaS",
|
|
139
|
+
... location="United States",
|
|
140
|
+
... persona="CTOs at mid-market companies",
|
|
141
|
+
... pain_points="Integration challenges, lack of visibility",
|
|
142
|
+
... brand_details="Modern, dev-focused tooling",
|
|
143
|
+
... direct_competitors="Asana, Monday.com"
|
|
144
|
+
... )
|
|
145
|
+
>>> print(f"Report ID: {result['report_id']}")
|
|
146
|
+
>>> print(f"Token (save for Solutions): {result['token']}")
|
|
147
|
+
"""
|
|
148
|
+
payload = {
|
|
149
|
+
"url": url,
|
|
150
|
+
"brand_name": brand_name,
|
|
151
|
+
"email": email,
|
|
152
|
+
"industry": industry,
|
|
153
|
+
"location": location,
|
|
154
|
+
"persona": persona,
|
|
155
|
+
"pain_points": pain_points,
|
|
156
|
+
"brand_details": brand_details,
|
|
157
|
+
"direct_competitors": direct_competitors,
|
|
158
|
+
"tier": tier,
|
|
159
|
+
**kwargs
|
|
160
|
+
}
|
|
161
|
+
return self._post("/reports/signal", payload)
|
|
162
|
+
|
|
163
|
+
def scan(
|
|
164
|
+
self,
|
|
165
|
+
url: str,
|
|
166
|
+
brand_name: str,
|
|
167
|
+
email: str,
|
|
168
|
+
tier: str = "basic",
|
|
169
|
+
**kwargs
|
|
170
|
+
) -> Dict[str, Any]:
|
|
171
|
+
"""
|
|
172
|
+
Run an SEO Audit (Scan).
|
|
173
|
+
|
|
174
|
+
Comprehensive SEO analysis with prioritized recommendations.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
url: Website URL to audit (required)
|
|
178
|
+
brand_name: Your brand name (max 100 chars, required)
|
|
179
|
+
email: Email for notifications (required)
|
|
180
|
+
tier: "basic" (1 credit, $25) or "premium" (2 credits, $50)
|
|
181
|
+
|
|
182
|
+
Optional kwargs:
|
|
183
|
+
competitor_urls: List of competitor URLs to compare against
|
|
184
|
+
report_style: "executive", "technical", or "comprehensive" (default: "executive")
|
|
185
|
+
webhook_url: URL to receive POST when report completes (HTTPS required)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Report creation response with report_id and status
|
|
189
|
+
|
|
190
|
+
Example:
|
|
191
|
+
>>> result = client.scan(
|
|
192
|
+
... url="https://acme.com",
|
|
193
|
+
... brand_name="Acme Corp",
|
|
194
|
+
... email="you@acme.com",
|
|
195
|
+
... tier="premium",
|
|
196
|
+
... competitor_urls=["https://competitor1.com", "https://competitor2.com"]
|
|
197
|
+
... )
|
|
198
|
+
>>> print(f"Report ID: {result['report_id']}")
|
|
199
|
+
"""
|
|
200
|
+
payload = {
|
|
201
|
+
"url": url,
|
|
202
|
+
"brand_name": brand_name,
|
|
203
|
+
"email": email,
|
|
204
|
+
"tier": tier,
|
|
205
|
+
**kwargs
|
|
206
|
+
}
|
|
207
|
+
return self._post("/reports/scan", payload)
|
|
208
|
+
|
|
209
|
+
def solutions(
|
|
210
|
+
self,
|
|
211
|
+
email: str,
|
|
212
|
+
signal_token: str = None,
|
|
213
|
+
scan_token: str = None,
|
|
214
|
+
brand_name: str = None,
|
|
215
|
+
business_story: str = None,
|
|
216
|
+
decision: str = None,
|
|
217
|
+
success: str = None,
|
|
218
|
+
timeline: str = None,
|
|
219
|
+
scale_indicator: str = None,
|
|
220
|
+
**kwargs
|
|
221
|
+
) -> Dict[str, Any]:
|
|
222
|
+
"""
|
|
223
|
+
Run Strategic Advisory (Solutions).
|
|
224
|
+
|
|
225
|
+
Multi-AI strategic recommendations from 6 specialized agents.
|
|
226
|
+
Always uses Pro tier (2 credits, $50).
|
|
227
|
+
|
|
228
|
+
Three modes:
|
|
229
|
+
1. Signal Token Mode (recommended): Pass signal_token from a Signal report.
|
|
230
|
+
Solutions inherits context automatically. Optionally add scan_token.
|
|
231
|
+
2. Standalone Mode: Provide all business context fields.
|
|
232
|
+
3. Combined Mode: signal_token + scan_token for full context.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
email: Email for notifications (required)
|
|
236
|
+
signal_token: Token from a Signal report (Mode 1 - recommended)
|
|
237
|
+
scan_token: Token from a Scan report (optional, adds SEO context)
|
|
238
|
+
brand_name: Your brand name (max 100 chars, required for Mode 2)
|
|
239
|
+
business_story: About your business (max 2000 chars, required for Mode 2)
|
|
240
|
+
decision: Key challenge you're facing (max 1500 chars, required for Mode 2)
|
|
241
|
+
success: What success looks like (max 1000 chars, required for Mode 2)
|
|
242
|
+
timeline: Decision timeline (max 200 chars, required for Mode 2)
|
|
243
|
+
scale_indicator: Business scale indicator (max 100 chars, required for Mode 2)
|
|
244
|
+
|
|
245
|
+
Optional kwargs (for financial analysis):
|
|
246
|
+
include_financial: "yes" or "no" to include financial analysis
|
|
247
|
+
financial_context: Financial situation description (max 1000 chars)
|
|
248
|
+
monthly_revenue: Monthly revenue (max 50 chars)
|
|
249
|
+
monthly_costs: Monthly costs (max 50 chars)
|
|
250
|
+
cash_available: Available cash (max 50 chars)
|
|
251
|
+
webhook_url: URL to receive POST when report completes (HTTPS required)
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Report creation response with report_id and status
|
|
255
|
+
|
|
256
|
+
Example (Mode 1 - with Signal token):
|
|
257
|
+
>>> # First run Signal
|
|
258
|
+
>>> signal = client.signal(...)
|
|
259
|
+
>>> # Then run Solutions with the token
|
|
260
|
+
>>> result = client.solutions(
|
|
261
|
+
... email="you@acme.com",
|
|
262
|
+
... signal_token=signal["token"]
|
|
263
|
+
... )
|
|
264
|
+
|
|
265
|
+
Example (Mode 2 - standalone):
|
|
266
|
+
>>> result = client.solutions(
|
|
267
|
+
... email="you@acme.com",
|
|
268
|
+
... brand_name="Acme Corp",
|
|
269
|
+
... business_story="We're a B2B SaaS company in the project management space...",
|
|
270
|
+
... decision="Should we expand to enterprise market?",
|
|
271
|
+
... success="$10M ARR in 18 months",
|
|
272
|
+
... timeline="Q2 2025",
|
|
273
|
+
... scale_indicator="$2M ARR, 20 employees"
|
|
274
|
+
... )
|
|
275
|
+
"""
|
|
276
|
+
payload = {"email": email, **kwargs}
|
|
277
|
+
|
|
278
|
+
if signal_token:
|
|
279
|
+
payload["signal_token"] = signal_token
|
|
280
|
+
if scan_token:
|
|
281
|
+
payload["scan_token"] = scan_token
|
|
282
|
+
if brand_name:
|
|
283
|
+
payload["brand_name"] = brand_name
|
|
284
|
+
else:
|
|
285
|
+
# Standalone mode - all fields required
|
|
286
|
+
if not all([brand_name, business_story, decision, success, timeline, scale_indicator]):
|
|
287
|
+
raise ValidationError(
|
|
288
|
+
"Without signal_token, these fields are required: "
|
|
289
|
+
"brand_name, business_story, decision, success, timeline, scale_indicator"
|
|
290
|
+
)
|
|
291
|
+
payload.update({
|
|
292
|
+
"brand_name": brand_name,
|
|
293
|
+
"business_story": business_story,
|
|
294
|
+
"decision": decision,
|
|
295
|
+
"success": success,
|
|
296
|
+
"timeline": timeline,
|
|
297
|
+
"scale_indicator": scale_indicator,
|
|
298
|
+
})
|
|
299
|
+
if scan_token:
|
|
300
|
+
payload["scan_token"] = scan_token
|
|
301
|
+
|
|
302
|
+
return self._post("/reports/solutions", payload)
|
|
303
|
+
|
|
304
|
+
# =========================================================================
|
|
305
|
+
# Rerun Methods (minimal inputs - uses stored brand context)
|
|
306
|
+
# =========================================================================
|
|
307
|
+
|
|
308
|
+
def signal_rerun(
|
|
309
|
+
self,
|
|
310
|
+
brand_slug: str,
|
|
311
|
+
persona_slug: str,
|
|
312
|
+
email: str,
|
|
313
|
+
tier: str = "basic"
|
|
314
|
+
) -> Dict[str, Any]:
|
|
315
|
+
"""
|
|
316
|
+
Re-run a Signal report with minimal inputs.
|
|
317
|
+
|
|
318
|
+
Uses stored brand context - no need to re-enter all fields.
|
|
319
|
+
Ideal for automation (Zapier, Make, n8n) and dashboard "Run Again" flows.
|
|
320
|
+
|
|
321
|
+
Prerequisites:
|
|
322
|
+
- Brand must exist with populated brand_context
|
|
323
|
+
- Persona must be configured in brand_context.personas
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
brand_slug: Brand identifier (e.g., "acme_corp")
|
|
327
|
+
persona_slug: Persona identifier from brand settings (e.g., "cto-enterprise")
|
|
328
|
+
email: Email for notifications
|
|
329
|
+
tier: "basic" (1 credit) or "pro" (2 credits)
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Report creation response with report_id and status
|
|
333
|
+
|
|
334
|
+
Example:
|
|
335
|
+
>>> # After setting up brand and personas in dashboard
|
|
336
|
+
>>> result = client.signal_rerun(
|
|
337
|
+
... brand_slug="acme_corp",
|
|
338
|
+
... persona_slug="cto-enterprise",
|
|
339
|
+
... email="you@acme.com"
|
|
340
|
+
... )
|
|
341
|
+
>>> print(f"Report ID: {result['report_id']}")
|
|
342
|
+
|
|
343
|
+
Raises:
|
|
344
|
+
NotFoundError: If brand_slug or persona_slug not found
|
|
345
|
+
ValidationError: If brand_context is incomplete
|
|
346
|
+
"""
|
|
347
|
+
payload = {
|
|
348
|
+
"brand_slug": brand_slug,
|
|
349
|
+
"persona_slug": persona_slug,
|
|
350
|
+
"email": email,
|
|
351
|
+
"tier": tier,
|
|
352
|
+
}
|
|
353
|
+
return self._post("/reports/signal/rerun", payload)
|
|
354
|
+
|
|
355
|
+
def scan_rerun(
|
|
356
|
+
self,
|
|
357
|
+
brand_slug: str,
|
|
358
|
+
email: str,
|
|
359
|
+
tier: str = "basic"
|
|
360
|
+
) -> Dict[str, Any]:
|
|
361
|
+
"""
|
|
362
|
+
Re-run a Scan report with minimal inputs.
|
|
363
|
+
|
|
364
|
+
Uses stored brand context (website URL, competitor URLs).
|
|
365
|
+
Ideal for automation and scheduled SEO monitoring.
|
|
366
|
+
|
|
367
|
+
Prerequisites:
|
|
368
|
+
- Brand must exist with populated brand_context.website
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
brand_slug: Brand identifier (e.g., "acme_corp")
|
|
372
|
+
email: Email for notifications
|
|
373
|
+
tier: "basic" (1 credit) or "premium" (2 credits)
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Report creation response with report_id and status
|
|
377
|
+
|
|
378
|
+
Example:
|
|
379
|
+
>>> # After setting up brand in dashboard
|
|
380
|
+
>>> result = client.scan_rerun(
|
|
381
|
+
... brand_slug="acme_corp",
|
|
382
|
+
... email="you@acme.com",
|
|
383
|
+
... tier="premium"
|
|
384
|
+
... )
|
|
385
|
+
>>> print(f"Report ID: {result['report_id']}")
|
|
386
|
+
|
|
387
|
+
Raises:
|
|
388
|
+
NotFoundError: If brand_slug not found
|
|
389
|
+
ValidationError: If brand_context.website is missing
|
|
390
|
+
"""
|
|
391
|
+
payload = {
|
|
392
|
+
"brand_slug": brand_slug,
|
|
393
|
+
"email": email,
|
|
394
|
+
"tier": tier,
|
|
395
|
+
}
|
|
396
|
+
return self._post("/reports/scan/rerun", payload)
|
|
397
|
+
|
|
398
|
+
# =========================================================================
|
|
399
|
+
# Report Status & Listing
|
|
400
|
+
# =========================================================================
|
|
401
|
+
|
|
402
|
+
def get_report(self, report_id: str) -> Dict[str, Any]:
|
|
403
|
+
"""
|
|
404
|
+
Get report status and results.
|
|
405
|
+
|
|
406
|
+
Poll this endpoint to check if your report is ready.
|
|
407
|
+
When status is "completed", download URLs will be included.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
report_id: The report ID returned from signal(), scan(), or solutions()
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Report status with download URLs when completed:
|
|
414
|
+
- status: "queued", "processing", "completed", or "failed"
|
|
415
|
+
- download_url: Signed PDF URL (expires in 15 minutes)
|
|
416
|
+
- pptx_download_url: Signed PPTX URL (Pro/Premium tiers)
|
|
417
|
+
- intelligence_download_url: Signed JSON URL with full data
|
|
418
|
+
|
|
419
|
+
Example:
|
|
420
|
+
>>> report = client.get_report("rpt_abc123")
|
|
421
|
+
>>> if report["status"] == "completed":
|
|
422
|
+
... print(f"PDF: {report['download_url']}")
|
|
423
|
+
... print(f"JSON: {report['intelligence_download_url']}")
|
|
424
|
+
"""
|
|
425
|
+
return self._get(f"/reports/{report_id}")
|
|
426
|
+
|
|
427
|
+
def list_reports(
|
|
428
|
+
self,
|
|
429
|
+
page: int = 1,
|
|
430
|
+
page_size: int = 50
|
|
431
|
+
) -> Dict[str, Any]:
|
|
432
|
+
"""
|
|
433
|
+
List all reports for your organization.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
page: Page number (1-indexed)
|
|
437
|
+
page_size: Reports per page (max 100)
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Paginated list of reports with download URLs for completed reports
|
|
441
|
+
|
|
442
|
+
Example:
|
|
443
|
+
>>> result = client.list_reports(page=1, page_size=10)
|
|
444
|
+
>>> for report in result["reports"]:
|
|
445
|
+
... print(f"{report['report_id']}: {report['status']}")
|
|
446
|
+
"""
|
|
447
|
+
return self._get(f"/reports?page={page}&page_size={page_size}")
|
|
448
|
+
|
|
449
|
+
def wait_for_report(
|
|
450
|
+
self,
|
|
451
|
+
report_id: str,
|
|
452
|
+
timeout_minutes: int = 20,
|
|
453
|
+
poll_interval: int = 30
|
|
454
|
+
) -> Dict[str, Any]:
|
|
455
|
+
"""
|
|
456
|
+
Wait for a report to complete.
|
|
457
|
+
|
|
458
|
+
Polls the report status until it's completed, failed, or timeout.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
report_id: The report ID to wait for
|
|
462
|
+
timeout_minutes: Maximum time to wait (default 20 minutes)
|
|
463
|
+
poll_interval: Seconds between status checks (default 30)
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Completed report with download URLs
|
|
467
|
+
|
|
468
|
+
Raises:
|
|
469
|
+
SurmadoError: If report fails or times out
|
|
470
|
+
|
|
471
|
+
Example:
|
|
472
|
+
>>> result = client.signal(...)
|
|
473
|
+
>>> completed = client.wait_for_report(result["report_id"])
|
|
474
|
+
>>> print(f"PDF ready: {completed['download_url']}")
|
|
475
|
+
"""
|
|
476
|
+
start = time.time()
|
|
477
|
+
timeout_seconds = timeout_minutes * 60
|
|
478
|
+
|
|
479
|
+
while time.time() - start < timeout_seconds:
|
|
480
|
+
report = self.get_report(report_id)
|
|
481
|
+
status = report.get("status")
|
|
482
|
+
|
|
483
|
+
if status == "completed":
|
|
484
|
+
return report
|
|
485
|
+
elif status == "failed":
|
|
486
|
+
error_msg = report.get("error") or "Report processing failed"
|
|
487
|
+
raise SurmadoError(error_msg, response=report)
|
|
488
|
+
elif status == "cancelled":
|
|
489
|
+
raise SurmadoError("Report was cancelled", response=report)
|
|
490
|
+
|
|
491
|
+
time.sleep(poll_interval)
|
|
492
|
+
|
|
493
|
+
raise SurmadoError(
|
|
494
|
+
f"Report did not complete within {timeout_minutes} minutes",
|
|
495
|
+
response={"report_id": report_id, "status": "timeout"}
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# =========================================================================
|
|
499
|
+
# Internal HTTP Methods
|
|
500
|
+
# =========================================================================
|
|
501
|
+
|
|
502
|
+
def _headers(self) -> Dict[str, str]:
|
|
503
|
+
"""Build request headers."""
|
|
504
|
+
return {
|
|
505
|
+
"X-API-Key": self.api_key,
|
|
506
|
+
"Content-Type": "application/json",
|
|
507
|
+
"User-Agent": f"surmado-python/{__version__}",
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
def _post(self, endpoint: str, data: Dict) -> Dict[str, Any]:
|
|
511
|
+
"""Make a POST request."""
|
|
512
|
+
response = requests.post(
|
|
513
|
+
f"{self.base_url}{endpoint}",
|
|
514
|
+
json=data,
|
|
515
|
+
headers=self._headers(),
|
|
516
|
+
timeout=self.timeout
|
|
517
|
+
)
|
|
518
|
+
return self._handle_response(response)
|
|
519
|
+
|
|
520
|
+
def _get(self, endpoint: str) -> Dict[str, Any]:
|
|
521
|
+
"""Make a GET request."""
|
|
522
|
+
response = requests.get(
|
|
523
|
+
f"{self.base_url}{endpoint}",
|
|
524
|
+
headers=self._headers(),
|
|
525
|
+
timeout=self.timeout
|
|
526
|
+
)
|
|
527
|
+
return self._handle_response(response)
|
|
528
|
+
|
|
529
|
+
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
|
|
530
|
+
"""Handle API response and raise appropriate errors."""
|
|
531
|
+
try:
|
|
532
|
+
data = response.json()
|
|
533
|
+
except ValueError:
|
|
534
|
+
data = {"error": response.text}
|
|
535
|
+
|
|
536
|
+
if response.status_code == 401:
|
|
537
|
+
raise AuthenticationError(
|
|
538
|
+
"Invalid or missing API key",
|
|
539
|
+
status_code=401,
|
|
540
|
+
response=data
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
if response.status_code == 402:
|
|
544
|
+
raise InsufficientCreditsError(
|
|
545
|
+
data.get("message") or "Insufficient credits",
|
|
546
|
+
status_code=402,
|
|
547
|
+
response=data
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if response.status_code == 404:
|
|
551
|
+
raise NotFoundError(
|
|
552
|
+
data.get("error") or "Resource not found",
|
|
553
|
+
status_code=404,
|
|
554
|
+
response=data
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
if response.status_code == 422:
|
|
558
|
+
raise ValidationError(
|
|
559
|
+
data.get("detail") or data.get("error") or "Invalid request data",
|
|
560
|
+
status_code=422,
|
|
561
|
+
response=data
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
if response.status_code >= 400:
|
|
565
|
+
raise SurmadoError(
|
|
566
|
+
data.get("error") or f"API error: {response.status_code}",
|
|
567
|
+
status_code=response.status_code,
|
|
568
|
+
response=data
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
return data
|