> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.spyglass.so/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.spyglass.so/_mcp/server.

# Python

> The Spyglass Python SDK wraps the Spyglass API with synchronous and asynchronous clients.

The official Python SDK ([`spyglass-sdk`](https://pypi.org/project/spyglass-sdk/) on PyPI, [`spyglass-so/spyglass-python`](https://github.com/spyglass-so/spyglass-python) on GitHub) provides typed synchronous (`Spyglass`) and asynchronous (`AsyncSpyglass`) clients for the Spyglass API.

## Installation

Install `spyglass-sdk` with `pip` or `uv`:

**`pip`**

```bash pip
pip install spyglass-sdk
```

**`uv`**

```bash uv
uv add spyglass-sdk
```

## Usage

1. Get an API key from **Settings → API** in the [dashboard](https://app.spyglass.so).
2. Set the key as an environment variable named `SPYGLASS_API_KEY` or pass it directly to the `Spyglass` class as `token`.

```python
import os
from spyglass import Spyglass

client = Spyglass(token=os.environ["SPYGLASS_API_KEY"])

# 1. Resolve a brand by name
result = client.search_brands(names=["Nike"], limit=1)

if result.brands:
    brand = result.brands[0]

    # 2. Search the creatives that brand is running
    media_result = client.search_media(
        account_id=brand.id,
        query="marathon running shoes comfort",
        limit=10,
    )

    for ad in media_result.media:
        print(ad.id, ad.hook, ad.core_angle, ad.url)
```

## Searching Brands & Creators

### Search brands

Search brands by name (`names`), social handle (`ig_username`, `tiktok_username`), known IDs (`brand_ids`), or a natural-language brief (`query`):

```python
# Fuzzy name lookup
by_name = client.search_brands(
    names=["Athletic Greens", "AG1"],
    limit=5,
)

# Natural-language discovery
by_brief = client.search_brands(
    query="DTC electrolyte and hydration powder brands",
    limit=10,
)
```

### Search creators

Search creators by handle, name, or natural-language brief, and filter by demographic and profile attributes (`gender`, `age_range`, `ethnicity`, `location`, `language`):

```python
creators = client.search_creators(
    query="skincare routine and dermatology tips",
    gender="female",
    age_range="25-34",
    location="United States",
    language="English",
    limit=20,
)
```

## Searching Ad Creatives & Social Media

Pass `account_id` to inspect a specific brand or creator's creatives, or omit `account_id` and pass `query` to search across the entire Spyglass index:

```python
# Optional: look up valid product taxonomy paths first (free call)
taxonomies = client.search_taxonomies(query="running shoes")

media = client.search_media(
    query="UGC founder story explaining product benefits",
    channel=["META_AD", "TIKTOK"],
    format=["VIDEO"],
    status="ACTIVE",
    taxonomy=taxonomies.paths[:2],
    min_days_run=14,
    limit=20,
    offset=0,
)
```

## Insights, Campaigns, Landing Pages & Comments

### Creative insights, campaigns, and landing pages

Pull clustered creative themes (`HOOK`, `USP`, `PERSONA`, `MEDIA_MIX`), campaigns or offers, and ranked destination landing pages for any account:

```python
# Ranked hooks + weekly volume breakdown over the last 90 days
insights = client.account.insights(
    id=brand.id,
    type="HOOK",
    max_days_back=90,
    limit=10,
)

# Top landing pages where the brand sends ad traffic
campaigns = client.account.campaigns(
    id=brand.id,
    type="CAMPAIGN",
    date_preset="last-30-days",
    limit=10,
)

landing_pages = client.account.landing_pages(
    id=brand.id,
    show_inactive="false",
    limit=20,
)
```

### Post comments

Fetch public comments from Instagram posts/reels (by shortcode or URL) and TikTok videos (by video ID or URL):

```python
comments = client.get_comments(
    instagram_ids=["https://www.instagram.com/reel/C2xY9abcD1e/"],
    tiktok_ids=["https://www.tiktok.com/@nike/video/7234567890123456789"],
    limit=1,
)
```

## Indexing an Account

If Spyglass does not have a brand or creator yet, submit its website or social profile URL (`POST /account/index` is free) and poll until `status` is `"ready"`:

```python
import time

indexed = client.account.index(website_url="https://www.on.com")

status = indexed.status
while status == "indexing":
    time.sleep(3)
    check = client.account.index_status(id=indexed.brand_id)
    status = check.status

if status in ("ready", "already_exists"):
    account = client.account.get(id=indexed.brand_id)
    print("Indexed account:", account.name)
```

## Async Usage

Import `AsyncSpyglass` for non-blocking calls with `asyncio`:

```python
import asyncio
import os
from spyglass import AsyncSpyglass

client = AsyncSpyglass(token=os.environ["SPYGLASS_API_KEY"])


async def main() -> None:
    result = await client.search_brands(names=["Nike"], limit=3)
    for brand in result.brands:
        print(brand.id, brand.name)


asyncio.run(main())
```

## Error Handling

When the API returns a non-2xx status code, the SDK raises an `ApiError` containing the HTTP status code and parsed error body:

```python
from spyglass.core.api_error import ApiError

try:
    client.search_brands(names=["Nike"], limit=5)
except ApiError as e:
    print(e.status_code)
    print(e.body)
```

## Retries, Timeouts & Raw Responses

Requests default to a **60-second timeout** and **2 automatic retries** with exponential backoff on `408`, `429`, and `5xx` responses. Override these on the client or per request:

```python
client = Spyglass(
    token=os.environ["SPYGLASS_API_KEY"],
    timeout=30.0,
    max_retries=3,
)

response = client.search_brands(
    names=["Nike"],
    limit=5,
    request_options={"timeout_in_seconds": 15, "max_retries": 1},
)
```

Access HTTP headers and status codes via `.with_raw_response`:

```python
raw = client.with_raw_response.search_brands(names=["Nike"], limit=1)
print(raw.status_code)
print(raw.headers.get("retry-after"))
print(raw.data.brands)
```