Surmado API - Developer Guide
30 min read
TL;DR
The Surmado API lets you create Surmado Signal, Surmado Scan, and Surmado Solutions reports programmatically. Simplest approach: send brand_name directly to any endpoint. We auto-create brand_slug for you. Advanced users can manage brands separately. Authenticate with Bearer tokens, get webhooks when reports complete, white-label at no extra cost.
Surmado API - Developer Guide
Complete API reference for building AI visibility, competitive intelligence, and strategic advisory into your application.
This guide is for developers integrating with the Surmado API using code (Python, Node.js, Ruby, etc.).
Looking for no-code integrations? See NOVICE_GUIDE.md for Zapier, Make, and n8n tutorials.
Table of Contents
- Quick Start
- Authentication
- Brand Management
- Products
- Report Management
- API Key Management
- Webhooks
- Error Handling
- Code Examples
Quick Start
1. Get Your API Key
- Visit app.surmado.com
- Navigate to Settings → API Keys
- Click Create New API Key
- Save the key securely (shown only once!)
Your key format: sur_live_H2gPVFuEPytcw2EKQ79Z...
2. Create Your First Report
Simple Approach: Just send the company name - we’ll handle the rest!
curl -X POST https://api.surmado.com/v1/reports/signal \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_name": "Acme Corp",
"tier": "pro",
"email": "you@company.com",
"industry": "Technology",
"business_scale": "medium",
"location": "United States",
"product": "CRM software",
"persona": "Small business owners",
"pain_points": "Losing track of customer conversations",
"brand_details": "Simple CRM with email integration",
"direct_competitors": "HubSpot, Pipedrive",
"keywords": "CRM, sales tracking"
}'
Response:
{
"report_id": "rpt_abc123xyz",
"product": "signal",
"status": "queued",
"brand_slug": "acme_corp",
"brand_name": "Acme Corp",
"brand_created": true,
"note": "brand_slug created - prevents duplicates from name variations",
"created_at": "2025-11-15T22:35:00Z"
}
What happened:
- We auto-created
brand_slug: "acme_corp"from yourbrand_name brand_created: truetells you this was a new brand- Save the
brand_slugfor faster future requests!
3. Check Report Status
curl https://api.surmado.com/v1/reports/rpt_abc123xyz \
-H "Authorization: Bearer YOUR_API_KEY"
Response (when complete):
{
"id": "rpt_abc123xyz",
"brand_slug": "acme_corp",
"brand_name": "Acme Corp",
"product": "signal",
"status": "completed",
"download_url": "https://storage.googleapis.com/...",
"pptx_url": "https://storage.googleapis.com/...",
"token": "SIG-2025-11-A1B2C",
"created_at": "2025-11-15T22:35:00Z",
"completed_at": "2025-11-15T22:40:00Z"
}
Base URLs
Production (Recommended):
https://api.surmado.com
Direct Cloud Run:
https://surmado-api-tgfqeqkcgq-uc.a.run.app
Note: The API is publicly accessible. All security is handled at the application layer.
Authentication
All API requests require an API key in the Authorization header:
Authorization: Bearer sur_live_YOUR_API_KEY
Authentication Modes
Live Keys (Production):
Authorization: Bearer sur_live_H2gPVFuEPytcw2EKQ79Z...
- Real reports generated
- Credits deducted
- Use in production
Test Keys (Development):
Authorization: Bearer sur_test_fake_key_for_testing
- Mock responses
- No credits deducted
- No PDFs generated
Server Keys (Internal use only):
Authorization: Bearer sur_server_...
X-Org-Id: org_YOUR_ORG_ID
- For Netlify functions and backend integrations
- Requires organization context
Security Model
The API uses application-level authentication:
- FastAPI Middleware validates every request
- API Key Validation checks bcrypt-hashed keys against Firestore
- Organization Authorization ensures resource access is scoped to authenticated org
Invalid credentials are rejected with 401 Unauthorized before reaching business logic.
Brand Management
Surmado uses a hybrid brand management system - you choose the approach that fits your workflow:
Approach A: Simple (Recommended)
Just send brand_name directly to any report endpoint - we auto-create the brand!
curl -X POST https://api.surmado.com/v1/reports/signal \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"brand_name": "Acme Corp",
"tier": "pro",
...
}'
Response includes:
brand_slug: “acme_corp” (save this!)brand_created: true/falsenote: Explanation of why brand_slug was created
Perfect for: Quick integrations, automation tools, simple workflows
Approach B: Explicit
Create brand first, then use brand_slug for all reports:
# Step 1: Create brand
curl -X POST https://api.surmado.com/v1/brands/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"brand_name": "Acme Corp"}'
# Step 2: Use brand_slug
curl -X POST https://api.surmado.com/v1/reports/signal \
-d '{"brand_slug": "acme_corp", "tier": "pro", ...}'
Perfect for: Advanced workflows, brand management systems, analytics
Why brand_slug?
Prevents duplicate brands when team members use different spellings:
"Acme"→acme_corp"ACME Corp"→acme_corp"Acme Corporation"→acme_corp
All map to the same brand, keeping your reports organized.
Create Brand (Advanced)
Endpoint:
POST /v1/brands/
⚠️ Important: Include the trailing slash. Without it, you’ll get a 307 redirect.
Request:
{
"brand_name": "Veterans Moving America"
}
Response (201 Created):
{
"brand_slug": "veterans_moving_america",
"brand_name": "Veterans Moving America",
"created_at": "2025-11-15T10:30:00Z"
}
Brand Slug Rules:
- Auto-generated from
brand_name - Lowercase with underscores
- Example: “Acme Corp” →
acme_corp - Max 64 characters
- Unique per organization
Error: Brand Exists (409):
{
"detail": {
"error": "brand_exists",
"brand_slug": "veterans_moving_america",
"message": "Brand 'Veterans Moving America' already exists. Please select it from your brands list."
}
}
Example:
curl -X POST https://api.surmado.com/v1/brands/ \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"brand_name": "Veterans Moving America"}'
List Brands
Endpoint:
GET /v1/brands/
⚠️ Important: Include the trailing slash.
Response (200 OK):
{
"brands": [
{
"brand_slug": "veterans_moving_america",
"brand_name": "Veterans Moving America",
"created_at": "2025-11-15T10:30:00Z"
},
{
"brand_slug": "acme_corp",
"brand_name": "Acme Corp",
"created_at": "2025-11-14T09:15:00Z"
}
]
}
Brands are sorted by creation date (newest first).
Example:
curl https://api.surmado.com/v1/brands/ \
-H "Authorization: Bearer sur_live_YOUR_KEY"
Brand Workflow Pattern
Recommended integration flow:
import requests
API_KEY = "sur_live_YOUR_KEY"
API_URL = "https://api.surmado.com"
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
# Step 1: Try to create brand (or get existing)
try:
response = requests.post(
f"{API_URL}/v1/brands/",
headers=headers,
json={"brand_name": "Acme Corp"}
)
if response.status_code == 201:
brand_slug = response.json()["brand_slug"]
elif response.status_code == 409:
# Brand exists - extract slug from error
brand_slug = response.json()["detail"]["brand_slug"]
except Exception as e:
print(f"Error: {e}")
# Step 2: Use brand_slug for reports
report_response = requests.post(
f"{API_URL}/v1/reports/signal",
headers=headers,
json={
"brand_slug": brand_slug,
"tier": "pro",
# ... other fields
}
)
Profile Management (Advanced)
Profiles store persistent brand context and preferences across reports. Use these endpoints for the wizard flow or to manage brand settings programmatically.
Update Brand Context
Endpoint:
PUT /v1/profiles/{brand_slug}/brand-context
Request:
{
"brand_name": "Acme Corp",
"brand_story": "Why customers choose us - we simplify complex workflows",
"personas": {
"enterprise-cto": {
"slug": "enterprise-cto",
"label": "Enterprise CTO",
"persona": "Technical leaders at Fortune 500 companies",
"pain_points": "Integration complexity, vendor lock-in",
"location": "United States",
"competitors": ["Salesforce", "SAP"],
"alternatives": "Build in-house, spreadsheets"
}
},
"competitors": ["HubSpot", "Salesforce"],
"alternative_solutions": "Spreadsheets, manual tracking"
}
Merge/Clear Semantics:
- Fields not included → unchanged
nullor""→ clears the field- Value provided → overwrites
Response (200 OK):
{
"brand_slug": "acme_corp",
"updated_fields": ["brand_story", "personas", "competitors"]
}
Update Preferences
Endpoint:
PUT /v1/profiles/{brand_slug}/preferences
Request:
{
"tracked_metrics": [
{"metric": "signal_visibility", "alert_threshold": -10, "priority": "high"},
{"metric": "scan_seo", "alert_threshold": -5, "priority": "medium"}
],
"notification_email": "alerts@acme.com",
"monitor": {
"auto_run_enabled": true,
"cadence": "weekly",
"day_of_week": "monday",
"time_utc": "09:00",
"persona_slots": {
"persona_a": "enterprise-cto"
}
}
}
Response (200 OK):
{
"brand_slug": "acme_corp",
"updated_fields": ["tracked_metrics", "notification_email", "monitor"]
}
Get Profile
Endpoint:
GET /v1/profiles/{brand_slug}
Response (200 OK):
{
"brand_slug": "acme_corp",
"brand_name": "Acme Corp",
"org_id": "org_xyz",
"brand_context": {
"brand_story": "...",
"personas": {...},
"competitors": [...]
},
"preferences": {
"tracked_metrics": [...],
"monitor": {...}
},
"current": {
"signal_visibility": 72,
"scan_seo": 85
}
}
Products
Surmado offers three intelligence products via API:
| Product | What It Does | Credits | Time |
|---|---|---|---|
| Signal | AI visibility analysis | 2 | ~5 min |
| Pulse | Website competitive analysis | 1-2 | ~3 min |
| Solutions | Strategic advisory board | 2 | ~10 min |
Signal API
Analyze how your brand appears in AI-powered search (ChatGPT, Perplexity, Claude, etc.).
Endpoint
POST /v1/reports/signal
Tiers
| Tier | Credits | Features |
|---|---|---|
basic | 1 | Essential visibility analysis, PDF |
pro | 2 | Everything + PowerPoint + deeper insights |
Pro Tier Benefits:
- ✅ Comprehensive competitive analysis
- ✅ PowerPoint file automatically generated
- ✅ Delivered via webhook with
pptx_url
Request Schema
Required Fields:
| Field | Type | Description | Example |
|---|---|---|---|
brand_name OR brand_slug | string | Brand identifier (we auto-create slug from name) | "Acme Corp" |
tier | string | "basic" or "pro" | "basic" |
email | string | Contact email | "cto@acme.com" |
industry | string | Industry/sector | "B2B SaaS" |
business_scale | string | Business scale | "small" | "medium" | "large" |
location | string | Primary location | "San Francisco" |
persona | string | Target customer/audience | "CTOs at mid-market companies" |
pain_points | string | Pain points you address | "Integration challenges" |
brand_details | string | Brief brand/product description | "AI-powered CRM with email integration" |
direct_competitors | string | Competitors (comma-separated) | "HubSpot, Pipedrive, Salesforce" |
Optional Fields:
| Field | Type | Tier Availability | Description |
|---|---|---|---|
product | string | Both | Product/service description (enhances analysis) |
indirect_competitors | string | Both | Indirect competitors (comma-separated) |
keywords | string | Both | Keywords to analyze (comma-separated) |
competitor_urls | array | Both | Array of competitor URLs (more detailed in Pro) |
webhook_url | string (URL) | Both | Webhook for completion notification |
generate_pptx | boolean | Pro only | Generate PowerPoint presentation (default: true) |
is_agency_white_label | boolean | Both | Enable white-label mode |
agency_name | string | Both | Agency name (required if white-label enabled) |
💡 Tier Differences:
- Basic (1 credit): 20-page PDF report, essential visibility analysis
- Pro (2 credits): 40-page PDF + PowerPoint (
.pptx), comprehensive competitive analysis, deeper insights
Complete JSON Structure:
{
"brand_name": "string", // REQUIRED (OR brand_slug)
"tier": "basic | pro", // REQUIRED
"email": "string (email)", // REQUIRED
"industry": "string", // REQUIRED
"business_scale": "string", // REQUIRED
"location": "string", // REQUIRED
"persona": "string", // REQUIRED
"pain_points": "string", // REQUIRED
"product": "string", // Optional
"brand_details": "string", // REQUIRED - brief brand/product description
"direct_competitors": "string", // REQUIRED - comma-separated competitors
"indirect_competitors": "string", // Optional
"keywords": "string", // Optional
"competitor_urls": ["string"], // Optional
"webhook_url": "string (URL)", // Optional
"generate_pptx": true, // Optional
"is_agency_white_label": false, // Optional
"agency_name": "string" // Optional
}
Example: Signal Pro
curl -X POST https://api.surmado.com/v1/reports/signal \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_slug": "acme_corp",
"tier": "pro",
"email": "founder@acme.com",
"industry": "B2B SaaS",
"business_scale": "large",
"location": "United States",
"product": "CRM software for small businesses",
"persona": "Small business owners who need better customer tracking",
"pain_points": "Losing track of customer conversations and follow-ups",
"brand_details": "Simple CRM with email integration and mobile app. 4.8 stars on G2.",
"direct_competitors": "HubSpot, Pipedrive, Salesforce Essentials",
"keywords": "CRM, customer relationship management, sales tracking",
"webhook_url": "https://your-app.com/webhooks/surmado"
}'
White-Label Mode
Agencies can rebrand reports with their own name:
{
"brand_slug": "client_company",
"tier": "pro",
...
"is_agency_white_label": true,
"agency_name": "Your Agency Name"
}
When enabled:
- Report cover shows agency name instead of “SIGNAL”
- All “Surmado” mentions replaced
- Cross-product promotions removed
- Contact information removed
Response
{
"id": "rpt_abc123xyz",
"token": "",
"product": "signal",
"status": "queued",
"created_at": "2025-11-15T22:35:00Z"
}
Status Lifecycle:
queued- Report accepted, queued for processingprocessing- AI analysis in progresscompleted- Report ready, webhook sentfailed- Error occurred, credits refunded
Pulse API
Website competitive analysis with SEO, content, and technical insights.
Endpoint
POST /v1/reports/pulse
Tiers
| Tier | Credits | Pages | Features |
|---|---|---|---|
basic | 1 | 30 | Performance, SEO, accessibility |
premium | 2 | 100 | Everything + competitor analysis |
Request Schema
Required Fields:
| Field | Type | Description | Example |
|---|---|---|---|
brand_name OR brand_slug | string | Brand identifier | "Acme Corp" |
url | string (URL) | Website to audit | "https://acme.com" |
email | string (email) | Contact email | "marketing@acme.com" |
tier | string | "basic" or "premium" | "basic" |
Optional Fields:
| Field | Type | Tier Availability | Description | Default |
|---|---|---|---|---|
report_style | string | Both | "executive" | "technical" | "comprehensive" | "executive" |
competitor_urls | array | Premium only | Competitor URLs to compare against | [] |
webhook_url | string (URL) | Both | Webhook for completion notification | - |
is_agency_white_label | boolean | Both | Enable white-label mode | false |
agency_name | string | Both | Agency name (required if white-label enabled) | - |
💡 Tier Differences:
- Basic (1 credit): 30-page report, performance/SEO/accessibility analysis
- Premium (2 credits): 100-page report, everything in Basic + competitor analysis + deeper insights
Complete JSON Structure:
{
"brand_name": "string", // REQUIRED (OR brand_slug)
"url": "string (URL)", // REQUIRED
"email": "string (email)", // REQUIRED
"tier": "basic | premium", // REQUIRED
"report_style": "string", // Optional (default: "executive")
"competitor_urls": ["string"], // Optional
"webhook_url": "string (URL)", // Optional
"is_agency_white_label": false, // Optional
"agency_name": "string" // Optional
}
Example: Pulse Premium
curl -X POST https://api.surmado.com/v1/reports/pulse \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_slug": "acme_corp",
"tier": "premium",
"url": "https://www.acme.com",
"email": "founder@acme.com",
"report_style": "executive",
"competitor_urls": [
"https://competitor1.com",
"https://competitor2.com"
],
"webhook_url": "https://your-app.com/webhooks/pulse"
}'
Response
{
"id": "rpt_xyz789abc",
"token": "SCAN-2025-11-XXXXX",
"product": "scan",
"status": "queued",
"created_at": "2025-11-15T01:00:00Z"
}
Solutions API
Strategic advisory reports with AI board of directors (CMO, CTO, Product Lead, Board Chair).
Endpoint
POST /v1/reports/solutions
Credits
2 credits (flat rate)
Two Modes of Operation
Solutions supports two modes:
| Mode | When to Use | Required Fields |
|---|---|---|
| Signal Token Mode | When you’ve already run Signal and want strategic recommendations based on that data | email + signal_token only |
| Standalone Mode | Fresh strategic advisory without prior Signal analysis | All fields (business_story, decision, etc.) |
Signal Token Mode is the recommended approach for the best strategic recommendations, as Solutions gets full context from your Signal report’s AI visibility analysis.
Request Schema
Required Fields (Standalone Mode):
| Field | Type | Description | Example |
|---|---|---|---|
brand_name OR brand_slug | string | Brand identifier | "Acme Corp" |
email | string (email) | Contact email | "founder@acme.com" |
business_story | string | Tell us about your business | "B2B SaaS CRM, 50 employees, $3M ARR" |
decision | string | Key decision or challenge | "Should we build a mobile app?" |
success | string | What does success look like? | "40% engagement increase in 6 months" |
timeline | string | Timeline for decision | "Need decision in next 30 days" |
scale_indicator | string | Business scale/size | "regional" or "$3M ARR" |
Required Fields (Signal Token Mode):
| Field | Type | Description | Example |
|---|---|---|---|
email | string (email) | Contact email | "founder@acme.com" |
signal_token | string | Token from completed Signal report | "SIG-2025-11-XXXXX" |
Note: In Signal Token Mode,
brand_slug/brand_nameare inherited from the Signal report. All other fields are optional.
Optional Fields:
| Field | Type | Description |
|---|---|---|
signal_token | string | Signal report token for enhanced analysis |
include_financial | string | Include financial analysis? ("yes" or "no") |
financial_context | string | Financial context/situation |
monthly_revenue | string | Monthly revenue |
monthly_costs | string | Monthly costs |
cash_available | string | Cash available |
webhook_url | string (URL) | Webhook for completion notification |
is_agency_white_label | boolean | Enable white-label mode |
agency_name | string | Agency name (required if white-label enabled) |
Complete JSON Structure:
{
"brand_name": "string", // REQUIRED (OR brand_slug)
"email": "string (email)", // REQUIRED
"business_story": "string", // REQUIRED
"decision": "string", // REQUIRED
"success": "string", // REQUIRED
"timeline": "string", // REQUIRED
"scale_indicator": "string", // REQUIRED
"signal_token": "string", // Optional
"include_financial": "string", // Optional
"financial_context": "string", // Optional
"monthly_revenue": "string", // Optional
"monthly_costs": "string", // Optional
"cash_available": "string", // Optional
"webhook_url": "string (URL)", // Optional
"is_agency_white_label": false, // Optional
"agency_name": "string" // Optional
}
Example: Solutions (Standalone)
curl -X POST https://api.surmado.com/v1/reports/solutions \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_name": "Acme Corp",
"email": "founder@acme.com",
"business_story": "B2B SaaS CRM, 50 employees, $3M ARR, Austin TX",
"decision": "Should we build a mobile app or improve web experience?",
"success": "40% engagement increase in 6 months without major budget increase",
"timeline": "Need decision in next 30 days",
"scale_indicator": "regional"
}'
Example: Solutions (With Signal Data)
curl -X POST https://api.surmado.com/v1/reports/solutions \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_slug": "acme_corp",
"email": "founder@acme.com",
"name": "John Smith",
"business_story": "We ran Signal and want strategic recommendations",
"decision": "How do we improve our AI visibility rankings?",
"success": "Beat our competitors in AI search results",
"timeline": "Q1 2025",
"scale_indicator": "national",
"signal_token": "SIG-2025-11-XXXXX"
}'
Response
{
"id": "rpt_def456ghi",
"token": "SOL-2025-11-XXXXX",
"product": "strategy",
"status": "queued",
"created_at": "2025-11-15T01:00:00Z"
}
Complete Bundle API
“One click, full visibility analysis.”
Run all three products (Scan + Signal + Solutions) with a single API call. Perfect for onboarding or comprehensive brand analysis.
Endpoint
POST /v1/reports/bundle
Credits
6 credits total:
- Scan Pro: 2 credits
- Signal Pro: 2 credits
- Solutions Pro: 2 credits
Flow
- Atomic credit deduction (6 credits upfront)
- Scan + Signal queued in parallel (both start immediately)
- Solutions auto-triggered when Signal completes (uses Signal intelligence)
Request Schema
Required Fields:
| Field | Type | Description | Example |
|---|---|---|---|
brand_name OR brand_slug | string | Brand identifier | "Acme Corp" |
email | string | Contact email | "founder@acme.com" |
url | string (URL) | Brand website | "https://acme.com" |
industry | string | Industry/sector | "B2B SaaS" |
location | string | Primary location | "United States" |
persona | string | Target customer | "Small business owners" |
pain_points | string | Problems you solve | "Losing track of customers" |
Optional Fields:
| Field | Type | Description |
|---|---|---|
business_scale | string | "small" | "medium" | "large" |
product | string | Product/service description |
brand_details | string | Brand positioning details |
direct_competitors | string | Competitors (comma-separated) |
competitor_urls | array | Competitor URLs for Scan analysis |
webhook_url | string (URL) | Webhook for completion notifications |
is_agency_white_label | boolean | Enable white-label mode |
agency_name | string | Agency name for white-label |
Example: Complete Bundle
curl -X POST https://api.surmado.com/v1/reports/bundle \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_name": "Acme Corp",
"email": "founder@acme.com",
"url": "https://acme.com",
"industry": "B2B SaaS",
"business_scale": "large",
"location": "United States",
"product": "CRM software for small businesses",
"persona": "Small business owners who need better customer tracking",
"pain_points": "Losing track of customer conversations and follow-ups",
"brand_details": "Simple CRM with email integration and mobile app",
"direct_competitors": "HubSpot, Pipedrive, Salesforce Essentials",
"competitor_urls": ["https://hubspot.com", "https://pipedrive.com"],
"webhook_url": "https://your-app.com/webhooks/surmado"
}'
Response
{
"bundle_id": "bnd_abc123def456",
"bundle_type": "complete",
"credits_charged": 6,
"credits_remaining": 94,
"reports": {
"scan": {
"report_id": "rpt_scan_xyz789",
"status": "queued"
},
"signal": {
"report_id": "rpt_signal_abc123",
"status": "queued"
},
"solutions": {
"report_id": "rpt_solutions_def456",
"status": "waiting_on_signal"
}
}
}
Status Lifecycle:
- Scan:
queued→processing→completed - Signal:
queued→processing→completed - Solutions:
waiting_on_signal→queued→processing→completed
Solutions transitions to queued automatically when Signal completes, inheriting the Signal intelligence for strategic recommendations.
Signal → Solutions Chaining
“Where you stand in AI’s eyes → how to fix it.”
The most powerful workflow is running Signal first (AI visibility analysis), then Solutions (strategic recommendations). Solutions uses the Signal intelligence to provide highly contextual strategic advice.
Two Methods
| Method | Credits | Use Case |
|---|---|---|
| Manual Chain (2 API calls) | 2 + 2 = 4 | Full control, custom timing |
| Combo Endpoint (1 API call) | 4 | Automated, one-click |
Method 1: Manual Chain (Signal Token Mode)
Step 1: Run Signal Report
curl -X POST https://api.surmado.com/v1/reports/signal \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_name": "Acme Corp",
"tier": "pro",
"email": "founder@acme.com",
"industry": "B2B SaaS",
"business_scale": "large",
"location": "United States",
"product": "CRM software",
"persona": "Small business owners",
"pain_points": "Losing track of customers",
"brand_details": "Simple CRM with email integration",
"direct_competitors": "HubSpot, Pipedrive",
"webhook_url": "https://your-app.com/webhooks"
}'
Response:
{
"report_id": "rpt_signal_abc123",
"product": "signal",
"status": "queued",
"brand_slug": "acme_corp"
}
Step 2: Wait for Signal to complete (via webhook or polling)
When complete, the Signal report will have a token like SIG-2025-11-XXXXX.
Step 3: Run Solutions with Signal Token
curl -X POST https://api.surmado.com/v1/reports/solutions \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "founder@acme.com",
"signal_token": "SIG-2025-11-XXXXX",
"webhook_url": "https://your-app.com/webhooks"
}'
That’s it! Only email and signal_token are required. Solutions inherits:
brand_slugandbrand_namefrom the Signal report- Full AI visibility intelligence for strategic recommendations
Response:
{
"report_id": "rpt_solutions_def456",
"product": "solutions",
"status": "queued",
"brand_slug": "acme_corp"
}
Method 2: Combo Endpoint (Automatic Chain)
Single API call that creates both reports and chains them automatically.
curl -X POST https://api.surmado.com/v1/reports/combo/signal-solutions \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_name": "Acme Corp",
"tier": "pro",
"email": "founder@acme.com",
"industry": "B2B SaaS",
"business_scale": "large",
"location": "United States",
"product": "CRM software",
"persona": "Small business owners",
"pain_points": "Losing track of customers",
"brand_details": "Simple CRM with email integration",
"direct_competitors": "HubSpot, Pipedrive",
"webhook_url": "https://your-app.com/webhooks"
}'
Response:
{
"combo_type": "signal-solutions",
"signal_report_id": "rpt_signal_abc123",
"solutions_report_id": "rpt_solutions_def456",
"credits_charged": 4
}
Flow:
- API deducts 4 credits upfront
- Creates both reports atomically
- Queues Signal for processing
- When Signal completes, Solutions is auto-triggered with Signal intelligence
- Both webhooks fire when each report completes
Solutions report starts in waiting_on_signal status, then transitions to queued → processing → completed after Signal finishes.
Choosing Between Methods
| Scenario | Recommended Method |
|---|---|
| Automated pipeline, no user interaction | Combo Endpoint |
| User wants to review Signal before running Solutions | Manual Chain |
| Building a “one-click full analysis” button | Combo Endpoint |
| User already has a Signal report they want to extend | Manual Chain |
| Webform with two-step wizard | Manual Chain |
Python Example: Manual Chain
import requests
import time
API_KEY = "sur_live_YOUR_KEY"
API_URL = "https://api.surmado.com"
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
# Step 1: Create Signal report
signal_response = requests.post(
f"{API_URL}/v1/reports/signal",
headers=headers,
json={
"brand_name": "Acme Corp",
"tier": "pro",
"email": "founder@acme.com",
"industry": "B2B SaaS",
"business_scale": "large",
"location": "United States",
"product": "CRM software",
"persona": "Small business owners",
"pain_points": "Losing track of customers",
"brand_details": "Simple CRM with email integration",
"direct_competitors": "HubSpot, Pipedrive"
}
)
signal_report_id = signal_response.json()["report_id"]
print(f"Signal report created: {signal_report_id}")
# Step 2: Poll for Signal completion (or use webhook in production)
while True:
status_response = requests.get(
f"{API_URL}/v1/reports/{signal_report_id}",
headers=headers
)
status_data = status_response.json()
if status_data["status"] == "completed":
signal_token = status_data["token"]
print(f"Signal completed! Token: {signal_token}")
break
elif status_data["status"] == "failed":
print(f"Signal failed: {status_data.get('error')}")
exit(1)
print(f"Signal status: {status_data['status']}...")
time.sleep(30)
# Step 3: Create Solutions report with Signal token
solutions_response = requests.post(
f"{API_URL}/v1/reports/solutions",
headers=headers,
json={
"email": "founder@acme.com",
"signal_token": signal_token
}
)
solutions_report_id = solutions_response.json()["report_id"]
print(f"Solutions report created: {solutions_report_id}")
Node.js Example: Combo Endpoint
const axios = require('axios');
const API_KEY = 'sur_live_YOUR_KEY';
const API_URL = 'https://api.surmado.com';
async function runSignalSolutionsCombo() {
const response = await axios.post(
`${API_URL}/v1/reports/combo/signal-solutions`,
{
brand_name: 'Acme Corp',
tier: 'pro',
email: 'founder@acme.com',
industry: 'B2B SaaS',
business_scale: 'large',
location: 'United States',
product: 'CRM software',
persona: 'Small business owners',
pain_points: 'Losing track of customers',
brand_details: 'Simple CRM with email integration',
direct_competitors: 'HubSpot, Pipedrive',
webhook_url: 'https://your-app.com/webhooks'
},
{
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
}
}
);
console.log('Combo created:', response.data);
// { combo_type: 'signal-solutions', signal_report_id: '...', solutions_report_id: '...', credits_charged: 4 }
}
runSignalSolutionsCombo();
Report Management
Get Single Report
Endpoint:
GET /v1/reports/{report_id}
Response:
{
"id": "rpt_abc123",
"brand_slug": "acme_corp",
"brand_name": "Acme Corp",
"token": "SIG-2025-11-XXXXX",
"product": "signal",
"status": "completed",
"created_at": "2025-11-11T01:00:00Z",
"completed_at": "2025-11-11T01:05:23Z",
"result_url": "gs://surmado-reports/...",
"download_url": "https://storage.googleapis.com/...",
"pptx_url": "https://storage.googleapis.com/...",
"error_message": null
}
Fields:
download_url- Pre-signed URL for PDF (expires in 15 minutes)pptx_url- Pre-signed URL for PowerPoint (Signal Pro only)status-queued,processing,completed, orfailed
Example:
curl https://api.surmado.com/v1/reports/rpt_abc123 \
-H "Authorization: Bearer sur_live_YOUR_KEY"
List Reports
Endpoint:
GET /v1/reports?page=1&page_size=50
Query Parameters:
page- Page number (default: 1)page_size- Reports per page (default: 50, max: 100)
Response:
{
"reports": [
{
"id": "rpt_abc123",
"brand_slug": "acme_corp",
"brand_name": "Acme Corp",
"product": "signal",
"status": "completed",
"created_at": "2025-11-11T01:00:00Z",
"download_url": "https://storage.googleapis.com/..."
}
],
"total": 150,
"page": 1,
"page_size": 50
}
Example:
curl "https://api.surmado.com/v1/reports?page=1&page_size=25" \
-H "Authorization: Bearer sur_live_YOUR_KEY"
Get Report Data (Advanced)
Extract specific fields from reports without downloading PDFs.
Endpoint:
GET /v1/reports/{report_id}/data?fields=field1,field2
Query Parameters:
fields(optional) - Comma-separated list of fields- Supports dot notation for nested fields (e.g.,
analysis.competitors) - Omit to get all data
Example: Specific Fields
curl "https://api.surmado.com/v1/reports/rpt_abc123/data?fields=brand_name,status,industry" \
-H "Authorization: Bearer sur_live_YOUR_KEY"
Response:
{
"brand_name": "Acme Corp",
"status": "completed",
"industry": "B2B SaaS"
}
Example: Nested Fields
curl "https://api.surmado.com/v1/reports/rpt_abc123/data?fields=analysis.competitors,analysis.market_size" \
-H "Authorization: Bearer sur_live_YOUR_KEY"
Response:
{
"analysis": {
"market_size": "$800 billion",
"competitors": ["HubSpot", "Pipedrive", "Salesforce"]
}
}
API Key Management
Manage API keys programmatically.
Create API Key
Endpoint:
POST /v1/api-keys
Request:
{
"name": "Production Integration"
}
Response:
{
"key": "sur_live_abc123...", // ⚠️ Shown only once!
"key_id": "abc123",
"name": "Production Integration",
"created_at": "2025-11-11T01:00:00Z"
}
Example:
curl -X POST https://api.surmado.com/v1/api-keys \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Production Integration"}'
List API Keys
Endpoint:
GET /v1/api-keys
Response:
{
"keys": [
{
"key_id": "abc123",
"name": "Production Integration",
"key_prefix": "sur_live_abc",
"created_at": "2025-11-11T01:00:00Z",
"last_used_at": "2025-11-11T02:30:00Z",
"last_used_ip": "203.0.113.42",
"is_active": true
}
]
}
Revoke API Key
Endpoint:
DELETE /v1/api-keys/{key_id}
Response:
{
"revoked": true,
"key_id": "abc123",
"revoked_at": "2025-11-11T03:00:00Z"
}
Webhooks
Get notified when reports complete with download URLs.
How Webhooks Work
- Include
webhook_urlin report request - Receive immediate
status: "queued"response - Backend processes report (2-10 minutes)
- Firestore write triggers webhook delivery
- Webhook POST sent to your URL with completion data
Infrastructure:
- Delivered via Cloud Function (webhook-dispatcher)
- Triggered by Firestore document updates
- Retries: 3 attempts (30s, 2min, 5min intervals)
- Timeout: 540s (your endpoint must respond within 9 minutes)
Setup
Add webhook_url to any report request:
{
"brand_slug": "acme_corp",
"tier": "pro",
"webhook_url": "https://your-app.com/api/webhooks/surmado",
...
}
Requirements:
- Must be HTTPS (HTTP not supported)
- Must return 2xx status code to acknowledge receipt
- Should respond within 30 seconds (9 min max timeout)
Webhook Payload (Completed)
Complete payload structure (all fields you’ll receive):
{
"report_id": "rep_abc123xyz",
"org_id": "org_xyz789abc",
"brand_slug": "acme_corp",
"brand_name": "Acme Corp",
"status": "completed",
"report_type": "signal",
"tier": "pro",
"result_url": "https://storage.googleapis.com/.../signal_report.pdf",
"pptx_url": "https://storage.googleapis.com/.../signal_report.pptx",
"completed_at": "2025-11-17T06:00:00Z"
}
Field Reference:
| Field | Type | Always Present? | Description |
|---|---|---|---|
report_id | string | ✅ Yes | Unique report identifier |
org_id | string | ✅ Yes | Your organization ID |
brand_slug | string | ✅ Yes | Brand identifier (immutable) |
brand_name | string | ✅ Yes | Brand display name |
status | string | ✅ Yes | Always “completed” for success webhooks |
report_type | string | ✅ Yes | ”signal” | “scan” | “solutions” | “monitor” |
tier | string | ✅ Yes | ”basic” | “pro” |
result_url | string | ✅ Yes | PDF download link (7-day expiry) |
pptx_url | string | ⚠️ Pro only | PowerPoint file (Signal/Scan Pro only) |
completed_at | string (ISO 8601) | ✅ Yes | UTC timestamp when report finished |
URL Expiry: All result_url and pptx_url links expire after 7 days. Download and store files if needed long-term.
Webhook Payload (Failed)
{
"report_id": "rep_abc123xyz",
"org_id": "org_xyz789abc",
"brand_slug": "acme_corp",
"brand_name": "Acme Corp",
"status": "failed",
"report_type": "signal",
"tier": "pro",
"error": "Generation failed: Invalid URL format",
"completed_at": "2025-11-17T06:00:00Z"
}
Note: Credits are automatically refunded for failed reports.
Security (HMAC Verification)
All webhooks include an HMAC-SHA256 signature:
X-Surmado-Signature: sha256=abc123def456...
Verify authenticity:
import hmac
import hashlib
import json
def verify_webhook(payload_bytes, signature_header, webhook_secret):
"""
Verify Surmado webhook signature.
Args:
payload_bytes: Raw request body (bytes)
signature_header: Value of X-Surmado-Signature header
webhook_secret: Your webhook secret from Surmado dashboard
"""
expected_signature = hmac.new(
webhook_secret.encode('utf-8'),
payload_bytes,
hashlib.sha256
).hexdigest()
received_signature = signature_header.replace('sha256=', '')
return hmac.compare_digest(expected_signature, received_signature)
# Usage
@app.post("/webhooks/surmado")
async def handle_webhook(request):
signature = request.headers.get('X-Surmado-Signature')
payload_bytes = await request.body()
if not verify_webhook(payload_bytes, signature, WEBHOOK_SECRET):
return {"error": "Invalid signature"}, 401
payload = json.loads(payload_bytes)
if payload['status'] == 'completed':
pdf_url = payload['result_url']
pptx_url = payload.get('pptx_url') # Pro tier only
# Process report...
return {"success": True}
Get Your Webhook Secret:
- Log into app.surmado.com
- Navigate to Settings → Webhooks
- Copy your webhook signing secret
Webhook Headers
All webhooks include these HTTP headers:
POST {your_webhook_url}
Content-Type: application/json
X-Surmado-Signature: sha256=abc123def456...
User-Agent: Surmado-Webhook/1.0
| Header | Value | Purpose |
|---|---|---|
Content-Type | application/json | Payload format |
X-Surmado-Signature | sha256={hex} | HMAC-SHA256 signature for verification |
User-Agent | Surmado-Webhook/1.0 | Identifies webhook source |
Retry Logic
Automatic retries on failure:
| Attempt | Timing | Trigger |
|---|---|---|
| Initial | Immediately when report completes | N/A |
| Retry 1 | 30 seconds after initial failure | Non-2xx response or timeout |
| Retry 2 | 2 minutes after retry 1 failure | Non-2xx response or timeout |
| Retry 3 | 5 minutes after retry 2 failure | Non-2xx response or timeout |
After 3 failed attempts, delivery is abandoned. You can still retrieve the report from the dashboard.
What counts as failure:
- Non-2xx HTTP status code (400, 500, etc.)
- Connection timeout (your endpoint doesn’t respond)
- DNS resolution failure
- SSL/TLS errors
Best practice: Return 200 OK immediately, then process the webhook asynchronously.
Testing Webhooks
Easiest: webhook.site (No setup required)
# 1. Go to https://webhook.site - copy your unique URL
# 2. Run test script
cd /path/to/api
./test_webhook.sh https://webhook.site/YOUR-UNIQUE-ID
# 3. Wait 2-5 minutes, see webhook arrive in real-time
Advanced: ngrok (Local development)
# Terminal 1: Start local server
python -m http.server 8000
# Terminal 2: Expose with ngrok
ngrok http 8000
# Terminal 3: Create test report
curl -X POST https://api.surmado.com/v1/reports/signal \
-H "Authorization: Bearer sur_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"brand_name": "Test Company",
"tier": "basic",
"email": "test@example.com",
"industry": "Technology",
"business_scale": "small",
"location": "San Francisco",
"persona": "Developers",
"pain_points": "Testing webhooks",
"webhook_url": "https://abc123.ngrok.io/webhook"
}'
See detailed testing guide: WEBHOOK_TESTING_GUIDE.md
Common Issues
“Webhook never arrived”
- Test with webhook.site first - Verify webhooks work in isolation
- Check your server logs - Did the request arrive but fail processing?
- Verify HTTPS - HTTP endpoints are not supported
- Check your server returns
200 OK- Non-2xx responses trigger retries - Check your firewall/CORS - Ensure your endpoint accepts external POST requests
- Contact support - Share your
report_idand webhook URL for investigation
“I received webhook but signature verification failed”
- Ensure you’re using the webhook secret from app.surmado.com settings
- Verify you’re hashing the raw request body (bytes, not parsed JSON)
- See signature verification examples above
“Can I receive duplicate webhooks?”
- Yes, if initial delivery succeeded but we didn’t receive acknowledgment
- Always check
report_idand deduplicate on your end
Error Handling
HTTP Status Codes
| Code | Meaning |
|---|---|
200 | Success |
201 | Created |
202 | Accepted (report queued) |
400 | Bad Request |
401 | Unauthorized |
402 | Payment Required (insufficient credits) |
404 | Not Found |
409 | Conflict (brand already exists) |
422 | Validation Error |
500 | Internal Server Error |
Error Response Format
{
"detail": {
"code": "insufficient_credits",
"message": "Insufficient credits. Required: 2, Available: 0"
}
}
Common Error Codes
| Code | Description | Fix |
|---|---|---|
invalid_api_key | API key not found or invalid | Check key format |
api_key_revoked | API key has been revoked | Create new key |
insufficient_credits | Not enough credits | Purchase more credits |
brand_not_found | Brand slug doesn’t exist | Create brand first with POST /v1/brands/ |
brand_exists | Brand name already exists | Use existing brand_slug from error |
invalid_tier | Invalid tier specified | Use “basic” or “pro” |
validation_error | Missing or invalid fields | Check required fields |
Code Examples
Python
import requests
API_KEY = "sur_live_YOUR_API_KEY"
API_URL = "https://api.surmado.com"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
# Create brand (with 409 handling)
def get_or_create_brand(brand_name):
response = requests.post(
f"{API_URL}/v1/brands/",
headers=headers,
json={"brand_name": brand_name}
)
if response.status_code == 201:
return response.json()["brand_slug"]
elif response.status_code == 409:
return response.json()["detail"]["brand_slug"]
else:
raise Exception(f"Error: {response.json()}")
# Create Signal report
brand_slug = get_or_create_brand("Acme Corp")
report_response = requests.post(
f"{API_URL}/v1/reports/signal",
headers=headers,
json={
"brand_slug": brand_slug,
"tier": "pro",
"email": "founder@acme.com",
"industry": "B2B SaaS",
"business_scale": "large",
"location": "United States",
"product": "CRM software",
"persona": "Small business owners",
"pain_points": "Losing track of customers",
"brand_details": "Simple CRM with email integration",
"direct_competitors": "HubSpot, Pipedrive",
"keywords": "CRM, sales tracking",
"webhook_url": "https://your-app.com/webhooks"
}
)
report = report_response.json()
print(f"Report ID: {report['id']}")
print(f"Status: {report['status']}")
# Check status
status_response = requests.get(
f"{API_URL}/v1/reports/{report['id']}",
headers=headers
)
print(status_response.json())
Node.js
const axios = require('axios');
const API_KEY = 'sur_live_YOUR_API_KEY';
const API_URL = 'https://api.surmado.com';
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
};
// Create or get brand
async function getOrCreateBrand(brandName) {
try {
const response = await axios.post(
`${API_URL}/v1/brands/`,
{ brand_name: brandName },
{ headers }
);
return response.data.brand_slug;
} catch (error) {
if (error.response?.status === 409) {
return error.response.data.detail.brand_slug;
}
throw error;
}
}
// Create Pulse report
async function createPulseReport() {
const brandSlug = await getOrCreateBrand('Acme Corp');
const response = await axios.post(
`${API_URL}/v1/reports/pulse`,
{
brand_slug: brandSlug,
tier: 'premium',
url: 'https://acme.com',
email: 'founder@acme.com',
report_style: 'executive',
webhook_url: 'https://your-app.com/webhooks'
},
{ headers }
);
console.log('Report ID:', response.data.id);
console.log('Token:', response.data.token);
return response.data;
}
createPulseReport()
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error.response?.data || error.message));
cURL
#!/bin/bash
API_KEY="sur_live_YOUR_API_KEY"
API_URL="https://api.surmado.com"
# Create brand
BRAND_RESPONSE=$(curl -s -X POST "${API_URL}/v1/brands/" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{"brand_name": "Acme Corp"}')
# Extract brand_slug (handles both 201 and 409)
BRAND_SLUG=$(echo "$BRAND_RESPONSE" | jq -r '.brand_slug // .detail.brand_slug')
echo "Brand slug: $BRAND_SLUG"
# Create Solutions report
REPORT_RESPONSE=$(curl -s -X POST "${API_URL}/v1/reports/solutions" \
-H "Authorization: Bearer ${API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"brand_slug\": \"${BRAND_SLUG}\",
\"email\": \"founder@acme.com\",
\"name\": \"John Smith\",
\"business_story\": \"B2B SaaS CRM, 50 employees, $3M ARR\",
\"decision\": \"Should we build mobile app or improve web?\",
\"success\": \"40% engagement increase in 6 months\",
\"timeline\": \"30 days\",
\"scale_indicator\": \"regional\"
}")
REPORT_ID=$(echo "$REPORT_RESPONSE" | jq -r '.id')
echo "Report ID: $REPORT_ID"
# Poll for completion
while true; do
STATUS_RESPONSE=$(curl -s "${API_URL}/v1/reports/${REPORT_ID}" \
-H "Authorization: Bearer ${API_KEY}")
STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.status')
echo "Status: $STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
echo "$STATUS_RESPONSE" | jq .
break
fi
sleep 30
done
Rate Limits
Default limit: 20 requests per minute (1 request every 3 seconds)
This applies to all API endpoints and all customers.
When you exceed the limit:
- HTTP 429 (Too Many Requests) response
Retry-After: 60header tells you to wait 60 seconds- Credits are NOT charged for rate-limited requests
Rate limit headers:
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 15
X-RateLimit-Reset: 1699564800
Need higher limits? Contact support@surmado.com with your use case. We can configure custom limits for high-volume integrations on a case-by-case basis.
Input Validation:
- All text fields have reasonable length limits (100-2000 characters)
- Oversized inputs are rejected with HTTP 400 BEFORE charging credits
- This protects you from accidental waste
Best Practices
1. Use Webhooks in Production
Don’t poll for status. Use webhooks for notifications:
# Good
report = create_report(webhook_url="https://your-app.com/webhook")
# Your webhook receives notification when complete
# Bad
while report['status'] != 'completed':
time.sleep(10)
report = get_report(report_id)
2. Handle 409 Gracefully
def get_or_create_brand(name):
response = create_brand(name)
if response.status_code == 409:
return response.json()["detail"]["brand_slug"]
return response.json()["brand_slug"]
3. Reuse Brand Profiles
# Good: Create once
brand_slug = get_or_create_brand("Acme Corp")
for month in months:
create_report(brand_slug=brand_slug, ...)
# Bad: Create every time
for month in months:
brand_slug = create_brand(f"Acme Corp") # 409 errors!
create_report(brand_slug=brand_slug, ...)
4. Test with Test Keys
# Development
API_KEY = "sur_test_fake_key"
# Production
API_KEY = os.environ["SURMADO_API_KEY"]
5. Monitor Credit Balance
# Check before creating expensive reports
org_data = get_organization_data()
if org_data['credits'] < 2:
send_alert("Low credits!")
Support
Questions? Contact hi@surmado.com
Dashboard: app.surmado.com
Status: status.surmado.com
Looking for no-code integrations? → NOVICE_GUIDE.md
Setting up locally? → README.md
Was this helpful?
Thanks for your feedback!
Have suggestions for improvement?
Tell us moreHelp Us Improve This Article
Know a better way to explain this? Have a real-world example or tip to share?
Contribute and earn credits:
- Submit: Get $25 credit (Signal, Scan, or Solutions)
- If accepted: Get an additional $25 credit ($50 total)
- Plus: Byline credit on this article