KaroStudio API
Create and edit images in your project's style from your own code: your shop, your CMS, a nightly script or automations like Zapier and Make.
The API is a classic REST interface with JSON. It does everything the app does: read the style, create images and series, match photos, upscale, vectorize, convert and search the Collection. Every result follows your project's rules and lands in the Collection.
Getting started
API or MCP?#
Both use the same tokens and the same scopes. The difference is who makes the calls.
- Who calls itAn AI assistant like Claude or ChatGPTYour code, a shop, a CMS, Zapier
- Typical useIn conversation, e.g. "Make me 4 posts for the summer party"Automated and recurring, e.g. matching all new product photos every night
- Results arriveRight in the chatAs JSON with image URLs, by polling or webhook
- DocsThis page
| REST API | MCP | |
|---|---|---|
| Who calls it | Your code, a shop, a CMS, Zapier | An AI assistant like Claude or ChatGPT |
| Typical use | Automated and recurring, e.g. matching all new product photos every night | In conversation, e.g. "Make me 4 posts for the summer party" |
| Results arrive | As JSON with image URLs, by polling or webhook | Right in the chat |
| Docs | This page | MCP docs |
Quickstart#
From token to first image in three steps.
- 1
Create a token
In KaroStudio, open Account settings, then AI connection and API. Choose Create token, then pick the scopes and the project.
- 2
Start a job
Terminalcurl -X POST "https://api.karostudio.example/v1/projects/hafenkueche/images" \ -H "Authorization: Bearer <YOUR_TOKEN>" \ -H "Content-Type: application/json" \ -d '{"template":"social-media","subjects":["Summer at the harbor"]}'The response comes back right away and includes the job ID.
- 3
Fetch the result
Terminalcurl "https://api.karostudio.example/v1/jobs/job_7f3c2a" \ -H "Authorization: Bearer <YOUR_TOKEN>"Once it says
"status": "succeeded", you'll find the image URLs underimages. Instead of polling, you can also pass a webhook.
Basics
Authentication#
Every request sends the token in a header. Requests without HTTPS are rejected.
Authorization: Bearer <YOUR_TOKEN>projects:readSee projects and templatesstyle:readRead the style and rules. Changes are only possible in the app.images:writeCreate, match, upscale, vectorize, convert, uploadcollection:readFind resultscollection:writeRename, mark and delete results
| Scope | Allows |
|---|---|
projects:read | See projects and templates |
style:read | Read the style and rules. Changes are only possible in the app. |
images:write | Create, match, upscale, vectorize, convert, upload |
collection:read | Find results |
collection:write | Rename, mark and delete results |
Jobs#
Creating images takes a few seconds. That's why creating calls respond right away with 202 and a job ID. Get the status via GET /jobs/{job} or have a webhook notify you.
queuedAccepted, waiting to start.runningIn progress. progress shows the share done in percent.succeededAll images are ready.partialSome subjects are done, others failed. Each image lists its reason.failedNothing could be created. error gives the reason.canceledCanceled by you.
| Status | Meaning |
|---|---|
queued | Accepted, waiting to start. |
running | In progress. progress shows the share done in percent. |
succeeded | All images are ready. |
partial | Some subjects are done, others failed. Each image lists its reason. |
failed | Nothing could be created. error gives the reason. |
canceled | Canceled by you. |
Webhooks#
Pass a URL in webhook when you start a job. When the job is done, we send a POST there. If your URL doesn't answer with a 2xx, we retry up to [Number] times.
{
"event": "job.succeeded",
"created_at": "2026-10-12T08:14:03Z",
"data": {
"job": "job_7f3c2a",
"project": "hafenkueche",
"images": [{ "id": "img_01", "url": "https://…/img_01.png" }]
}
}Every message is signed with your webhook secret. Check the signature in the KaroStudio-Signature header before you trust the message:
import crypto from "node:crypto";
// header: value of KaroStudio-Signature, e.g. "t=1791800043,v1=5f2b…"
function isValid(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const stale = Math.abs(Date.now() / 1000 - Number(parts.t)) > 300; // older than 5 minutes
return !stale && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}job.succeededAll images of a job are readyjob.partialDone, but not every subject workedjob.failedThe job failed
| Event | When |
|---|---|
job.succeeded | All images of a job are ready |
job.partial | Done, but not every subject worked |
job.failed | The job failed |
Pagination#
Lists come in pages. limit sets the page size, and next in the response is the cursor for the next page. When it's null, you've reached the end.
curl "https://api.karostudio.example/v1/projects/hafenkueche/collection?limit=50&after=c_b01" \
-H "Authorization: Bearer <YOUR_TOKEN>"Versioning#
The version is part of the path: /v1. Within a version we only add things, like new fields. Ignore unknown fields and your code will never break. Breaking changes only come with a new version and [Notice period] of notice.
Reference
Projects#
Style#
Create images#
Jobs#
Edit images#
Uploads#
Collection#
Errors#
Errors come with a matching HTTP status and always have the same shape. Include the request_id when you contact support.
{
"error": {
"code": "forbidden",
"message": "This token can't use the project nordgarten.",
"request_id": "req_7d2e"
}
}- 400A field is missing or has the wrong type. The message names the field.
invalid_request - 401The token is missing, mistyped or revoked.
unauthenticated - 402Your quota for this period is used up.
quota_exceeded - 403The token lacks the scope, or the project isn't shared with it.
forbidden - 404The project, job or image doesn't exist.
not_found - 409The job conflicts with a locked rule.
rule_locked - 422The file is damaged or in an unsupported format.
invalid_file - 429Too many requests. The Retry-After header gives the seconds to wait.
rate_limited - 500Something went wrong on our side. Share the request_id and support can help quickly.
server_error
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | A field is missing or has the wrong type. The message names the field. |
| 401 | unauthenticated | The token is missing, mistyped or revoked. |
| 402 | quota_exceeded | Your quota for this period is used up. |
| 403 | forbidden | The token lacks the scope, or the project isn't shared with it. |
| 404 | not_found | The project, job or image doesn't exist. |
| 409 | rule_locked | The job conflicts with a locked rule. |
| 422 | invalid_file | The file is damaged or in an unsupported format. |
| 429 | rate_limited | Too many requests. The Retry-After header gives the seconds to wait. |
| 500 | server_error | Something went wrong on our side. Share the request_id and support can help quickly. |
Limits#
- Requests per minute[Number] per token
- Subjects per job[Number]
- Upload file size[Size] MB
- QuotaPer your plan: [Plan]
| What | Limit |
|---|---|
| Requests per minute | [Number] per token |
| Subjects per job | [Number] |
| Upload file size | [Size] MB |
| Quota | Per your plan: [Plan] |
Every response includes the RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers, so you can see ahead of time when to slow down.
Tools
OpenAPI file#
The whole API as OpenAPI 3.1. Use it to generate clients for almost any language, import every endpoint into Postman or Insomnia, or open it in the Swagger Editor.
karostudio-openapi.json
13 endpoints, version 1.0.0
npx @openapitools/openapi-generator-cli generate \
-i karostudio-openapi.json -g typescript-fetch -o ./karostudioStill have questions? Write to us. We'll help with your integration.
Contact us