Developer docs
Apps (workflows)
One-click multi-step recipes — list, run, poll, and write your own.
An app is a small recipe that chains steps: a chat step can write a prompt, an image step renders it, a video step animates the result. You fill in a handful of inputs and get every step's output back in one run. Poremu ships built-in apps (storyboard, product-shot, photo-to-video, …) and you can publish your own.
Every route below works with an API key (Authorization: Bearer pk_live_…) and lives under /v1/workflows.
List apps
GET /v1/workflows?scope=builtin|mine|community&category=photo&limit=24&cursor=…Returns { items: WorkflowDto[], nextCursor }. scope=builtin (default) returns Poremu's templates (ownerId: "builtin"), mine your own, community public apps made by other users (sorted by run count). Each item carries its full definition (inputs, steps), creditsFrom (price of one run with default inputs) and counters.
GET /v1/workflows/{id}id is a built-in slug (storyboard) or a workflow uuid.
Apps that use an unfiltered model (uncensored: true) are only listed and runnable for adult-verified accounts with unfiltered models enabled.
Inputs
inputs[] describes what a run needs:
type | Value you send | Notes |
|---|---|---|
text, textarea | string ≤ 4000 chars | free text |
select | one of options[].value | the option label is what the prompt sees |
number | number | |
image | https URL or an uploaded asset id — or an array of them | up to max images (default 1); see Uploading images |
required inputs must be present (a default counts). Unknown keys are rejected.
Estimate
POST /v1/workflows/{id}/estimate
{ "inputs": { "idea": "a courier robot in a rainy neon city" } }→ { credits, balance, sufficient }. The estimate is the sum of the steps: image/video steps are priced like a normal generation with the step's params; chat steps are priced on the rendered prompt plus the step's maxOutputTokens. Missing images are fine for estimates.
Run
POST /v1/workflows/{id}/run
{
"inputs": { "idea": "a courier robot in a rainy neon city", "style": "anime" },
"visibility": "private"
}Returns a WorkflowRunDto immediately:
{
"id": "6d4e…", "workflowId": "storyboard", "workflowName": "Storyboard",
"status": "QUEUED", "currentStep": 0,
"inputs": { "idea": "…", "style": "anime" },
"steps": [
{ "stepId": "shots", "type": "chat", "status": "QUEUED", "text": null, "generation": null, "credits": 1 },
{ "stepId": "board", "type": "image", "status": "QUEUED", "text": null, "generation": null, "credits": 20 }
],
"credits": 21, "error": null, "createdAt": "…", "completedAt": null
}- The full estimate is reserved up front (one
RESERVEentry withworkflowRunIdin your credit history). Image/video steps are charged from that reserve as they finish, chat steps at their actual token usage. When the run ends, the unused part is refunded.creditson a finished run is what you actually paid. - Each step's
creditsis the estimate until the step finishes, then the actual charge. - Every image/video step creates a regular generation (visible in
GET /, carriesv1/ generations workflowRunId).steps[].generationis that generation with itsoutputs[]. visibility: "private"is honoured on paid plans; free accounts publish the final output on success.- Runs count against your plan's concurrency (
429 CONCURRENCY_LIMIT), separately from stand-alone generations. - All text inputs go through the content policy check (
422 PROMPT_BLOCKED).
Poll or wait
GET /v1/workflows/runs/{id}
GET /v1/workflows/runs/{id}/wait?timeout=30000 # long-poll, max 60 s
GET /v1/workflows/runs?limit=20&cursor=…&workflowId=storyboard&status=SUCCEEDEDRun statuses are the same as generations: QUEUED → PROCESSING → SUCCEEDED | FAILED | CANCELED. currentStep is the index of the step being worked on; steps[i].status tells you where each step is. On failure error names the step's error code (CONTENT_BLOCKED, PROVIDER_ERROR, TIMEOUT, …) and later steps are marked CANCELED. A step that runs longer than 20 minutes fails the run.
Web clients receive workflow_run.updated events on the realtime socket on every change (see Generation lifecycle for the socket); very large payloads are trimmed — refetch the run when status is terminal.
Cancel
POST /v1/workflows/runs/{id}/cancelStops the run: queued step generations are cancelled, a step that is already rendering finishes on its own, and the unused reserve is refunded.
Write your own app
POST /v1/workflows
{
"name": "Poster from a sketch",
"description": "Turns a rough sketch into a printable poster",
"category": "design",
"visibility": "public",
"inputs": [
{ "key": "sketch", "label": "Sketch", "type": "image", "required": true },
{ "key": "mood", "label": "Mood", "type": "select", "default": "bold",
"options": [ { "value": "bold", "label": "Bold, high contrast" }, { "value": "soft", "label": "Soft pastel" } ] },
{ "key": "notes", "label": "Notes", "type": "text" }
],
"steps": [
{ "id": "brief", "type": "chat", "title": "Write the brief", "model": "gpt-5.4-mini",
"system": "You write concise image prompts. English only, max 60 words, no preamble.",
"prompt": "Poster mood: {{input.mood}}. Extra notes: {{input.notes}}", "maxOutputTokens": 150 },
{ "id": "poster", "type": "image", "title": "Render", "model": "nano-banana-pro",
"images": ["input.sketch"], "prompt": "{{steps.brief.text}}",
"params": { "aspectRatio": "2:3", "resolution": "2k" } }
]
}Template syntax
| Placeholder | Resolves to |
|---|---|
{{input.key}} | a text / textarea / number value, or a select option's label |
{{steps.id.text}} | the text produced by an earlier chat step |
Unknown placeholders render as empty strings and repeated whitespace is collapsed. Image sources are listed separately in images: input.<key> (an image input) or steps.<id> (every output of an earlier image step).
Steps
| Field | chat | image / video |
|---|---|---|
model | a chat model id | an image / video model id (the step type must match the model's modality) |
prompt | user message (template) | generation prompt (template) |
system, maxOutputTokens | optional | — |
images | optional (vision models) | reference / input images; the model must accept them |
params | — | aspectRatio, resolution, quality, duration, audio, count — same rules as POST / |
negativePrompt | — | optional |
Limits: ≤ 12 inputs, 1–8 steps, name ≤ 60 chars. Input keys match ^[a-zA-Z][a-zA-Z0-9_]{0,30}$, step ids ^[a-zA-Z][a-zA-Z0-9_-]{0,30}$.
Validation errors
400 VALIDATION_ERROR with details: [{ path, message }], e.g.
step "poster" references "brief2" which is not an earlier chat step
unknown model "gpt-image-9" in step "poster"
step "poster": "kling-3.0" is a video model but the step is an image step
step "poster" uses unknown image input "photo"
select input "mood" needs optionsAn app whose steps use an unfiltered model is marked uncensored: true and can only be created, forked or run by adult-verified accounts.
Manage
PATCH /v1/workflows/{id} # owner only; same body, all fields optional
DELETE /v1/workflows/{id}
POST /v1/workflows/{id}/fork # copy a built-in or public app into your library (private)
POST /v1/workflows/{id}/like # → { likeCount, liked }
DELETE /v1/workflows/{id}/likevisibility: "public" lists the app under scope=community once people run it.
Example: run the storyboard app
RUN=$(curl -s https://api.poremu.ai/v1/workflows/storyboard/run \
-H "Authorization: Bearer $POREMU_API_KEY" -H "Content-Type: application/json" \
-d '{"inputs":{"idea":"a courier robot delivers a letter across a rainy neon city","style":"film"}}' | jq -r .id)
curl -s "https://api.poremu.ai/v1/workflows/runs/$RUN/wait?timeout=60000" \
-H "Authorization: Bearer $POREMU_API_KEY" | jq '.status, .steps[].generation.outputs[]?.url'const api = (path, init) =>
fetch(`https://api.poremu.ai${path}`, { ...init, headers: { Authorization: `Bearer ${process.env.POREMU_API_KEY}`, 'Content-Type': 'application/json', ...init?.headers } }).then((r) => r.json());
// upload the product photo first (see Uploading images), then:
let run = await api('/v1/workflows/product-shot/run', { method: 'POST', body: JSON.stringify({ inputs: { product: assetId, scene: 'marble', notes: 'add water droplets' } }) });
while (!['SUCCEEDED', 'FAILED', 'CANCELED'].includes(run.status)) run = await api(`/v1/workflows/runs/${run.id}/wait?timeout=30000`);
console.log(run.status, run.credits, run.steps.at(-1).generation?.outputs.map((o) => o.url));MCP
The Poremu MCP server exposes the same thing as tools: list_apps, get_app, run_app ({ id, inputs, visibility?, waitSeconds? }), get_app_run, wait_for_app_run. See MCP.