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 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
@@ -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