Skip to navigation

Node

The official TypeScript and Node.js SDK (@spyglasshq/sdk on npm, 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 install @spyglasshq/sdk

Usage

  1. Get an API key from Settings → API in the dashboard.
  2. Set the key as an environment variable named SPYGLASS_API_KEY or pass it directly to new Spyglass({ token }).
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):

// 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):

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:

// 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:

// 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):

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":

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:

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):

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:

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:

const { data, rawResponse } = await client
.searchBrands({ names: ["Nike"], limit: 1 })
.withRawResponse();
console.log(data.brands);
console.log(rawResponse.headers.get("retry-after"));