Stashr docs
API Recipes
Choose Node.js, Python, PHP, Go, or cURL on any example. Your choice updates every example on the page and is remembered as you move through the documentation.
These recipes use the production management API and a server-side STASHR_API_KEY environment variable. Node.js examples use the native APIs in Node.js 22. Python examples require Python 3 and the requests package. PHP examples require the cURL extension. Go examples use only the standard library.
Start with the setup block for your language. The request examples below assume those variables and helpers already exist. Set STASHR_PROJECT_ID, STASHR_FOLDER_ID, and STASHR_ASSET_ID only for examples that address an existing resource by ID.
const baseUrl = "https://stashr.dev";
const apiKey = process.env.STASHR_API_KEY;
const projectId = process.env.STASHR_PROJECT_ID || "";
const folderId = process.env.STASHR_FOLDER_ID || "";
const assetId = process.env.STASHR_ASSET_ID || "";
const authHeaders = { Authorization: "Bearer " + apiKey };JSON requests build a small native payload object first, then serialize it at the HTTP boundary. Upload requests use the same pattern with a metadata object plus a separate binary file part.
Keep API keys on a trusted server, fail requests when the response status is not successful, and inspect the common JSON error body before retrying. ID-based examples read STASHR_PROJECT_ID, STASHR_FOLDER_ID, or STASHR_ASSET_ID from the environment.
Discover The Workspace
Confirm which workspace the key belongs to before an import or migration:
const response = await fetch(baseUrl + "/api/v1/workspace", {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Projects
List Projects
Use the response to discover project IDs, slugs, and current asset counts:
const response = await fetch(baseUrl + "/api/v1/projects", {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Create A Project
const payload = {
"name": "Website",
"slug": "website",
"description": "Website assets"
};
const response = await fetch(baseUrl + "/api/v1/projects", {
method: "POST",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Project slugs cannot be changed because they form part of every public asset URL.
Get A Project
const response = await fetch(baseUrl + "/api/v1/projects/" + projectId, {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Update A Project
Only name and description are editable:
const payload = {
"name": "Website",
"description": "Website assets"
};
const response = await fetch(baseUrl + "/api/v1/projects/" + projectId, {
method: "PATCH",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Delete A Project
Deletion requires the current project slug in confirmSlug and permanently removes the project's folders, assets, and stored files:
const payload = {
"confirmSlug": "website"
};
const response = await fetch(baseUrl + "/api/v1/projects/" + projectId, {
method: "DELETE",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(await response.text());Folders
List Folders
const response = await fetch(baseUrl + "/api/v1/folders?projectSlug=website", {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Create A Folder
const payload = {
"projectSlug": "website",
"path": "brand/launch",
"description": "Launch assets"
};
const response = await fetch(baseUrl + "/api/v1/folders", {
method: "POST",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Uploads create missing intermediate folders automatically, so explicit folder creation is optional.
Get A Folder
const response = await fetch(baseUrl + "/api/v1/folders/" + folderId + "?projectSlug=website", {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Move, Rename, Or Describe A Folder
Update path, name, description, or a combination of them:
const payload = {
"path": "brand/launch",
"description": "Launch assets"
};
const response = await fetch(baseUrl + "/api/v1/folders/" + folderId, {
method: "PATCH",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Delete An Empty Folder By ID
const response = await fetch(baseUrl + "/api/v1/folders/" + folderId, {
method: "DELETE",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());Delete A Folder Tree By Path
Use recursive=true only when every descendant asset and folder should also be deleted:
const response = await fetch(baseUrl + "/api/v1/folders?projectSlug=website&path=brand%2Flaunch&recursive=true", {
method: "DELETE",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());Assets
Upload An Asset
import { readFile } from "node:fs/promises";
const metadata = {
"projectSlug": "website",
"folderPath": "brand/launch",
"title": "Hero image",
"altText": "Dashboard preview on a laptop",
"tags": ["hero", "launch"],
"storeOriginalImages": false
};
const form = new FormData();
form.set("metadata", JSON.stringify(metadata));
form.set("file", new Blob([await readFile("./hero.png")], { type: "image/png" }), "hero.png");
const response = await fetch(baseUrl + "/api/v1/assets", {
method: "POST",
headers: authHeaders,
body: form
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());The upload has two multipart parts: a JSON metadata object and the binary file. file and either metadata.projectSlug or metadata.projectId are required. metadata.folderPath, metadata.title, metadata.altText, metadata.tags, and metadata.storeOriginalImages are optional. Do not set the overall multipart Content-Type header yourself; the HTTP library adds the required boundary. The maximum upload size is 100 MB, and workspace storage and plan media restrictions still apply.
Existing integrations may continue sending metadata as individual multipart fields. New integrations should use the JSON metadata part shown above, and must not mix the two styles in one request.
List And Paginate Assets
This request lists a whole project recursively. Follow pagination.nextPage until it is null:
const response = await fetch(baseUrl + "/api/v1/assets?projectSlug=website&recursive=true&page=1&limit=100", {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Omit recursive, page, and limit for the compatibility behavior: an unpaginated exact-folder listing. Supplying either pagination field activates pagination with defaults of page=1 and limit=100.
Look Up Metadata By Path
const response = await fetch(baseUrl + "/api/v1/assets/lookup?projectSlug=website&path=brand%2Flaunch%2Fhero.optimized.webp", {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Use the stored path returned by an upload or listing response. The visible logical filename can differ from the optimized stored filename.
Get Metadata By ID
const response = await fetch(baseUrl + "/api/v1/assets/" + assetId, {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Update, Move, Or Rename
const payload = {
"folderPath": "brand/launch",
"filename": "hero.optimized.webp",
"title": "Hero image",
"altText": "Dashboard preview on a laptop",
"tags": ["hero", "launch"]
};
const response = await fetch(baseUrl + "/api/v1/assets/" + assetId, {
method: "PATCH",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Renaming keeps the existing extension. For an asset with original and optimized variants, use variantFilenames.optimized and variantFilenames.original when their extensions differ.
Download By ID
import { writeFile } from "node:fs/promises";
const response = await fetch(baseUrl + "/api/v1/assets/" + assetId + "/file", {
method: "GET",
headers: authHeaders,
redirect: "follow"
});
if (!response.ok) throw new Error(await response.text());
await writeFile("./asset", Buffer.from(await response.arrayBuffer()));The file route returns a 302 redirect, and each example follows it. For published content, use the public deliveryUrl instead.
Download By Stored Path
The compatibility by-path route also delivers the file:
import { writeFile } from "node:fs/promises";
const response = await fetch(baseUrl + "/api/v1/assets/by-path/website/brand/launch/hero.optimized.webp", {
method: "GET",
headers: authHeaders,
redirect: "follow"
});
if (!response.ok) throw new Error(await response.text());
await writeFile("./asset", Buffer.from(await response.arrayBuffer()));Delete By ID
const response = await fetch(baseUrl + "/api/v1/assets/" + assetId, {
method: "DELETE",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());Delete By Stored Path
const response = await fetch(baseUrl + "/api/v1/assets/by-path/website/brand/launch/hero.optimized.webp", {
method: "DELETE",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());Usage
Check storage, delivery, project limits, counts, and media breakdown before large imports:
const response = await fetch(baseUrl + "/api/v1/usage", {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Upload Optimization Settings
Read the workspace defaults:
const response = await fetch(baseUrl + "/api/v1/settings/optimization", {
method: "GET",
headers: authHeaders
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());Update quality, original-image storage, or both:
const payload = {
"quality": 82,
"storeOriginalImages": true
};
const response = await fetch(baseUrl + "/api/v1/settings/optimization", {
method: "PATCH",
headers: { ...authHeaders, "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());These settings affect files when they are stored. Public, on-demand image transforms are URL parameters and do not use the management API or an API key. See Image transforms.