Skip to main content

Surmado API - Developer Guide

30 min read

30 min read Level: Advanced For: Developer

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

1. Get Your API Key

  1. Visit app.surmado.com
  2. Navigate to Settings → API Keys
  3. Click Create New API Key
  4. 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 your brand_name
  • brand_created: true tells you this was a new brand
  • Save the brand_slug for 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:

  1. FastAPI Middleware validates every request
  2. API Key Validation checks bcrypt-hashed keys against Firestore
  3. 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:

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/false
  • note: 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
  • null or "" → 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:

ProductWhat It DoesCreditsTime
SignalAI visibility analysis2~5 min
PulseWebsite competitive analysis1-2~3 min
SolutionsStrategic advisory board2~10 min

Signal API

Analyze how your brand appears in AI-powered search (ChatGPT, Perplexity, Claude, etc.).

Endpoint

POST /v1/reports/signal

Tiers

TierCreditsFeatures
basic1Essential visibility analysis, PDF
pro2Everything + PowerPoint + deeper insights

Pro Tier Benefits:

  • ✅ Comprehensive competitive analysis
  • ✅ PowerPoint file automatically generated
  • ✅ Delivered via webhook with pptx_url

Request Schema

Required Fields:

FieldTypeDescriptionExample
brand_name OR brand_slugstringBrand identifier (we auto-create slug from name)"Acme Corp"
tierstring"basic" or "pro""basic"
emailstringContact email"cto@acme.com"
industrystringIndustry/sector"B2B SaaS"
business_scalestringBusiness scale"small" | "medium" | "large"
locationstringPrimary location"San Francisco"
personastringTarget customer/audience"CTOs at mid-market companies"
pain_pointsstringPain points you address"Integration challenges"
brand_detailsstringBrief brand/product description"AI-powered CRM with email integration"
direct_competitorsstringCompetitors (comma-separated)"HubSpot, Pipedrive, Salesforce"

Optional Fields:

FieldTypeTier AvailabilityDescription
productstringBothProduct/service description (enhances analysis)
indirect_competitorsstringBothIndirect competitors (comma-separated)
keywordsstringBothKeywords to analyze (comma-separated)
competitor_urlsarrayBothArray of competitor URLs (more detailed in Pro)
webhook_urlstring (URL)BothWebhook for completion notification
generate_pptxbooleanPro onlyGenerate PowerPoint presentation (default: true)
is_agency_white_labelbooleanBothEnable white-label mode
agency_namestringBothAgency 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:

  1. queued - Report accepted, queued for processing
  2. processing - AI analysis in progress
  3. completed - Report ready, webhook sent
  4. failed - Error occurred, credits refunded

Pulse API

Website competitive analysis with SEO, content, and technical insights.

Endpoint

POST /v1/reports/pulse

Tiers

TierCreditsPagesFeatures
basic130Performance, SEO, accessibility
premium2100Everything + competitor analysis

Request Schema

Required Fields:

FieldTypeDescriptionExample
brand_name OR brand_slugstringBrand identifier"Acme Corp"
urlstring (URL)Website to audit"https://acme.com"
emailstring (email)Contact email"marketing@acme.com"
tierstring"basic" or "premium""basic"

Optional Fields:

FieldTypeTier AvailabilityDescriptionDefault
report_stylestringBoth"executive" | "technical" | "comprehensive""executive"
competitor_urlsarrayPremium onlyCompetitor URLs to compare against[]
webhook_urlstring (URL)BothWebhook for completion notification-
is_agency_white_labelbooleanBothEnable white-label modefalse
agency_namestringBothAgency 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:

ModeWhen to UseRequired Fields
Signal Token ModeWhen you’ve already run Signal and want strategic recommendations based on that dataemail + signal_token only
Standalone ModeFresh strategic advisory without prior Signal analysisAll 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):

FieldTypeDescriptionExample
brand_name OR brand_slugstringBrand identifier"Acme Corp"
emailstring (email)Contact email"founder@acme.com"
business_storystringTell us about your business"B2B SaaS CRM, 50 employees, $3M ARR"
decisionstringKey decision or challenge"Should we build a mobile app?"
successstringWhat does success look like?"40% engagement increase in 6 months"
timelinestringTimeline for decision"Need decision in next 30 days"
scale_indicatorstringBusiness scale/size"regional" or "$3M ARR"

Required Fields (Signal Token Mode):

FieldTypeDescriptionExample
emailstring (email)Contact email"founder@acme.com"
signal_tokenstringToken from completed Signal report"SIG-2025-11-XXXXX"

Note: In Signal Token Mode, brand_slug/brand_name are inherited from the Signal report. All other fields are optional.

Optional Fields:

FieldTypeDescription
signal_tokenstringSignal report token for enhanced analysis
include_financialstringInclude financial analysis? ("yes" or "no")
financial_contextstringFinancial context/situation
monthly_revenuestringMonthly revenue
monthly_costsstringMonthly costs
cash_availablestringCash available
webhook_urlstring (URL)Webhook for completion notification
is_agency_white_labelbooleanEnable white-label mode
agency_namestringAgency 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

  1. Atomic credit deduction (6 credits upfront)
  2. Scan + Signal queued in parallel (both start immediately)
  3. Solutions auto-triggered when Signal completes (uses Signal intelligence)

Request Schema

Required Fields:

FieldTypeDescriptionExample
brand_name OR brand_slugstringBrand identifier"Acme Corp"
emailstringContact email"founder@acme.com"
urlstring (URL)Brand website"https://acme.com"
industrystringIndustry/sector"B2B SaaS"
locationstringPrimary location"United States"
personastringTarget customer"Small business owners"
pain_pointsstringProblems you solve"Losing track of customers"

Optional Fields:

FieldTypeDescription
business_scalestring"small" | "medium" | "large"
productstringProduct/service description
brand_detailsstringBrand positioning details
direct_competitorsstringCompetitors (comma-separated)
competitor_urlsarrayCompetitor URLs for Scan analysis
webhook_urlstring (URL)Webhook for completion notifications
is_agency_white_labelbooleanEnable white-label mode
agency_namestringAgency 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: queuedprocessingcompleted
  • Signal: queuedprocessingcompleted
  • Solutions: waiting_on_signalqueuedprocessingcompleted

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

MethodCreditsUse Case
Manual Chain (2 API calls)2 + 2 = 4Full control, custom timing
Combo Endpoint (1 API call)4Automated, 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_slug and brand_name from 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:

  1. API deducts 4 credits upfront
  2. Creates both reports atomically
  3. Queues Signal for processing
  4. When Signal completes, Solutions is auto-triggered with Signal intelligence
  5. Both webhooks fire when each report completes

Solutions report starts in waiting_on_signal status, then transitions to queuedprocessingcompleted after Signal finishes.


Choosing Between Methods

ScenarioRecommended Method
Automated pipeline, no user interactionCombo Endpoint
User wants to review Signal before running SolutionsManual Chain
Building a “one-click full analysis” buttonCombo Endpoint
User already has a Signal report they want to extendManual Chain
Webform with two-step wizardManual 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, or failed

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

  1. Include webhook_url in report request
  2. Receive immediate status: "queued" response
  3. Backend processes report (2-10 minutes)
  4. Firestore write triggers webhook delivery
  5. 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:

FieldTypeAlways Present?Description
report_idstring✅ YesUnique report identifier
org_idstring✅ YesYour organization ID
brand_slugstring✅ YesBrand identifier (immutable)
brand_namestring✅ YesBrand display name
statusstring✅ YesAlways “completed” for success webhooks
report_typestring✅ Yes”signal” | “scan” | “solutions” | “monitor”
tierstring✅ Yes”basic” | “pro”
result_urlstring✅ YesPDF download link (7-day expiry)
pptx_urlstring⚠️ Pro onlyPowerPoint file (Signal/Scan Pro only)
completed_atstring (ISO 8601)✅ YesUTC 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:

  1. Log into app.surmado.com
  2. Navigate to Settings → Webhooks
  3. 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
HeaderValuePurpose
Content-Typeapplication/jsonPayload format
X-Surmado-Signaturesha256={hex}HMAC-SHA256 signature for verification
User-AgentSurmado-Webhook/1.0Identifies webhook source

Retry Logic

Automatic retries on failure:

AttemptTimingTrigger
InitialImmediately when report completesN/A
Retry 130 seconds after initial failureNon-2xx response or timeout
Retry 22 minutes after retry 1 failureNon-2xx response or timeout
Retry 35 minutes after retry 2 failureNon-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”

  1. Test with webhook.site first - Verify webhooks work in isolation
  2. Check your server logs - Did the request arrive but fail processing?
  3. Verify HTTPS - HTTP endpoints are not supported
  4. Check your server returns 200 OK - Non-2xx responses trigger retries
  5. Check your firewall/CORS - Ensure your endpoint accepts external POST requests
  6. Contact support - Share your report_id and 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_id and deduplicate on your end

Error Handling

HTTP Status Codes

CodeMeaning
200Success
201Created
202Accepted (report queued)
400Bad Request
401Unauthorized
402Payment Required (insufficient credits)
404Not Found
409Conflict (brand already exists)
422Validation Error
500Internal Server Error

Error Response Format

{
  "detail": {
    "code": "insufficient_credits",
    "message": "Insufficient credits. Required: 2, Available: 0"
  }
}

Common Error Codes

CodeDescriptionFix
invalid_api_keyAPI key not found or invalidCheck key format
api_key_revokedAPI key has been revokedCreate new key
insufficient_creditsNot enough creditsPurchase more credits
brand_not_foundBrand slug doesn’t existCreate brand first with POST /v1/brands/
brand_existsBrand name already existsUse existing brand_slug from error
invalid_tierInvalid tier specifiedUse “basic” or “pro”
validation_errorMissing or invalid fieldsCheck 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: 60 header 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

Help 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
Contribute to This Article