Skip to content

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

text
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.

text
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:

typeValue you sendNotes
text, textareastring ≤ 4000 charsfree text
selectone of options[].valuethe option label is what the prompt sees
numbernumber
imagehttps URL or an uploaded asset id — or an array of themup to max images (default 1); see Uploading images

required inputs must be present (a default counts). Unknown keys are rejected.

Estimate

text
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

text
POST /v1/workflows/{id}/run
{
  "inputs": { "idea": "a courier robot in a rainy neon city", "style": "anime" },
  "visibility": "private"
}

Returns a WorkflowRunDto immediately:

json
{
  "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 RESERVE entry with workflowRunId in 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. credits on a finished run is what you actually paid.
  • Each step's credits is the estimate until the step finishes, then the actual charge.
  • Every image/video step creates a regular generation (visible in GET /v1/generations, carries workflowRunId). steps[].generation is that generation with its outputs[].
  • 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

text
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=SUCCEEDED

Run 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

text
POST /v1/workflows/runs/{id}/cancel

Stops 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

text
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

PlaceholderResolves 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

Fieldchatimage / video
modela chat model idan image / video model id (the step type must match the model's modality)
promptuser message (template)generation prompt (template)
system, maxOutputTokensoptional—
imagesoptional (vision models)reference / input images; the model must accept them
params—aspectRatio, resolution, quality, duration, audio, count — same rules as POST /v1/generations
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.

text
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 options

An app whose steps use an unfiltered model is marked uncensored: true and can only be created, forked or run by adult-verified accounts.

Manage

text
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}/like

visibility: "public" lists the app under scope=community once people run it.

Example: run the storyboard app

bash
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'
js
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.