TypeScript
Waveband does not publish an SDK package. The API is described by the OpenAPI 3.1 document, so you can generate request types from it and call the API with fetch. Node 18+ or any modern runtime works.
Types from OpenAPI
npx openapi-typescript https://waveband.io/api/v1/openapi.json -o waveband-api.d.ts
Request bodies, path and query parameters are typed. Responses are described in prose on the API reference, not as schemas, so type them where you use them:
import type { paths } from "./waveband-api";
type CreateCampaign =
paths["/api/v1/campaigns"]["post"]["requestBody"]["content"]["application/json"];Any other OpenAPI 3.1 generator works the same way.
A fetch helper
Copy this into your project. It sends your key, an optional idempotency key, and JSON; returns parsed JSON (or text for CSV); and throws on errors with the stable error code.
const WAVEBAND_HOST = "https://waveband.io"; // or your white-label host
export class WavebandError extends Error {
constructor(
readonly status: number,
readonly code: string,
message: string,
readonly details?: unknown,
) {
super(message);
}
}
export async function waveband<T = any>(
method: "GET" | "POST" | "PATCH" | "DELETE",
path: string,
options: { body?: unknown; query?: Record<string, string>; idempotencyKey?: string } = {},
): Promise<T> {
const url = new URL(`/api/v1${path}`, WAVEBAND_HOST);
for (const [key, value] of Object.entries(options.query ?? {})) url.searchParams.set(key, value);
const headers: Record<string, string> = {
authorization: `Bearer ${process.env.WAVEBAND_API_KEY}`,
};
if (options.body !== undefined) headers["content-type"] = "application/json";
if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
const res = await fetch(url, {
method,
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
const text = await res.text();
const json = text && res.headers.get("content-type")?.includes("json");
const data = json ? JSON.parse(text) : text;
if (!res.ok) {
const error = data?.error ?? {};
throw new WavebandError(res.status, error.code ?? `HTTP_${res.status}`, error.message ?? res.statusText, error.details);
}
return data as T;
}Launch flow: campaign, line item, creative
Money is integer USD cents. A campaign with no end date needs a daily budget. Campaigns and line items start as drafts; nothing spends until you activate them.
const { items: workspaces } = await waveband("GET", "/workspaces");
const workspaceId = workspaces[0].id;
// Once per workspace: delivery needs the advertiser domain and IAB category.
await waveband("PATCH", `/workspaces/${workspaceId}`, {
body: {
advertiserDomain: "acmeoutdoors.com",
iabCategory: "IAB22"
},
});
// Idempotency keys make retries safe: the same key never creates twice.
const campaign = await waveband("POST", "/campaigns", {
idempotencyKey: "spring-ctv-campaign",
body: {
workspaceId: workspaceId,
name: "Spring CTV Launch",
channel: "ctv",
startDate: "2026-12-01",
budgetUsdMinor: 500000,
dailyBudgetUsdMinor: 50000,
goalType: "impressions"
},
});
const lineItem = await waveband("POST", "/line-items", {
idempotencyKey: "spring-ctv-li-1",
body: {
campaignId: campaign.id,
name: "CTV: US prospecting",
startDate: "2026-12-01",
budgetUsdMinor: 250000,
bid: {
type: "fixed_cpm",
cpmUsdMinor: 2400
},
targeting: {
geo: {
countries: ["US"]
}
}
},
});
const creative = await waveband("POST", "/creatives", {
idempotencyKey: "spring-ctv-30",
body: {
workspaceId: workspaceId,
name: "Spring :30",
type: "video",
clickThroughUrl: "https://acmeoutdoors.com/spring",
assets: [
{
source: "https://cdn.acmeoutdoors.com/spring-30.mp4",
mimeType: "video/mp4",
width: 1920,
height: 1080,
durationSeconds: 30
}
]
},
});
await waveband("POST", `/line-items/${lineItem.id}/creatives`, {
body: { creativeId: creative.id },
});Check funding
const billing = await waveband("GET", "/billing");
// funded is false until the first top-up lands; nothing delivers before it.
if (billing.wallet && billing.wallet.availableUsdMinor < 500000) {
const { url } = await waveband("POST", "/billing/top-up-links", {
idempotencyKey: "top-up-2026-12",
body: { amountUsdMinor: 500000 },
});
// Send url to a person: they pay on the hosted page, then the wallet is credited.
}Forecast
Available impressions matching the targeting over the flight (at most 31 days, from tomorrow). It is not a delivery promise and not unique reach.
const forecast = await waveband("POST", "/forecasts", {
body: {
channel: "ctv",
targeting: {
geo: {
countries: ["US"]
}
},
startDate: "2026-12-01",
endDate: "2026-12-31"
},
});
if (forecast.status === "ok") {
console.log(forecast.impressions, forecast.low, forecast.high, forecast.unaccounted);
} else {
console.log(forecast.reason); // "busy": capacity used up for the minute, retry shortly
}Activate
Activate the campaign, then its line items. This spends money: the workspace needs its advertiser domain and category, and the account needs funds.
await waveband("PATCH", `/campaigns/${campaign.id}`, { body: { status: "active" } });
await waveband("PATCH", `/line-items/${lineItem.id}`, { body: { status: "active" } });Reports
const query = {
workspaceId: workspaceId,
from: "2026-12-01",
to: "2026-12-07",
groupBy: "campaign"
};
const { items } = await waveband("GET", "/reports", { query });
const csv = await waveband<string>("GET", "/reports", { query: { ...query, format: "csv" } });groupBy is day, campaign, line_item, creative, or channel. See Reports & metrics.
Errors
try {
await waveband("PATCH", `/line-items/${lineItem.id}`, { body: { budgetUsdMinor: 50 } });
} catch (err) {
if (err instanceof WavebandError && err.code === "WAVEBAND_BUDGET_BELOW_MINIMUM") {
console.log(err.message, err.details);
} else {
throw err;
}
}