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

# Node

> The Spyglass Node SDK wraps the Spyglass API for Node.js and TypeScript applications.

The official TypeScript and Node.js SDK ([`@spyglasshq/sdk`](https://www.npmjs.com/package/@spyglasshq/sdk) on npm, [`spyglass-so/spyglass-typescript`](https://github.com/spyglass-so/spyglass-typescript) on GitHub) provides a fully typed client for Node.js 18+, Bun, Deno, Vercel Functions, and Cloudflare Workers with zero external runtime dependencies.

## Installation

Install `@spyglasshq/sdk` with your package manager:

**`npm`**

```bash npm
npm install @spyglasshq/sdk
```

**`pnpm`**

```bash pnpm
pnpm add @spyglasshq/sdk
```

**`yarn`**

```bash yarn
yarn add @spyglasshq/sdk
```

**`bun`**

```bash bun
bun add @spyglasshq/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 `new Spyglass({ token })`.

```ts
import { Spyglass } from "@spyglasshq/sdk";

const client = new Spyglass({
  token: process.env.SPYGLASS_API_KEY!,
});

// 1. Resolve a brand by name
const { brands } = await client.searchBrands({
  names: ["Nike"],
  limit: 1,
});

const brand = brands[0];

if (brand) {
  // 2. Search the creatives that brand is running
  const { media } = await client.searchMedia({
    accountId: brand.id,
    query: "marathon running shoes comfort",
    limit: 10,
  });

  for (const ad of media) {
    console.log({
      id: ad.id,
      hook: ad.hook,
      coreAngle: ad.coreAngle,
      targetPersona: ad.targetPersona,
      daysRun: ad.daysRun,
      url: ad.url,
    });
  }
}
```

## Searching Brands & Creators

### Search brands

Search brands by name (`names`), social handle (`igUsername`, `tiktokUsername`), known IDs (`ids`), or a natural-language brief (`query`):

```ts
// Fuzzy name lookup
const byName = await client.searchBrands({
  names: ["Athletic Greens", "AG1"],
  limit: 5,
});

// Natural-language discovery
const byBrief = await client.searchBrands({
  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`, `ageRange`, `ethnicity`, `location`, `language`):

```ts
const { creators } = await client.searchCreators({
  query: "skincare routine and dermatology tips",
  gender: "female",
  ageRange: "25-34",
  location: "United States",
  language: "English",
  limit: 20,
});
```

## Searching Ad Creatives & Social Media

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

```ts
// Optional: look up valid product taxonomy paths first (free call)
const { paths } = await client.searchTaxonomies({
  query: "running shoes",
});

const { media, count } = await client.searchMedia({
  query: "UGC founder story explaining product benefits",
  channel: ["META_AD", "TIKTOK"],
  format: ["VIDEO"],
  status: "ACTIVE",
  taxonomy: paths.slice(0, 2),
  minDaysRun: 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:

```ts
// Ranked hooks + weekly volume breakdown over the last 90 days
const insights = await client.account.insights({
  id: brand.id,
  type: "HOOK",
  maxDaysBack: 90,
  limit: 10,
});

// Top landing pages where the brand sends ad traffic
const { campaigns } = await client.account.campaigns({
  id: brand.id,
  type: "CAMPAIGN",
  datePreset: "last-30-days",
  limit: 10,
});

const { landingPages } = await client.account.landingPages({
  id: brand.id,
  showInactive: "false",
  limit: 20,
});
```

### Post comments

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

```ts
const { posts } = await client.getComments({
  instagramIds: ["https://www.instagram.com/reel/C2xY9abcD1e/"],
  tiktokIds: ["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"`:

```ts
const indexed = await client.account.index({
  websiteUrl: "https://www.on.com",
});

let status = indexed.status;
while (status === "indexing") {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  const check = await client.account.indexStatus({ id: indexed.brandId });
  status = check.status;
}

if (status === "ready" || status === "already_exists") {
  const account = await client.account.get({ id: indexed.brandId });
  console.log("Indexed account:", account.name);
}
```

## TypeScript Types

All request and response types are exported under the `spyglass` namespace:

```ts
import { spyglass } from "@spyglasshq/sdk";

const request: spyglass.SearchMediaRequest = {
  query: "summer sale",
  channel: ["META_AD"],
  limit: 25,
};

type MediaItem = spyglass.SearchMediaResponse.Media.Item;
```

## Error Handling

When the API returns a non-2xx status code, the SDK throws a `SpyglassError` (or a status-specific subclass such as `PaymentRequiredError` or `TooManyRequestsError`):

```ts
import { SpyglassError } from "@spyglasshq/sdk";

try {
  await client.searchBrands({ names: ["Nike"], limit: 5 });
} catch (err) {
  if (err instanceof SpyglassError) {
    console.error("Status:", err.statusCode);
    console.error("Message:", err.message);
    console.error("Body:", err.body);
  }
}
```

## Retries, Timeouts & Raw Responses

The SDK defaults to a **60-second timeout** and **2 automatic retries** with exponential backoff on `408`, `429`, and `5xx` responses. Configure these on the client or override them per request:

```ts
const controller = new AbortController();

const response = await client.searchMedia(
  { query: "protein bar", limit: 10 },
  {
    timeoutInSeconds: 30,
    maxRetries: 3,
    abortSignal: controller.signal,
  },
);
```

Chain `.withRawResponse()` on any request promise to inspect HTTP status codes and headers alongside parsed data:

```ts
const { data, rawResponse } = await client
  .searchBrands({ names: ["Nike"], limit: 1 })
  .withRawResponse();

console.log(data.brands);
console.log(rawResponse.headers.get("retry-after"));
```