Apps API
Publish a workflow as an App, then call it over HTTP with an API key. The caller pays for the run. Make it private to keep it yours.
An App is a workflow you have published. Once it is published, anyone holding an API key with the apps:execute scope can run it with one POST and read the result back with a GET. The key's owner pays for the run, not you. If you want an app only you can call, publish it as private. This page covers the HTTP side. For the editor side (subdomains, sign-in, pricing for visitors) see Publishing Apps.
The flow has four steps:
- Publish the workflow and get its slug.
- Find the input keys the app accepts.
POST /api/v1/apps/{slug}/executeto start a run.GET /api/v1/apps/{slug}/executions/{executionId}/polluntil it finishes. The execute response hands you this path aspollUrl.
1. Publish
Publish from the Publish button in the editor, or over the API with a key that has the workflows:write scope:
curl -X POST https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/publish \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"title": "Kawarimi",
"description": "Prompt in, image out.",
"customSlug": "kawarimi"
}'
{
"slug": "kawarimi",
"url": "https://kawarimi.wireflow.ai",
"publishedAt": "2026-10-02T12:00:00.000Z",
"publishSettings": {
"title": "Kawarimi",
"exposedInputs": ["node-prompt"],
"dailyCreditBudget": 100,
"creditsPerRun": 0,
"visibility": "public"
}
}
Things worth knowing:
- Any workflow owner can publish. There is no admin gate.
customSlugmust be 3 to 60 characters: lowercase letters, numbers and hyphens, starting and ending with a letter or number. A slug already used by another app or collection returns409. If you send nocustomSlugon a first publish, the slug is the workflow name plus the first 10 characters of the workflow id. A workflow that was published before keeps its slug.exposedInputsis the list of input node ids the app accepts. If you leave it out or send an empty list, every Input node in the workflow is exposed.visibilityis"public"(the default) or"private". See Private apps. Anything else returns400.- Every publish call rebuilds the app settings from the request body. Send your
title,description,exposedInputs,exampleOutputsanddailyCreditBudgetagain when you update an app, or they reset (dailyCreditBudgetgoes back to 100).visibilityis the exception: leave it out and the app keeps whatever it had, so a plain re-publish cannot turn a private app public. - The app runs the saved workflow at call time, so edits to the workflow take effect on the next run.
DELETE /api/v1/workflows/{id}/publishunpublishes the app. The slug stays reserved for you.
2. Find the input keys
Every input has a key, and the key is what you send in inputs. Two ways to read them:
Anyone, no key (a public app). The public app config lists the exposed inputs. For a private app only the owner gets it: send an API key with the apps:execute scope, or be signed in as the owner. Everyone else gets 404.
curl https://www.wireflow.ai/api/v1/apps/kawarimi
{
"title": "Kawarimi",
"description": "Prompt in, image out.",
"inputs": [
{
"nodeId": "node-prompt",
"key": "prompt",
"label": "Prompt",
"type": "text",
"placeholder": ""
}
],
"ownerName": "Your Name",
"publishedAt": "2026-10-02T12:00:00.000Z"
}
The public config never includes an input's saved value, only its id, key, label, type and placeholder. A saved value can be a credential, and this endpoint needs no key.
The owner. GET /api/v1/workflows/{id}/publish (needs workflows:write) returns inputNodes with nodeId, label and type for every Input node, plus the current publishSettings. It does not include keys, so read those from the public config above.
Where a key comes from:
- The key the author set on the Input node, if there is one.
- Otherwise the node's label, or its name if it has no label. A node with neither is read by its id, so
input-1760000000000becomesinput_1760000000000. - Either way only the letters A to Z and the digits 0 to 9 are kept, lowercased. Anything else is a separator, a run of separators becomes one underscore, and the result is cut to 64 characters.
Reference Imagesbecomesreference_images. Accented letters drop out:Café Menubecomescaf_menu. - If two inputs would end up with the same key, or the result is empty, the input uses its exact node id instead. A label with no A to Z letters or digits, such as one written in Japanese or Cyrillic, ends up here.
A key does not change when the author deletes a node and adds a new one with the same label, which a node id does. It does change if the label is renamed, and an input that fell back to its node id changes with the node. The workflow owner gets the same keys from GET /api/v1/workflows/{id}/run.
One rare case: if an input's key is also the node id of a different node, the node id wins on execute, so the config lists that input under its own node id instead.
After a successful publish, the Publish dropdown in the editor also shows a ready-made curl call with the real slug and node ids filled in. Those calls work as they are. Swap in the keys if you want calls that survive a re-created node.
3. Execute
curl -X POST https://www.wireflow.ai/api/v1/apps/kawarimi/execute \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{"inputs": {"prompt": "a fox in a paper boat"}}'
The key needs the apps:execute scope. A full-access key has it.
Request body
| Field | Type | Description |
|---|---|---|
inputs |
object of key to string |
Keys are the key values from step 2. Node ids from the same list also work. Values are the text or image URL. |
Response (200, the run has started, it has not finished)
{
"executionId": "cm1exec123",
"status": "RUNNING",
"pollUrl": "/api/v1/apps/kawarimi/executions/cm1exec123/poll",
"pollToken": "..."
}
pollUrl is the path of the poll route for this run. Put the host in front of it (https://www.wireflow.ai). It carries no credential, so authenticate the poll as described in step 4.
pollToken is a short-lived token for reading this one run back without the key. It is valid for 24 hours. It is left out of the response if the server could not sign one, so do not depend on it. With the same API key you do not need it.
What the inputs do
- Send each input under its
key. A key that matches no exposed input returns400withcode: "unknown_input". The message names the key and the body listsvalidKeys. Nothing runs and nothing is charged. A long key is shortened to 64 characters in the reply, and at most 20 are named. - A value sent under a key must be a string of at most 2000 characters, and an image input must be an
https://URL. A value that breaks a rule returns400(invalid_input_type,input_too_longorinvalid_media_input) and names the key. Under a key, nothing is cut or skipped without an error. - Video and audio inputs cannot be set through the Apps API yet. An Import node that takes a video or audio file (the config lists its
typeasvideooraudio, and an Import node with no type yet is listed astextand refused the same way) has no way to receive a value here: the step that places your value only delivers images and text, so the run would use the author's saved file and still be charged. A value sent under the key of such an input returns400withcode: "unsupported_media_input", and nothing is charged. The real fix is tracked in #2550. - Sending the key and the node id of the same input in one call returns
400(ambiguous_input), because there is no telling which value you meant. inputshas to be an object. A string, a number,trueor a list with items in it returns400(invalid_inputs). Leaving it out, or sendingnull,false,0,""or[], runs the app with no inputs.- Node ids that exist in the workflow keep working exactly as before, so existing integrations do not change. A value sent under a node id is converted to a string and cut to 2000 characters, a non-https image URL is skipped and the app runs with the author's saved image, a value for a video or audio input is accepted and has no effect (the same #2550 gap), and a node id that is not an exposed input is ignored. One thing did change: a node id that matches no node in the workflow at all is now
unknown_input, like any other unknown key. It used to be ignored, so a stale id ran the app on the author's saved value. - Other Input node types take the string as their text value. An empty string (or only spaces) counts as no value, so the app runs with the author's saved text for that input. That holds whether you send it under a key or a node id.
- Utility nodes you list in
exposedInputs(a TikTok import, for example) also accept a value, by key or by node id. The value is set as the node'surl. The public config does not list these nodes. Use the node id, or read the key fromGET /api/v1/workflows/{id}/runas the owner. - An empty or unreadable JSON body is treated as no inputs.
Who pays
The owner of the API key pays the credits for every node in the run. The app creator is not charged. A creditsPerRun price set on the app is a charge on signed-in visitors using the web page, and it is not applied to API key calls.
The key owner must have more than 0 credits to start, otherwise the call returns 402.
Without a key
A call with no Authorization header is treated as a visitor run on the public page. The app owner pays, a limit of 3 requests per minute per IP applies, and the app's dailyCreditBudget (100 credits a day by default) caps total public usage. Use a key for anything programmatic. A header that does not start with Bearer sk- is also treated as a visitor run, so check your key is sent exactly as in the examples.
4. Poll
curl https://www.wireflow.ai/api/v1/apps/kawarimi/executions/cm1exec123/poll \
-H "Authorization: Bearer sk-your-api-key"
Authenticate with one of:
- An API key with
apps:executethat belongs to the same account as the key that started the run. It does not have to be the exact same key. A key owned by someone else gets404. - The
pollToken, as thex-wireflow-run-tokenheader or the?t=query parameter.
Response while running
{ "status": "RUNNING", "outputs": [] }
Response when done
{
"status": "COMPLETED",
"outputs": [
{
"nodeId": "node-image",
"type": "image:generate",
"urls": ["https://cdn.wireflow.ai/outputs/abc.png"]
}
]
}
Each item in outputs has nodeId and type, and either urls (media), text, or both. Text-only items can also carry a frameUrl. Outputs come from the nodes the author picked as example outputs when publishing. If none were picked, they come from the workflow's last nodes (nodes with no outgoing wire).
status is the stored run status. Keep polling while it is RUNNING (or PENDING). COMPLETED and FAILED are the normal endings, and CANCELLED and TIMEOUT can also appear. A failed run adds an error string:
{ "status": "FAILED", "outputs": [], "error": "The app encountered an error" }
Poll every few seconds.
Private apps
A private app is for its owner only. It is how you package a pipeline as your own API: publish it private, call it with your own key (or from your own MCP client), and nobody else can find it, open it or run it.
Publish it private with visibility, or flip the Private switch in the Publish dropdown:
curl -X POST https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/publish \
-H "Authorization: Bearer sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{"title": "Kawarimi", "customSlug": "kawarimi", "visibility": "private"}'
GET /api/v1/workflows/{id}/publish returns the current audience as a top-level visibility. An app published before this existed reads as public. To make it public again, publish with "visibility": "public".
Who the owner is. The account that published the app, and nobody else. A teammate, a collaborator with edit access and a visitor on a link-shared board are not the owner here, and they get the same 404 a stranger gets. They keep whatever access they had to the workflow itself: the canvas, and the workflow's own run door POST /api/v1/workflows/{id}/execute, which admits anyone who can edit it (POST /api/v1/workflows/{id}/run is owner-only and always was). Over MCP, someone who could read or run the workflow only because it was published loses get_workflow, clone_template and run_workflow on it once it is private. The reason is billing: a signed-in run through the app page is paid by the app's owner, and a public edit link makes everyone an editor.
What a stranger gets. 404 {"error": "App not found"} from every door, byte for byte what a slug that does not exist returns. It is never a 403, so neither the status nor the body of the answer says whether the slug exists. That covers the app page ({slug}.wireflow.ai, including its title in the page metadata), GET /api/v1/apps/{slug}, POST /api/v1/apps/{slug}/execute, the upload, access and register routes, the run history, collection pages and the collection API (a private app is left out of the list), and the MCP run_app tool. A bad input from a stranger is a 404 too, not the 400 that lists the app's input keys. Nothing is charged.
What the owner does. Everything, with the same calls: by session on the page, or with a key holding apps:execute over the API. The key must belong to the owner. On GET /api/v1/apps/{slug} a bad key, a key without the scope or someone else's key counts as no key and gets the 404. On POST /api/v1/apps/{slug}/execute a bad or under-scoped key still gets its usual 401 first, before the app is looked up, so that answer says nothing about the slug.
Things worth knowing:
- A game is playable by anyone with the link, so a game cannot be private. Publishing one with
visibility: "private"returns400. A private site (a compositor page) is fine: only you can open it. - Private is about the app, not the board. Link sharing and the
templatetag are separate settings, and Private does not change them. If you want the graph hidden as well, leave link sharing on Private. - A run someone started before you made the app private stays readable by whoever started it: with their own session, or with the run token until it expires (24 hours). Nobody can start a new one.
- Calling a private app costs the key owner's credits, which are yours, like any other key call.
From MCP: run_app
The MCP server has a run_app tool that runs an app by slug, for any app that is public and for your own private ones:
{ "slug": "kawarimi", "inputs": { "prompt": "a fox in a paper boat" } }
It takes the same inputs keys as the HTTP call and goes through the same code, so the key rules, the refusals and the pricing are identical. It returns an executionId, a pollUrl and, when the server could sign one, a pollToken. Poll with get_execution (needs executions:read), or GET the pollUrl with the token as ?t= or the x-wireflow-run-token header. The credits are the connected account's. It needs the workflows:execute permission, which is in the default grant. A parameter the tool does not have is refused, not dropped, so an input passed at the top level instead of inside inputs cannot silently run the app on its saved values. Someone else's private app, and a slug that does not exist, both come back as App not found.
Errors
Errors from the Apps routes are { "error": "message" }. Some add a code and extra fields, listed below.
The publish route uses two shapes. A 401 for a bad or missing key returns the nested form { "error": { "type", "message", "code" } } described in Errors. The checks inside the route (404 workflow not found, 400 bad slug or mode, 409 slug taken) return the flat form { "error": "message" }.
| Status | Route | Meaning |
|---|---|---|
401 |
execute | Bad key: Invalid API key, API key has expired, Account suspended, or API key missing required scope: apps:execute. |
402 |
execute | Insufficient credits. Please top up your account. The key owner has no credits. |
404 |
execute | App not found. The slug does not exist, the app is unpublished, or the app is private and the caller is not its owner. All three look the same. |
404 |
poll | Execution not found. The id is wrong, belongs to another app, or the caller did not start it. All three look the same. |
429 |
execute | Too many requests. Please try again later. Limit is 20 requests per minute per API key. Honor the Retry-After header (seconds). |
429 |
execute | This app has reached its daily usage limit. Please try again tomorrow. Only on calls with no key: the app's dailyCreditBudget for the day is spent. |
400 |
execute | code: "team_blueprint_in_published_app" with the nodeId. Only on calls with no key, when the app contains a team blueprint. |
400 |
execute | A node is wired to a context blueprint. The body carries code: "context_blueprint_output_wired" and the nodeIds. |
400 |
execute | code: "unknown_input". A key matches no exposed input. The body has unknownKeys and validKeys. |
400 |
execute | code: "invalid_input_type", "input_too_long" or "invalid_media_input". A value sent under a key is not a string, is over 2000 characters, or is an image that is not an https:// URL. The body has keys, and maxChars for the length limit. |
400 |
execute | code: "unsupported_media_input". A value was sent under the key of a video or audio input, which the Apps API cannot deliver yet (#2550). The body has keys. Nothing is charged. |
400 |
execute | code: "ambiguous_input" (two names for one input) or code: "invalid_inputs" (inputs is not an object). |
503 |
execute | This app is temporarily unavailable. The creator has been notified. Returned when the run was refused for lack of credits or an unavailable plan feature. For a key run those are the key owner's credits and plan, whatever the message says. |
500 |
execute | Server configuration error, Failed to start execution, or Failed to run app. |
Any other failure from starting the run is passed through with its own status and error text.
Limits
This is how the API behaves today.
- A value sent under a key is a string of at most 2000 characters. A value sent under a node id is cut to 2000 characters.
- A key follows the Input node's key or label, so renaming the label changes it. A node id changes whenever a node is deleted and re-created.
- Video and audio inputs cannot be set through the Apps API yet (#2550). A value under a key is refused, and under a node id it is accepted and has no effect.
- Execute returns once the run has started. There is no option to wait for the result in one call. Poll the
pollUrlit returns. - 20 execute calls per minute per API key.
- Any caller with an
apps:executekey can run any public app. To limit an app to yourself, publish it as private. - Run results are readable by the key (or token) that started the run, not by the app creator through this route.