Create jobs
Render up to 10 creatives in one call, each a page or a video with its own values. Optionally start them at once and get the files back in the same call.
A job is one file: a page (or a flow, which renders as a video) filled with modifications. One request creates 1 to 10 jobs, which suits a carousel, every size of an ad, or two variants of one page. For more than 10 files, or rows from a sheet, use a batch.
POST /api/jobsRequires batches:create, plus renders:create when run is true.
Request body
{
"workspace": "acme",
"jobs": [
{
"page": "k7fq2m8a",
"modifications": {
"Headline": "Weekend offer",
"Price": "<s1>₹8,999</s1> <s2>₹12,999</s2>",
"Photo": { "asset": "shoe.png", "fit": "cover", "zoom": 1.2, "panX": 0.1 },
"Badge": { "visible": false },
"Subline": null
},
"output": { "format": "png", "fileName": "weekend-offer-feed" }
},
{
"page": "p3rtv9we",
"modifications": { "Headline": "Weekend offer" },
"output": { "format": "jpeg" }
},
{
"flow": "f2dm6x9r",
"modifications": { "k7fq2m8a#2@Headline": "Last chance" },
"output": { "format": "mp4" }
}
],
"run": true,
"webhookUrl": "https://example.com/hooks/kalpana"
}| Field | Required | Description |
|---|---|---|
workspace | Yes | Workspace slug or id. |
jobs | Yes | 1 to 10 jobs. See below. |
batch | No | Adds every job to that batch (its id). The batch must exist and must not be archived. Without it the jobs stand alone. |
run | No | false (default) saves the jobs as pending and spends nothing. true starts rendering at once. |
webhookUrl | No | Sends each job's result to your URL. See Callback URLs. API keys only. It must be a public https URL of up to 2048 characters, with no username or password in it and no port below 1024 other than 443. |
Fields the API does not know are refused, so a misspelt option never passes silently.
A job
| Field | Required | Description |
|---|---|---|
page | One of page or flow | The id of the page to render. Take it from pages in Get a template. |
flow | One of page or flow | The id of a flow, a video that joins pages. Its output.format must be a video format. |
modifications | No | Layer to value. See Modifications. Without it the page renders as designed. |
output | No | What to make. See below. |
A job is one page, so a key in modifications that names another page (<pageId>@<layer> with a different page) is refused. To fill several pages, make one job per page.
Output
| Field | Description |
|---|---|
format | png (default), jpeg or webp for an image; mp4 or gif for a video. A video format needs an animated page or a flow. |
fileName | The file's name without its extension, up to 200 characters. Without it, a job on its own is named after the template and, when it has several pages, the page; a job in a batch follows the batch's file name pattern. |
stillTime | For an image of an animated page: the second of the motion to show, from 0 to 600. Without it the page's own still time applies, else its last frame. Not for video. |
Fields other than these are refused. Options such as scale and quality will arrive here without changing the shape.
Credits
When run is true, the cost of the whole request is checked before any job starts, and the call fails with 402 if there are not enough credits. Nothing is created. The error's data holds cost and remaining. Each job's creditCost is charged once, only when its render succeeds; a failed render is not charged. With run set to false nothing is spent.
Responses
The body is the same in every case: a status for the request and the jobs, in the order you sent them.
| Case | Status | Request status |
|---|---|---|
| Not run | 201 | pending |
| Run, and every job finished within the wait | 201 | done (each job says success or failed) |
| Run, and a job is still rendering when the wait ends | 202 | processing |
A 202 also carries these headers:
| Header | Value |
|---|---|
Location | /api/jobs?workspace=acme&ids=<id>,<id>, the jobs to poll with Get jobs. |
Retry-After | 5, the seconds to wait between polls. |
A request is done once no job is rendering or waiting. Read each job's status to see how it went: one job failing does not fail the request.
How long the call waits
The call holds its answer for up to 30 seconds. Most images finish well within that, so one call usually returns the files.
Send the standard Prefer header to change the wait:
| Header | Effect |
|---|---|
Prefer: wait=N | Wait up to N seconds. The value is capped at 30. |
Prefer: wait=0 | Do not wait. You get the jobs back at once. |
An unreadable Prefer value is ignored, and the default wait applies. Send Prefer: wait=N when your client has its own timeout shorter than 30 seconds, such as wait=12 for a client that gives up after 15. You then get the ids back with a 202 instead of being cut off with no answer.
If your client disconnects anyway, nothing is lost. The jobs keep rendering, are charged once on success, and their files show in List creatives. Every call creates new jobs, so before you repeat a call that timed out, check the creatives list.
Example
curl --request POST "https://app.kalpana.one/api/jobs" \
--header "x-api-key: $KALPANA_API_KEY" \
--header "content-type: application/json" \
--header "Prefer: wait=25" \
--data '{
"workspace": "acme",
"jobs": [
{
"page": "k7fq2m8a",
"modifications": { "Headline": "Weekend offer", "Photo": "shoe.png" }
},
{
"page": "p3rtv9we",
"modifications": { "Headline": "Weekend offer", "Photo": "shoe.png" },
"output": { "format": "jpeg" }
}
],
"run": true
}'Finished in time: 201
{
"status": "done",
"jobs": [
{
"id": "7b0c2d1e-4f5a-4c8b-9e3d-2a6f1b8c0d11",
"status": "success",
"template": { "id": "t4hz8c1n", "name": "Weekend sale" },
"page": { "id": "k7fq2m8a", "name": "Feed" },
"flow": null,
"batch": null,
"modifications": { "Headline": "Weekend offer", "Photo": { "asset": "0b0f5a3e-5b1c-4d65-9a5e-3f7d2f2f6c11", "name": "shoe.png" } },
"output": { "format": "png", "kind": "image" },
"creditCost": 1,
"error": null,
"file": {
"creativeId": "5d3a9c70-1e2b-4f86-a7c4-90b1d2e3f4a5",
"url": "https://cdn.kalpana.one/...",
"fileName": "weekend-sale-feed.png",
"contentType": "image/png",
"sizeBytes": 284113,
"width": 1080,
"height": 1080
},
"createdAt": "2026-11-09T09:41:07.512Z",
"updatedAt": "2026-11-09T09:41:11.208Z"
},
{
"id": "c19e5a44-8d70-4b2f-b6a1-3e7d9f0a2c58",
"status": "failed",
"template": { "id": "t4hz8c1n", "name": "Weekend sale" },
"page": { "id": "p3rtv9we", "name": "Story" },
"flow": null,
"batch": null,
"modifications": { "Headline": "Weekend offer", "Photo": { "asset": "0b0f5a3e-5b1c-4d65-9a5e-3f7d2f2f6c11", "name": "shoe.png" } },
"output": { "format": "jpeg", "kind": "image" },
"creditCost": 1,
"error": "The render failed.",
"file": null,
"createdAt": "2026-11-09T09:41:07.512Z",
"updatedAt": "2026-11-09T09:41:10.640Z"
}
]
}A 201 means every job has settled, not that every job succeeded. Check each job's status.
Still rendering: 202
HTTP/1.1 202 Accepted
Location: /api/jobs?workspace=acme&ids=3f6a1c9e-2b84-4d07-a5e1-7c0d8b9a4f12
Retry-After: 5{
"status": "processing",
"jobs": [
{
"id": "3f6a1c9e-2b84-4d07-a5e1-7c0d8b9a4f12",
"status": "processing",
"template": { "id": "t4hz8c1n", "name": "Weekend sale" },
"page": null,
"flow": { "id": "f2dm6x9r", "name": "Launch video" },
"batch": null,
"modifications": { "k7fq2m8a#2@Headline": "Last chance" },
"output": { "format": "mp4", "kind": "video" },
"creditCost": 5,
"error": null,
"file": null,
"createdAt": "2026-11-09T09:41:07.512Z",
"updatedAt": "2026-11-09T09:41:07.901Z"
}
]
}Poll the Location URL with Get jobs until the request status is done, or wait for the callback.
Refused: 400 with several issues
Every problem across every job comes back in one answer, so you fix the request in one pass. Nothing is saved.
{
"defined": true,
"code": "BAD_REQUEST",
"status": 400,
"message": "Invalid jobs. jobs[0].Headlin: No layer \"Headlin\" in this template. Its layers: Headline, Photo, Badge. jobs[1].output.format: A flow is a video: use mp4 or gif. jobs[2].page: No page \"zzzz0000\" in this workspace.",
"data": {
"issues": [
{ "job": 0, "field": "Headlin", "code": "unknown_layer", "message": "No layer \"Headlin\" in this template. Its layers: Headline, Photo, Badge." },
{ "job": 1, "field": "output.format", "code": "bad_value", "message": "A flow is a video: use mp4 or gif." },
{ "job": 2, "field": "page", "code": "unknown_page", "message": "No page \"zzzz0000\" in this workspace." }
]
}
}Not enough credits: 402
{
"defined": true,
"code": "PAYMENT_REQUIRED",
"status": 402,
"message": "These jobs cost 7 credits; 3 are left.",
"data": { "cost": 7, "remaining": 3 }
}The job object
| Field | Description |
|---|---|
id | The job's id (a UUID). |
status | pending, processing, success, failed or cancelled. |
template | { id, name } of the template. |
page, flow | { id, name } of what it renders. The other is null. |
batch | The batch the job belongs to. null when it stands alone. |
modifications | The values the job was made from, as the server read them: text as a string, an image as { asset, name }, plus any framing or visible. null for a job made in the Kalpana app. |
output | { format, kind }: what the job makes. kind is image or video. |
creditCost | Credits charged when the render succeeds. |
error | Why the render failed. null unless status is failed. |
file | null until the job succeeds. Then creativeId, url (a download link), fileName, contentType, sizeBytes, width and height. |
createdAt, updatedAt | Timestamps. |
output is what the job makes; file is what it made.
Using it from n8n or Zapier
One HTTP step with run set to true usually returns the files, so the next step can use jobs[0].file.url straight away. For the rare slow render, read the Location header and poll Get jobs, or pass webhookUrl and continue when the callback arrives.
Errors
| Status | Cause |
|---|---|
400 | The request is not valid. data.issues lists every problem with its job index, field, code and message: an unknown layer, a page or flow not in the workspace, a value of the wrong form, an output the page cannot make (a video format on a page with no motion, stillTime on a video), an unknown field, more than 10 jobs, an archived batch, or a webhookUrl that is not allowed. A request without an API key, such as one from the Studio, cannot use webhookUrl. |
402 | Not enough credits to run, or webhookUrl is set and the organization has no active subscription. |
403 | Your role or key lacks batches:create, or lacks renders:create when run is true. |
404 | The batch is not in the workspace. |
413 | The body is larger than 4 MiB. |
See Errors for the error body.
Updated 10 Nov 2026