---
title: "How do I let others run my workflow via API with their own credits?"
description: "Publish your workflow as an App: others run it via API with POST /api/v1/apps/{slug}/execute and their own key, and their own credits pay for each run."
updated: 2026-10-19
---

To let other people run your workflow through the API, publish it as an App. Anyone with a Wireflow API key that has the `apps:execute` scope can then start it with `POST /api/v1/apps/{slug}/execute`, and the key owner's credits pay for every node in the run, not yours.

Your own workflows are already an API for you: `POST /api/v1/workflows/{id}/run` runs any workflow you own with your key, as shown in [How do I run an AI workflow from my app](/docs/trigger-a-workflow-from-your-app). That route is owner-only. Publishing adds a second route, by slug, that other accounts can call. This page covers the API side. For the app's web page, sign-in and subscriptions, see [Publishing Apps](/docs/publishing-apps).

## How do I deploy a workflow as a production API?

Publish it, either with the **Publish** button in the editor or with `POST /api/v1/workflows/{id}/publish` and a key that has the `workflows:write` scope. The reply gives you the app's `slug`, which is the name callers use.

```bash
curl -X POST https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/publish \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Kawarimi",
    "description": "Prompt in, image out.",
    "customSlug": "kawarimi"
  }'
```

```javascript
const res = await fetch('https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/publish', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.WIREFLOW_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: 'Kawarimi',
    description: 'Prompt in, image out.',
    customSlug: 'kawarimi',
  }),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const app = await res.json();
console.log(app.slug, app.url);
```

```python
import os
import requests

res = requests.post(
    "https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/publish",
    headers={"Authorization": f"Bearer {os.environ['WIREFLOW_API_KEY']}"},
    json={"title": "Kawarimi", "description": "Prompt in, image out.", "customSlug": "kawarimi"},
)
res.raise_for_status()
print(res.json()["slug"])
```

Response:

```json
{
  "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"
  }
}
```

Before you rely on it:

- `customSlug` takes 3 to 60 lowercase letters, numbers and hyphens, starting and ending with a letter or number. A slug that another app already uses returns `409`.
- `exposedInputs` lists the input node ids that callers may set. Leave it out and every Input node is exposed.
- Every publish call rebuilds the settings from its body. Send `title`, `description`, `exposedInputs`, `exampleOutputs` and `dailyCreditBudget` again on each update, or they reset. `visibility` is the exception: leave it out and it stays as it was.
- The app runs the saved workflow at call time, so your edits apply from the next run.
- Callers get the outputs of the nodes you picked as example outputs, or, if you picked none, of the workflow's last nodes.
- `DELETE /api/v1/workflows/{id}/publish` unpublishes the app. The slug stays reserved for you.

## How do I find the input keys callers send?

`GET /api/v1/apps/{slug}` returns the app's public config, and each entry in `inputs` has the `key` to send. For a public app, this call needs no API key.

```bash
curl https://www.wireflow.ai/api/v1/apps/kawarimi
```

```json
{
  "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"
}
```

- A key is the key set on the Input node, or else its label or name: lowercased, with any run of characters other than A to Z and 0 to 9 turned into one underscore, and cut to 64 characters. `Reference Images` becomes `reference_images`.
- Renaming the label changes the key, so tell your callers when you rename one.
- The config never includes an input's saved value, only its id, key, label, type and placeholder.
- After you publish, the **Publish** dropdown in the editor shows a ready-made curl call with your slug filled in.

## How do I call a published workflow with curl?

Send the input values to `POST /api/v1/apps/{slug}/execute` with your API key, then `GET` the `pollUrl` it returns until the run finishes.

```bash
curl -X POST https://www.wireflow.ai/api/v1/apps/kawarimi/execute \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"inputs": {"prompt": "a fox in a paper boat"}}'
```

Response `200 OK`, which means the run has started, not finished:

```json
{
  "executionId": "cm1exec123",
  "status": "RUNNING",
  "pollUrl": "/api/v1/apps/kawarimi/executions/cm1exec123/poll",
  "pollToken": "..."
}
```

Put `https://www.wireflow.ai` in front of `pollUrl` and poll it every few seconds:

```bash
curl https://www.wireflow.ai/api/v1/apps/kawarimi/executions/cm1exec123/poll \
  -H "Authorization: Bearer $WIREFLOW_API_KEY"
```

```json
{
  "status": "COMPLETED",
  "outputs": [
    {
      "nodeId": "node-image",
      "type": "image:generate",
      "urls": ["https://cdn.wireflow.ai/outputs/abc.png"]
    }
  ],
  "outputsWithheld": 0
}
```

The same flow in JavaScript and Python:

```javascript
const HOST = 'https://www.wireflow.ai';
const headers = {
  Authorization: `Bearer ${process.env.WIREFLOW_API_KEY}`,
  'Content-Type': 'application/json',
};
const FINAL = ['COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT'];

const start = await fetch(`${HOST}/api/v1/apps/kawarimi/execute`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ inputs: { prompt: 'a fox in a paper boat' } }),
});
if (!start.ok) throw new Error(`${start.status}: ${await start.text()}`);
const { pollUrl } = await start.json();

let result;
while (true) {
  const res = await fetch(`${HOST}${pollUrl}`, { headers });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  result = await res.json();
  if (FINAL.includes(result.status)) break;
  await new Promise((r) => setTimeout(r, 3000));
}
if (result.status !== 'COMPLETED') throw new Error(result.error || result.status);
for (const item of result.outputs) console.log(item.nodeId, item.urls ?? item.text);
```

```python
import os
import time
import requests

HOST = "https://www.wireflow.ai"
HEADERS = {"Authorization": f"Bearer {os.environ['WIREFLOW_API_KEY']}"}
FINAL = {"COMPLETED", "FAILED", "CANCELLED", "TIMEOUT"}

start = requests.post(
    f"{HOST}/api/v1/apps/kawarimi/execute",
    headers=HEADERS,
    json={"inputs": {"prompt": "a fox in a paper boat"}},
)
start.raise_for_status()
poll_url = start.json()["pollUrl"]

while True:
    res = requests.get(HOST + poll_url, headers=HEADERS)
    res.raise_for_status()
    result = res.json()
    if result["status"] in FINAL:
        break
    time.sleep(3)

if result["status"] != "COMPLETED":
    raise RuntimeError(result.get("error") or result["status"])
for item in result["outputs"]:
    print(item["nodeId"], item.get("urls") or item.get("text"))
```

Keep polling while `status` is `RUNNING` or `PENDING`. `COMPLETED` and `FAILED` are the normal endings, `CANCELLED` and `TIMEOUT` can also appear, and a failed run adds an `error` string. Each item in `outputs` has `nodeId` and `type`, plus `urls` for media, `text`, or both. `outputsWithheld` counts outputs that were held back because they matched a value the app's author saved, and `0` means nothing was held back.

Rules every call follows:

- A value is a string of at most 2000 characters, and an image input must be an `https://` URL.
- Video and audio inputs cannot be set through the Apps API yet. A value sent under such a key is refused with `400` (`unsupported_media_input`), and nothing is charged.
- A key that matches no exposed input returns `400` with `code: "unknown_input"` and the list of `validKeys`. Nothing runs and nothing is charged.
- Execute returns once the run has started. There is no option to wait for the result in one call.
- Agents can run the same app over MCP with `run_app`, using the same input keys.

## Who pays when someone else runs my workflow?

The owner of the API key that starts the run pays the credits for every node in it. You, the app's creator, are not charged.

- The key owner needs more than 0 credits to start a run, or the call returns `402`.
- A call with no `Authorization` header counts as a visitor run on the public page. You pay for it, a limit of 3 requests per minute per IP applies, and the app's `dailyCreditBudget`, 100 credits a day by default, caps public use. A header that does not start with `Bearer sk-` is treated the same way, so use a key for anything programmatic.
- Calling your own app with your own key costs your credits, like any other key call.

## What is the difference between my API key and an app's key?

An app has no key of its own. Every caller uses their own `sk-` API key with the `apps:execute` scope, and that key decides who pays and who can read the result.

- A **Full access** key has `apps:execute`. A new key starts read-only, so tick the scope or choose Full access when you create the key in **Settings > API Keys**.
- The poll accepts any `apps:execute` key from the same account that started the run, or that run's `pollToken`. A key from another account gets `404`.
- Results are readable by the key or token that started the run, not by you as the app's creator through this route.
- To check which account a key acts as, call `GET /api/v1/me`.

## How do I charge or limit usage per user?

Over the API you don't set a price: each caller pays the model credits for their own run, and each API key can start 20 app runs a minute. A per-run price (`creditsPerRun`) set on the app is charged to signed-in visitors on the app's web page, not on API key calls, and subscriptions are sold on that page too. Set those up in [Publishing Apps](/docs/publishing-apps).

- Over 20 execute calls a minute, a key gets `429`. Wait the number of seconds in the `Retry-After` header.
- Keyless visitor runs stop for the day with `429` once the app's `dailyCreditBudget` is spent.

## Can I make the API private to my own account?

Yes. Publish with `"visibility": "private"`, or flip the **Private** switch in the Publish dropdown, and only your account can open or run the app. Everyone else gets `404` with `{"error": "App not found"}`, the same answer as a slug that does not exist.

```bash
curl -X POST https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/publish \
  -H "Authorization: Bearer $WIREFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Kawarimi", "customSlug": "kawarimi", "visibility": "private"}'
```

- Call it with your own key that has `apps:execute`, or from your own MCP client with `run_app`.
- Private is about the app, not the board. Link sharing and the `template` tag are separate settings.
- To make it public again, publish with `"visibility": "public"`.

Last updated: 2026-10-19

---

Documentation index: fetch https://www.wireflow.ai/llms.txt for the full list of Wireflow docs.
