Static Ads Lab
Reference

Pagination

How list endpoints paginate using the starting_after cursor pattern.

Static Ads Lab list endpoints return a fixed-size page plus a has_more flag. Pagination is cursor-based via the starting_after query parameter.

The list response shape

{
  "data": [
    { "id": "ia_a1b2c3d4e5f67890", "...": "..." },
    { "id": "ia_b2c3d4e5f6789012", "...": "..." }
  ],
  "has_more": true,
  "total_count": 357,
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-04-30T14:30:00.000Z"
  }
}
  • data is always an array.
  • has_more is true if more items exist after this page.
  • total_count is the exact number of items matching your filters, independent of pagination. Use it to show "20 of 357" before you have fetched all 357.
  • Default limit is 20; max is 100.

Cursor-based pagination with starting_after

Pass the last item's id as starting_after= to get the next page.

async function listAll(url, headers) {
  const all = [];
  let startingAfter;
  while (true) {
    const params = new URLSearchParams();
    if (startingAfter) params.set("starting_after", startingAfter);
    params.set("limit", "100");
    const r = await fetch(`${url}?${params}`, { headers });
    const { data, has_more } = await r.json();
    all.push(...data);
    if (!has_more) break;
    startingAfter = data[data.length - 1].id;
  }
  return all;
}

const allBrands = await listAll(
  "https://api.staticadslab.com/v1/brands",
  { "X-API-Key": process.env.SAL_API_KEY },
);

Filter parameters

Most list endpoints accept resource-specific filters in addition to starting_after and limit:

EndpointCommon filters
/v1/productsbrand_id
/v1/product-variantsproduct_id
/v1/audiencesproduct_id
/v1/imagesproduct_id, image_type (product, lifestyle, logo, background, other)
/v1/design-templatesstatus, industry, ad_format, deleted, ids
/v1/swipe-filesindustry, ad_format, aspect_ratio, status, archived, data_source_ids, include_own, ids
/v1/image-adsstatus, brand_id, product_id, product_variant_id, reference_ad_url, batch_id, editable, sku_code, search, ids

Some filters accept multiple values. On /v1/swipe-files, repeat the parameter (?industry=A&industry=B). On /v1/image-ads, comma-separate (?status=processing,completed). Values within one dimension match any; different dimensions combine with AND.

See each endpoint's API reference page for the full filter list.

Facets: discovering filter values

Rather than guessing which filter values exist, ask for them. Name the dimensions you want in the facets parameter and the response gains a facets object of values with exact counts.

const params = new URLSearchParams({
  limit: "20",
  facets: "industry,aspect_ratio",
});

const r = await fetch(`https://api.staticadslab.com/v1/swipe-files?${params}`, {
  headers: { "X-API-Key": process.env.SAL_API_KEY },
});
const { data, total_count, facets } = await r.json();

// facets.industry -> [{ value: "Apparel & Accessories", count: 116 }, ...]
{
  "data": ["..."],
  "has_more": true,
  "total_count": 231,
  "facets": {
    "industry": [
      { "value": "Apparel & Accessories", "count": 116 },
      { "value": "Health & Beauty", "count": 43 }
    ],
    "aspect_ratio": [
      { "value": "1:1", "count": 125 },
      { "value": "4:5", "count": 66 }
    ]
  }
}

Supported dimensions:

EndpointFacet dimensions
/v1/swipe-filesindustry, ad_format, aspect_ratio
/v1/design-templatesindustry, ad_format, status
/v1/image-adsstatus

A facet key is a filter parameter

Each facet key is the name of the query parameter that filters on it, and each facet value is a valid value for that parameter. So you turn a facet straight back into a filter with no mapping:

const top = facets.industry[0];
const narrowed = await fetch(
  `https://api.staticadslab.com/v1/swipe-files?industry=${encodeURIComponent(top.value)}`,
  { headers: { "X-API-Key": process.env.SAL_API_KEY } },
);
// The narrowed total_count equals top.count exactly.

Counts exclude their own dimension's filter

Each dimension's counts honour every other active filter but not its own. With industry=Apparel & Accessories applied:

  • the industry facet still lists every industry, with library-wide counts, so you can offer a switch to another one;
  • the ad_format and aspect_ratio facets narrow to Apparel only, so their counts describe what you would actually get.

This is standard disjunctive faceting. It means a count always predicts the size of the next refinement.

Notes

  • Facets are opt-in. Omit the parameter and nothing is computed.
  • Counts are exact. At most 100 values per dimension, ordered by count descending.
  • Values with zero matches are omitted; every dimension you request is present, as an empty array if nothing matched.
  • An unrecognised dimension returns 400 VALIDATION_ERROR listing the allowed names, rather than being ignored.
  • Because facets ignore the filter for their own dimension, the block is identical across pages of the same filter set. Request it on the first page only.

Batch reading by IDs

For image ads and design templates, you can read up to N specific resources in one call without iterating, by passing a comma-separated ids= parameter. When ids= is provided, cursor pagination is ignored.

GET /v1/image-ads?ids=ia_a,ia_b,ia_c
GET /v1/design-templates?ids=dt_a,dt_b

This is the recommended pattern for batch polling. See Async jobs.

Pitfalls

  • Don't use data.length === limit as a stop condition — the API may return fewer items than limit even when has_more is true. Always trust has_more.
  • starting_after is the ID of an item you've already received. Don't synthesize cursors client-side.
  • Parallel cursor requests aren't supported — paginate serially.
  • Some legacy patterns refer to cursor — the actual parameter is starting_after.
  • Don't derive a total from data.length across pages while you are still paginating; read total_count instead.
  • Don't rebuild filter dropdowns from the values present in data — one page is not the whole result set. That's what facets is for.