> ## Documentation Index
> Fetch the complete documentation index at: https://www.getsoundlink.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Access the API with TypeScript

> Install the soundlink SDK and call campaigns, write methods, and metrics from Node.js or Edge.

Install the official **`soundlink`** package to call the Public API with full TypeScript types. For raw HTTP, see [Quickstart](/docs/quickstart). Not sure which approach fits your stack? See [SDK overview](/docs/sdks).

## Prerequisites

* API key (`sk_*`) with the scopes you need — [Authentication](/docs/authentication)
* **Node.js 18+** or an Edge runtime with `fetch`

Store the key in an environment variable (e.g. `SOUNDLINK_API_KEY`). Never hardcode keys or ship them to the browser.

## 1. Install

<CodeGroup>
  ```bash npm theme={"theme":{"light":"github-light","dark":"github-dark"}}
  npm install soundlink
  ```

  ```bash yarn theme={"theme":{"light":"github-light","dark":"github-dark"}}
  yarn add soundlink
  ```

  ```bash pnpm theme={"theme":{"light":"github-light","dark":"github-dark"}}
  pnpm add soundlink
  ```

  ```bash bun theme={"theme":{"light":"github-light","dark":"github-dark"}}
  bun add soundlink
  ```
</CodeGroup>

## 2. Initialize the client

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Soundlink } from "soundlink";

const soundlink = new Soundlink({
  apiKey: process.env.SOUNDLINK_API_KEY!,
});
```

You can also pass the key as a string: `new Soundlink('sk_your_prefix_your_secret')`.

The client sends `x-api-key` on every request. Your organization is determined from the key.

## 3. Ping and list campaigns

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error, meta } = await soundlink.ping();
if (error) {
  console.error(error.message, meta?.requestId);
  process.exit(1);
}

const { data: campaigns, error: listError } = await soundlink.campaigns.list({
  page: 1,
  pageSize: 100,
  sortBy: "createdAt",
  sortOrder: "desc",
});

if (listError) {
  console.error(listError.code, listError.message);
}

const { data: campaign } = await soundlink.campaigns.get(
  "f1e28d31-c358-4284-9bef-00a2334625fd",
);
// campaign?.generation === 3 → wallet (writable)
```

List and get require the `campaigns:read` scope. Equivalent `curl` examples are in [Quickstart](/docs/quickstart).

### Paginate all campaigns

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
for await (const campaign of soundlink.campaigns.listAll({ pageSize: 100 })) {
  console.log(campaign.campaignId, campaign.generation);
}
```

## Create and manage wallet campaigns

Requires `campaigns:write`. Write methods take `{ idempotencyKey }` — the SDK sends the `Idempotency-Key` header. Only **`generation: 3`** (wallet) campaigns support budget, tiers, and stop. See [Creating campaigns](/docs/creating-campaigns) and [Managing campaigns](/docs/managing-campaigns).

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data: catalog } = await soundlink.strategies.list();

const { data: created, error: createError } = await soundlink.campaigns.create(
  {
    spotifyUrl: "https://open.spotify.com/track/11dFghVXANMlKmJXsNCbNl",
    dailyBudget: 25,
    durationDays: 14,
    genre: "Pop",
    strategyType: "maximum_growth",
    campaignName: "Summer single push",
  },
  { idempotencyKey: "create-2026-07-21-mytrack-01" },
);

if (createError || !created) {
  console.error(createError?.code, createError?.message);
  return;
}

await soundlink.campaigns.increaseBudget(
  created.campaignId,
  { amount: 100, mode: "current_and_renewals" },
  { idempotencyKey: "budget-inc-01" },
);

// GET returns `tierId`; PATCH expects the same value as `targetingTierId`
const { data: tiers } = await soundlink.campaigns.tiers.get(created.campaignId);
await soundlink.campaigns.tiers.update(created.campaignId, {
  items: (tiers?.tiers ?? []).map((tier) => ({
    targetingTierId: tier.tierId,
    isEnabled: tier.isEnabled,
    newAllocationPercent: tier.allocationPercent,
  })),
});

await soundlink.campaigns.stop(created.campaignId, {
  idempotencyKey: "stop-01",
});
```

## Response pattern

Every method returns `{ data, error, meta? }` — the same envelope as the REST API. HTTP errors are **not thrown**; check `error` and use `requestId` when contacting support.

<Note>
  Do not use `try/catch` for normal API failures. The SDK only throws for
  configuration, parse, or transport errors. See [Errors](#errors) below and
  [Errors](/docs/errors) for API codes.
</Note>

## Metrics and exports

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data: overview } = await soundlink.metrics.overview(
  "f1e28d31-c358-4284-9bef-00a2334625fd",
  {
    startDate: "2026-01-01",
    endDate: "2026-03-31",
  },
);

const { data: stream } = await soundlink.metrics.breakdown.export(
  "f1e28d31-c358-4284-9bef-00a2334625fd",
  {
    startDate: "2026-01-01",
    endDate: "2026-03-31",
  },
);

if (stream) {
  for await (const row of stream) {
    await warehouse.insert(row);
  }
}
```

For smaller datasets: `await soundlink.metrics.breakdown.export.collect('f1e28d31-c358-4284-9bef-00a2334625fd')`.

Engagement exports work the same way: `soundlink.metrics.engagement.export` and `.export.collect`.

See [Understanding metrics](/docs/understanding-metrics) and [JSONL exports](/docs/jsonl-exports).

## Configuration

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const soundlink = new Soundlink({
  apiKey: process.env.SOUNDLINK_API_KEY!,
  baseUrl: "https://api.getsoundlink.com", // default
  timeout: 30_000,
  maxRetries: 2, // retries on 429/5xx; writes reuse the same Idempotency-Key
  fetch: customFetch, // optional — tests or Edge
});
```

## Methods

Every method maps 1:1 to a Public API route. All return `Promise<ApiResponse<T>>` unless noted.

### System

| Method             | REST endpoint  | Returns                 |
| ------------------ | -------------- | ----------------------- |
| `soundlink.ping()` | `GET /v1/ping` | `ApiResponse<PingData>` |

### Strategies

Requires `campaigns:read`.

| Method                        | REST endpoint        | Returns                              |
| ----------------------------- | -------------------- | ------------------------------------ |
| `soundlink.strategies.list()` | `GET /v1/strategies` | `ApiResponse<StrategiesCatalogData>` |

### Campaigns (read)

Requires `campaigns:read`.

| Method                                 | REST endpoint            | Returns                           |
| -------------------------------------- | ------------------------ | --------------------------------- |
| `soundlink.campaigns.list(params?)`    | `GET /v1/campaigns`      | `ApiResponse<CampaignListData>`   |
| `soundlink.campaigns.get(campaignId)`  | `GET /v1/campaigns/{id}` | `ApiResponse<CampaignDetail>`     |
| `soundlink.campaigns.listAll(params?)` | Paginated `list`         | `AsyncGenerator<CampaignSummary>` |

`list` params: `page`, `pageSize` (max **100**), `sortBy` (`createdAt` | `status`), `sortOrder` (`asc` | `desc`).

### Campaigns (write)

Requires `campaigns:write`. Pass `{ idempotencyKey }` on create, stop, and budget methods. Wallet campaigns only (`generation: 3`) for budget / tiers / stop.

| Method                                                             | REST endpoint              | Returns                                   |
| ------------------------------------------------------------------ | -------------------------- | ----------------------------------------- |
| `soundlink.campaigns.create(body, { idempotencyKey })`             | `POST /v1/campaigns`       | `ApiResponse<CampaignCreateData>`         |
| `soundlink.campaigns.stop(id, { idempotencyKey })`                 | `POST .../stop`            | `ApiResponse<CampaignStopData>`           |
| `soundlink.campaigns.increaseBudget(id, body, { idempotencyKey })` | `POST .../budget/increase` | `ApiResponse<IncreaseCampaignBudgetData>` |
| `soundlink.campaigns.decreaseBudget(id, body, { idempotencyKey })` | `POST .../budget/decrease` | `ApiResponse<DecreaseCampaignBudgetData>` |
| `soundlink.campaigns.tiers.get(id)`                                | `GET .../tiers`            | `ApiResponse<CampaignTiersData>`          |
| `soundlink.campaigns.tiers.update(id, body)`                       | `PATCH .../tiers`          | `ApiResponse<CampaignTiersData>`          |

### Metrics

Requires `metrics:read`.

| Method                                                     | REST endpoint                       | Returns                                        |
| ---------------------------------------------------------- | ----------------------------------- | ---------------------------------------------- |
| `soundlink.metrics.overview(id, params?)`                  | `GET .../metrics/overview`          | `ApiResponse<MetricsOverview>`                 |
| `soundlink.metrics.breakdown.list(id, params?)`            | `GET .../metrics/breakdown`         | `ApiResponse<BreakdownListData>`               |
| `soundlink.metrics.breakdown.export(id, params?)`          | `GET .../metrics/breakdown/export`  | `ApiResponse<JsonlStream<BreakdownRow>>`       |
| `soundlink.metrics.breakdown.export.collect(id, params?)`  | Same export route                   | `ApiResponse<ExportCollection<BreakdownRow>>`  |
| `soundlink.metrics.engagement.export(id, params?)`         | `GET .../metrics/engagement/export` | `ApiResponse<JsonlStream<EngagementRow>>`      |
| `soundlink.metrics.engagement.export.collect(id, params?)` | Same export route                   | `ApiResponse<ExportCollection<EngagementRow>>` |

Date params use inclusive `YYYY-MM-DD`. Export windows are capped at **90 days** per request. Breakdown list `pageSize` max is **500**.

## Types

All public types are exported from `soundlink` for use in your app:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import type {
  ApiResponse,
  ApiError,
  ApiMeta,
  PingData,
  CampaignSummary,
  CampaignDetail,
  CampaignGeneration,
  CampaignListData,
  CampaignListParams,
  CreateCampaignRequest,
  CampaignCreateData,
  StrategiesCatalogData,
  MetricsOverview,
  BreakdownRow,
  BreakdownListData,
  BreakdownListParams,
  EngagementRow,
  ExportCollection,
  JsonlStream,
  SoundlinkClientOptions,
  PublicApiErrorCode,
} from "soundlink";
```

| Group          | Types                                                                                                             |
| -------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Envelope**   | `ApiResponse<T>`, `ApiError`, `ApiMeta`, `PublicApiErrorCode`                                                     |
| **Campaigns**  | `CampaignSummary`, `CampaignDetail`, `CampaignGeneration`, `CreateCampaignRequest`, `CampaignStatus`              |
| **Strategies** | `StrategiesCatalogData`, `StrategyCatalogItem`, `StrategyType`                                                    |
| **Metrics**    | `MetricsOverview`, `BreakdownRow`, `BreakdownListData`, `EngagementRow`                                           |
| **Params**     | `DateRangeParams`, `BreakdownListParams`, `BreakdownExportParams`, `EngagementExportParams`, `IdempotencyOptions` |
| **Exports**    | `JsonlStream<T>`, `ExportCollection<T>`                                                                           |
| **Client**     | `Soundlink`, `SoundlinkClientOptions`                                                                             |

Row shapes match the warehouse schemas documented in [Understanding metrics](/docs/understanding-metrics). For full field lists, use the [API Reference](/docs/api-reference) or your IDE hover hints on the imported types.

## Errors

The SDK has **two** error paths. Most of the time you only need the first.

### API errors (returned, not thrown)

When the Public API returns `4xx` or `5xx`, the SDK resolves with `{ data: null, error, meta? }`:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const { data, error, meta } = await soundlink.campaigns.get("camp_unknown");

if (error) {
  // error.code    — e.g. 'campaign_not_found'
  // error.message — human-readable
  // error.status  — HTTP status (404, 429, …)
  // error.requestId — same as meta?.requestId
  // error.retryAfter — seconds, on 429 only
  // error.details — code-specific context when present
  //                 (e.g. available/required on insufficient_credit)

  if (error.code === "rate_limit_exceeded" && error.retryAfter) {
    await sleep(error.retryAfter * 1000);
  }

  if (error.code === "insufficient_credit") {
    console.error("wallet", error.details);
  }
}
```

Always check `error` before using `data`. Error codes and HTTP mapping: [Errors](/docs/errors).

### SDK errors (thrown)

These extend `SoundlinkSdkError` and mean something went wrong **outside** a normal API response:

| Class                  | When                                       |
| ---------------------- | ------------------------------------------ |
| `SoundlinkConfigError` | Missing or invalid API key at construction |
| `SoundlinkParseError`  | Response body could not be parsed          |
| `SoundlinkSdkError`    | Unexpected transport failure               |

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { Soundlink, SoundlinkConfigError } from "soundlink";

try {
  const soundlink = new Soundlink({ apiKey: "" }); // throws SoundlinkConfigError
} catch (err) {
  if (err instanceof SoundlinkConfigError) {
    console.error("Fix your API key configuration");
  }
}
```

Use `try/catch` for client setup and rare transport failures. Use `{ data, error }` for everything the API returns.

## Next

[Creating campaigns](/docs/creating-campaigns) → [Managing campaigns](/docs/managing-campaigns) → [Syncing campaigns](/docs/syncing-campaigns) → [Understanding metrics](/docs/understanding-metrics)
