Swipe file
A searchable library of reference images. Upload competitor ads and inspiration, then find them by natural-language search.
A swipe file is a lightweight, org-scoped library of static reference images — competitor ads, inspiration, anything you want to point at later. Each upload is embedded and auto-classified so you can retrieve it with filtered vector search. Unlike design templates, swipe files are just the image plus metadata — no JSON tree, no reverse-engineering pipeline.
Mental model
Use a swipe file when you want to stash a reference image and find it fast, without turning it into a structured, editable end-product. On upload, each image is:
- Stored (compressed, with a thumbnail).
- Embedded with a multimodal model into a shared text/image vector space.
- Auto-classified into one
industryand onead_formatfrom our standard taxonomies.
You then retrieve images with filtered vector search: a text query is embedded and cosine-matched against the image embeddings, optionally narrowed by industry, ad_format, and aspect_ratio.
Your library is the union of your own uploads and the data sources your organization is subscribed to — curated collections managed by Static Ads Lab that you can toggle on or off at any time.
Swipe files are free — there is no per-image charge.
ID prefix: sf_ (swipe files), sfs_ (data sources).
Fields
| Field | Type | Description |
|---|---|---|
id | string | sf_… |
image_url | string | URL of the stored image |
thumbnail_url | string | null | URL of a smaller thumbnail |
width / height | number | null | Pixel dimensions |
aspect_ratio | string | null | e.g. "1:1", "4:5", "9:16" |
industry | string | null | Auto-classified Shopify L1 category |
ad_format | string | null | Auto-classified creative format |
source | enum | upload | import | studio_concept | sal_managed |
is_managed | boolean | True for designs from a managed data source (read-only) |
status | enum | processing | ready | failed |
error | string | null | Failure reason when status is failed |
created_at / updated_at | string | ISO timestamps |
Lifecycle
stateDiagram-v2
[*] --> processing: POST /v1/swipe-files (multipart upload)
processing --> ready: embed + classify succeed
processing --> failed: embedding failed
ready --> [*]
failed --> [*]Ingest is asynchronous. The upload returns immediately with status: "processing"; the item becomes ready once its embedding and classification finish. A swipe file only appears in search results once it is ready. If classification degrades, the item still becomes ready with industry/ad_format left null — only an embedding failure marks it failed.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /v1/swipe-files | Upload a reference image (multipart) |
GET | /v1/swipe-files | List swipe files with filters |
GET | /v1/swipe-files/:id | Get a swipe file |
DELETE | /v1/swipe-files/:id | Archive (soft-delete) a swipe file |
POST | /v1/swipe-files/:id/restore | Restore an archived swipe file |
POST | /v1/swipe-files/search | Filtered vector search |
GET | /v1/swipe-files/data-sources | List data sources with subscription state |
PUT | /v1/swipe-files/data-sources/:id/subscription | Enable or disable a data source |
Data sources
Beyond your own uploads, your swipe file can pull from data sources: shared, Static Ads Lab-curated collections of reference ads. New organizations are automatically subscribed to the default collection, so your library is stocked from day one. Subscriptions are per-organization and idempotent to toggle.
- Designs from a subscribed source appear in your list and search results alongside your own uploads, marked with
is_managed: trueandsource: "sal_managed". - Managed designs are read-only: archiving, restoring, or deleting them returns
SWIPE_FILE_READ_ONLY. To hide them, disable the source. - Data sources are free — the curated content adds no charges to your account.
List available data sources
const response = await fetch("https://api.staticadslab.com/v1/swipe-files/data-sources", {
headers: { "X-API-Key": "YOUR_API_KEY" },
});
const { data } = await response.json();
for (const source of data) {
console.log(source.id, source.name, source.subscribed, source.item_count);
}
// sfs_all_sal_ads "All Static Ads Lab Ads" true 240Enable or disable a data source
await fetch(
"https://api.staticadslab.com/v1/swipe-files/data-sources/sfs_all_sal_ads/subscription",
{
method: "PUT",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ active: false }), // true re-enables
},
);Scope list/search to specific sources
Both GET /v1/swipe-files and POST /v1/swipe-files/search accept data_source_ids and include_own:
const response = await fetch("https://api.staticadslab.com/v1/swipe-files/search", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "bold typography sale announcement",
data_source_ids: ["sfs_all_sal_ads"], // only this source's designs
include_own: false, // exclude my own uploads
}),
});Omit data_source_ids to search everything you are subscribed to; pass data_source_ids: [] with include_own: true to search only your own uploads.
Common patterns
Upload a reference image
Upload raw bytes directly with multipart/form-data — no need to pre-host the image. search and read require the swipe_files:read scope; uploads require swipe_files:write.
const form = new FormData();
form.append("file", fileBlob, "competitor-ad.png"); // File or Blob
const response = await fetch("https://api.staticadslab.com/v1/swipe-files", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
// Do NOT set Content-Type — the runtime adds the multipart boundary.
},
body: form,
});
const { data } = await response.json();
console.log(data.id, data.status); // sf_..., "processing"Then poll until ready:
async function waitForSwipeFile(id) {
while (true) {
const r = await fetch(
`https://api.staticadslab.com/v1/swipe-files/${id}`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } },
);
const { data } = await r.json();
if (data.status === "ready") return data;
if (data.status === "failed") throw new Error(data.error ?? "Ingest failed");
await new Promise((res) => setTimeout(res, 2000));
}
}Search by description
const response = await fetch("https://api.staticadslab.com/v1/swipe-files/search", {
method: "POST",
headers: {
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "minimalist beige flatlay with soft shadows",
ad_format: ["Sale", "Offer"], // optional filters; string or array (matches any)
aspect_ratio: "1:1",
limit: 20,
}),
});
const { data } = await response.json();
for (const hit of data) {
console.log(hit.id, hit.similarity, hit.image_url);
}Every metadata filter (industry, ad_format, aspect_ratio) accepts a single
string or an array of strings. Multiple values on one dimension match any of
them; different dimensions combine with AND.
List and filter without a query
// Repeat a query param to match any of multiple values on that dimension.
const params = new URLSearchParams({ status: "ready" });
params.append("industry", "Apparel & Accessories");
params.append("industry", "Health & Beauty");
const response = await fetch(
`https://api.staticadslab.com/v1/swipe-files?${params}`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } },
);
const { data, has_more, total_count } = await response.json();total_count is the exact number of designs matching your filters, independent
of the page size — useful for a "20 of 231" style label before you have loaded
everything.
Discovering which filter values exist
A library you did not upload yourself (a subscribed data source, for instance) can hold industries and formats you have no way to guess. Ask for the available values instead of inferring them from a page of results:
const params = new URLSearchParams({
limit: "20",
facets: "industry,ad_format,aspect_ratio",
});
const response = await fetch(
`https://api.staticadslab.com/v1/swipe-files?${params}`,
{ headers: { "X-API-Key": "YOUR_API_KEY" } },
);
const { data, total_count, facets } = await response.json();
// facets.industry -> [{ value: "Apparel & Accessories", count: 116 }, ...]
// Each facet key is the filter parameter name, and each value is a valid value
// for it, so a facet round-trips into a filter directly.Counts exclude their own dimension's filter: with industry applied, the
industry facet still lists every industry (so you can switch), while
ad_format and aspect_ratio narrow to the selected industry. See
Pagination for the full convention.
Pitfalls
- Ingest is async. A newly uploaded swipe file is
processingand will not appear in/searchresults until it isready. PollGET /v1/swipe-files/:idor list withstatus=processingto track it. searchis aPOSTbut read-intent — it requires theswipe_files:readscope, notwrite.- Supported upload formats are PNG, JPEG, and WebP. WebP is transcoded to PNG before embedding.
- Your library is org-scoped plus any subscribed data sources. If unexpected designs appear (or disappear), check
GET /v1/swipe-files/data-sources— a subscription toggle changes list and search results immediately. - Managed designs (
is_managed: true) cannot be archived or restored —DELETEreturnsSWIPE_FILE_READ_ONLY. Disable the data source instead. - Don't build filter dropdowns from the
industry/ad_formatvalues present in a page ofdata. Results are ordered newest-first, so one page is often dominated by a single industry and the rest of the library looks like it does not exist. Usefacets. - Facet counts describe the filtered library, not a
/searchresult set. Search is vector-ranked over a single page, so pair it with values rather than counts.
Related
Prompt for your agent
Read https://www.staticadslab.com/docs/resources/swipe-file.mdx and write a function that uploads a reference image to the swipe file via multipart POST /v1/swipe-files, polls until status is ready, then runs POST /v1/swipe-files/search with a text query and returns the top matches with their similarity scores.