> ## Documentation Index
> Fetch the complete documentation index at: https://docs.justflow.it/llms.txt
> Use this file to discover all available pages before exploring further.

# Generate a diagram

> Generate a BPMN diagram from natural language with the async generate-then-poll flow.

Turn a plain-language description of a process into a saved, laid-out diagram and a rendered image. Because generation runs an LLM and then renders the result, it can take many seconds — so the API is **asynchronous**: you create a generation job, then poll it until it finishes.

<Note>
  All requests are **server-to-server only**. Never expose `jfi_sk_live_...` keys in a browser or client app.
</Note>

## Why async?

A single generation involves an LLM call plus a server-side layout and render pass. That can comfortably exceed the lifetime of a normal HTTP request. Rather than hold a connection open (and risk timeouts), `POST /v1/diagrams/generate` returns immediately with `202 Accepted` and a **Generation** job in `queued` status. You then poll `GET /v1/generations/{id}` until the job reaches a terminal state.

<Tip>
  Webhooks for generation completion are planned for **v2**. Until then, poll the generation endpoint with backoff (see the example below).
</Tip>

## The flow

<Steps>
  <Step title="Create the generation">
    `POST /v1/diagrams/generate` with your `prompt`. You get back `202 Accepted`, a `Generation` with `status: "queued"`, a `Location` header pointing at the poll URL, and a `Retry-After` hint.
  </Step>

  <Step title="Poll for status">
    `GET` the URL from the `Location` header. While `status` is `queued` or `processing`, wait and try again (respecting `Retry-After` and backing off).
  </Step>

  <Step title="Read the result">
    When `status` is `succeeded`, the `diagram` (the created, saved diagram) and `image_url` (the rendered image for the requested theme/format) are populated. When `status` is `failed`, `error` explains why.
  </Step>
</Steps>

## Create a generation

<ParamField path="POST /v1/diagrams/generate" />

Requires the `generate` scope. **Counts against your monthly generation quota** (500 generations per rolling 30 days).

### Body

<ParamField body="prompt" type="string" required>
  Natural-language description of the process to generate.
</ParamField>

<ParamField body="theme" type="string">
  Theme for the rendered image: `"light"` (default) or `"dark"`.
</ParamField>

<ParamField body="format" type="string">
  Image format: `"png"` (default) or `"svg"`.
</ParamField>

<ParamField body="name" type="string">
  Optional name for the resulting diagram.
</ParamField>

<ParamField body="folder_id" type="string | null">
  Optional folder (UUID) to place the diagram in, or `null` for the root.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl https://justflow.it/api/v1/diagrams/generate \
    -H "Authorization: Bearer jfi_sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{
      "prompt": "An employee submits an expense report. A manager approves or rejects it. If approved, finance reimburses the employee.",
      "theme": "light",
      "format": "png",
      "name": "Expense approval"
    }'
  ```

  ```javascript JavaScript (fetch) theme={null}
  const res = await fetch("https://justflow.it/api/v1/diagrams/generate", {
    method: "POST",
    headers: {
      Authorization: "Bearer jfi_sk_live_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      prompt:
        "An employee submits an expense report. A manager approves or rejects it. If approved, finance reimburses the employee.",
      theme: "light",
      format: "png",
      name: "Expense approval",
    }),
  });

  // 202 Accepted — a queued generation job
  const generation = await res.json();
  const pollUrl = res.headers.get("Location");
  ```

  ```python Python (requests) theme={null}
  import requests

  res = requests.post(
      "https://justflow.it/api/v1/diagrams/generate",
      headers={"Authorization": "Bearer jfi_sk_live_..."},
      json={
          "prompt": "An employee submits an expense report. A manager approves or rejects it. If approved, finance reimburses the employee.",
          "theme": "light",
          "format": "png",
          "name": "Expense approval",
      },
  )

  # 202 Accepted — a queued generation job
  generation = res.json()
  poll_url = res.headers["Location"]
  ```
</CodeGroup>

### Response

`202 Accepted`

```http theme={null}
HTTP/1.1 202 Accepted
Location: /v1/generations/gen_3pK9xQ2mL7vR
Retry-After: 2
X-Request-Id: req_8aF2c1
```

```json theme={null}
{
  "object": "generation",
  "id": "gen_3pK9xQ2mL7vR",
  "status": "queued",
  "diagram": null,
  "image_url": null,
  "error": null,
  "created_at": "2026-06-07T14:22:05Z"
}
```

<ResponseField name="object" type="string">
  Always `"generation"`.
</ResponseField>

<ResponseField name="id" type="string">
  The generation job id, prefixed with `gen_`. Use it to poll.
</ResponseField>

<ResponseField name="status" type="string">
  One of `"queued"`, `"processing"`, `"succeeded"`, `"failed"`.
</ResponseField>

<ResponseField name="diagram" type="object | null">
  The created, saved [Diagram](/api-reference/diagrams) once `status` is `"succeeded"`; otherwise `null`.
</ResponseField>

<ResponseField name="image_url" type="string | null">
  Path to the rendered image (per the requested `theme`/`format`) once `status` is `"succeeded"`; otherwise `null`. It is **relative to the API base** (`https://justflow.it`) — prepend the base to fetch it.
</ResponseField>

<ResponseField name="error" type="object | null">
  `{ "code": string, "message": string }` when `status` is `"failed"`; otherwise `null`.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO-8601 UTC timestamp.
</ResponseField>

## Poll the generation

<ParamField path="GET /v1/generations/{id}" />

Requires the `generate` scope. Poll until `status` reaches a terminal state (`succeeded` or `failed`).

<CodeGroup>
  ```bash curl theme={null}
  curl https://justflow.it/api/v1/generations/gen_3pK9xQ2mL7vR \
    -H "Authorization: Bearer jfi_sk_live_..."
  ```

  ```javascript JavaScript (fetch) theme={null}
  const res = await fetch(
    "https://justflow.it/api/v1/generations/gen_3pK9xQ2mL7vR",
    { headers: { Authorization: "Bearer jfi_sk_live_..." } }
  );
  const generation = await res.json();
  ```

  ```python Python (requests) theme={null}
  import requests

  res = requests.get(
      "https://justflow.it/api/v1/generations/gen_3pK9xQ2mL7vR",
      headers={"Authorization": "Bearer jfi_sk_live_..."},
  )
  generation = res.json()
  ```
</CodeGroup>

### Response — succeeded

`200 OK`

```json theme={null}
{
  "object": "generation",
  "id": "gen_3pK9xQ2mL7vR",
  "status": "succeeded",
  "diagram": {
    "object": "diagram",
    "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
    "name": "Expense approval",
    "folder_id": null,
    "organization_id": null,
    "created_by": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
    "created_at": "2026-06-07T14:22:09Z",
    "updated_at": "2026-06-07T14:22:09Z"
  },
  "image_url": "/api/v1/diagrams/a1b2c3d4-5e6f-7890-abcd-ef1234567890/image?theme=light&format=png",
  "error": null,
  "created_at": "2026-06-07T14:22:05Z"
}
```

### Response — failed

`200 OK`

```json theme={null}
{
  "object": "generation",
  "id": "gen_3pK9xQ2mL7vR",
  "status": "failed",
  "diagram": null,
  "image_url": null,
  "error": {
    "code": "validation_failed",
    "message": "The generated process could not be laid out as a valid diagram."
  },
  "created_at": "2026-06-07T14:22:05Z"
}
```

<Note>
  A failed generation returns `200 OK` with `status: "failed"` — the HTTP request to poll succeeded; the *job* did not. Inspect `generation.status` and `generation.error`, not the HTTP status code, to decide outcome.
</Note>

## Full polling example

Poll the `Location` URL, respecting the initial `Retry-After` and then backing off, until the job is terminal.

<CodeGroup>
  ```bash curl theme={null}
  #!/usr/bin/env bash
  set -euo pipefail
  KEY="jfi_sk_live_..."
  BASE="https://justflow.it/api/v1"

  # 1. Create the generation
  RESP=$(curl -s -D - -o /tmp/gen.json "$BASE/diagrams/generate" \
    -H "Authorization: Bearer $KEY" \
    -H "Content-Type: application/json" \
    -d '{"prompt":"Order-to-cash process for a B2B SaaS company","format":"svg"}')

  # The generation id is in the 202 body; poll it against the same BASE.
  GEN_ID=$(jq -r '.id' /tmp/gen.json)
  DELAY=$(printf '%s' "$RESP" | awk -F': ' 'tolower($1)=="retry-after"{print $2}' | tr -d '\r')
  DELAY=${DELAY:-2}

  # 2. Poll until terminal
  while true; do
    curl -s "$BASE/generations/$GEN_ID" -H "Authorization: Bearer $KEY" -o /tmp/poll.json
    STATUS=$(jq -r '.status' /tmp/poll.json)
    echo "status: $STATUS"
    case "$STATUS" in
      succeeded) jq '{diagram_id: .diagram.id, image_url}' /tmp/poll.json; break ;;
      failed)    jq '.error' /tmp/poll.json; exit 1 ;;
      *)         sleep "$DELAY"; DELAY=$(( DELAY < 16 ? DELAY * 2 : 16 )) ;;
    esac
  done
  ```

  ```javascript JavaScript (fetch) theme={null}
  const KEY = "jfi_sk_live_...";
  const BASE = "https://justflow.it/api/v1";
  const auth = { Authorization: `Bearer ${KEY}` };
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  async function generateDiagram(prompt) {
    // 1. Create the generation
    const create = await fetch(`${BASE}/diagrams/generate`, {
      method: "POST",
      headers: { ...auth, "Content-Type": "application/json" },
      body: JSON.stringify({ prompt, format: "svg" }),
    });
    if (create.status !== 202) throw new Error(`unexpected ${create.status}`);

    // The generation id is in the 202 body; poll it against the same BASE.
    const { id } = await create.json();
    let delay = (Number(create.headers.get("Retry-After")) || 2) * 1000;

    // 2. Poll until terminal
    while (true) {
      const res = await fetch(`${BASE}/generations/${id}`, { headers: auth });
      const gen = await res.json();
      if (gen.status === "succeeded") {
        return { diagram: gen.diagram, imageUrl: gen.image_url };
      }
      if (gen.status === "failed") {
        throw new Error(`generation failed: ${gen.error.message}`);
      }
      await sleep(delay);
      delay = Math.min(delay * 2, 16000); // exponential backoff, capped at 16s
    }
  }
  ```

  ```python Python (requests) theme={null}
  import time
  import requests

  KEY = "jfi_sk_live_..."
  BASE = "https://justflow.it/api/v1"
  AUTH = {"Authorization": f"Bearer {KEY}"}


  def generate_diagram(prompt: str) -> dict:
      # 1. Create the generation
      create = requests.post(
          f"{BASE}/diagrams/generate",
          headers=AUTH,
          json={"prompt": prompt, "format": "svg"},
      )
      if create.status_code != 202:
          raise RuntimeError(f"unexpected {create.status_code}")

      # The generation id is in the 202 body; poll it against the same BASE.
      gen_id = create.json()["id"]
      delay = int(create.headers.get("Retry-After", "2"))

      # 2. Poll until terminal
      while True:
          gen = requests.get(f"{BASE}/generations/{gen_id}", headers=AUTH).json()
          if gen["status"] == "succeeded":
              return {"diagram": gen["diagram"], "image_url": gen["image_url"]}
          if gen["status"] == "failed":
              raise RuntimeError(f"generation failed: {gen['error']['message']}")
          time.sleep(delay)
          delay = min(delay * 2, 16)  # exponential backoff, capped at 16s
  ```
</CodeGroup>

<Tip>
  Already have a saved diagram and just want to re-fetch or re-theme its image? Use [`GET /v1/diagrams/{id}/image`](/api-reference/diagrams) with `theme` and `format` query params — no regeneration, no quota cost.
</Tip>

## Edit an existing diagram

Change a saved diagram's content with another natural-language instruction. Editing runs an LLM and a render too, so it uses the **same asynchronous flow** as generation: create an edit job, then poll. Requires the `generate` scope and counts against the generation quota.

<ParamField path="POST /v1/diagrams/{id}/edit" />

### Body

<ParamField body="prompt" type="string" required>
  Natural-language instruction describing the change — e.g. "add a rejection path after the approval gateway" or "rename the first task to 'Receive invoice'".
</ParamField>

<ParamField body="theme" type="string">
  Theme for the re-rendered image: `"light"` (default) or `"dark"`.
</ParamField>

<ParamField body="format" type="string">
  Image format: `"png"` (default) or `"svg"`.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://justflow.it/api/v1/diagrams/c4e8a1b2-5d6f-4a9c-8e30-1b2c3d4e5f60/edit \
    -H "Authorization: Bearer jfi_sk_live_..." \
    -H "Content-Type: application/json" \
    -d '{ "prompt": "Add a rejection path after the approval gateway" }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    "https://justflow.it/api/v1/diagrams/c4e8a1b2-5d6f-4a9c-8e30-1b2c3d4e5f60/edit",
    {
      method: "POST",
      headers: {
        Authorization: "Bearer jfi_sk_live_...",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ prompt: "Add a rejection path after the approval gateway" }),
    }
  );
  const generation = await res.json(); // 202 — poll it like a generation
  ```

  ```python Python theme={null}
  import requests
  res = requests.post(
      "https://justflow.it/api/v1/diagrams/c4e8a1b2-5d6f-4a9c-8e30-1b2c3d4e5f60/edit",
      headers={"Authorization": "Bearer jfi_sk_live_..."},
      json={"prompt": "Add a rejection path after the approval gateway"},
  )
  generation = res.json()  # 202 — poll it like a generation
  ```
</CodeGroup>

Returns `202 Accepted` with a `Generation` (identical shape to [generate](#create-a-generation)). Poll `GET /v1/generations/{id}` exactly as above; on success, `diagram` is the **updated** diagram and `image_url` is the re-rendered image.

<Note>
  Editing applies the instruction to the diagram's current content and updates it **in place** (same diagram `id`). It is the only way to change a diagram's content — `PATCH /v1/diagrams/{id}` changes metadata (name, folder) only.
</Note>

## Quota & rate limits

Generation is metered on two axes, both per key:

<CardGroup cols={2}>
  <Card title="Generation quota" icon="gauge">
    500 generations per rolling 30 days. Each accepted `POST /v1/diagrams/generate` (returned as `202 Accepted`) counts once — including jobs that later end in `failed`.
  </Card>

  <Card title="Burst limit" icon="bolt">
    120 requests / 60s across all endpoints, including your poll calls.
  </Card>
</CardGroup>

Every response carries `RateLimit-Limit`, `RateLimit-Remaining`, and `RateLimit-Reset` (seconds). On a `429`, a `Retry-After` (seconds) header tells you how long to wait.

<Warning>
  When the monthly generation quota is exhausted, `POST /v1/diagrams/generate` returns `429` with `error.code` `quota_exceeded`. When you're sending too fast, it returns `429` with `rate_limit_exceeded`. Back off using `Retry-After`.
</Warning>

## Errors

Every error uses the standard envelope:

```json theme={null}
{
  "error": {
    "type": "permission_error",
    "code": "insufficient_scope",
    "message": "This key is missing the required scope: generate."
  },
  "request_id": "req_8aF2c1"
}
```

<Accordion title="Errors you may hit on this endpoint">
  | Status | `type`                  | `code`                | When                                     |
  | ------ | ----------------------- | --------------------- | ---------------------------------------- |
  | 400    | `invalid_request_error` | `missing_parameter`   | `prompt` is absent                       |
  | 400    | `invalid_request_error` | `invalid_parameter`   | Bad `folder_id`, `theme`, or `format`    |
  | 400    | `invalid_request_error` | `invalid_json`        | Malformed request body                   |
  | 401    | `authentication_error`  | `invalid_api_key`     | Missing/invalid/revoked/expired key      |
  | 403    | `permission_error`      | `plan_required`       | Free plan — the API requires a paid plan |
  | 403    | `permission_error`      | `insufficient_scope`  | Key lacks the `generate` scope           |
  | 404    | `not_found_error`       | `resource_not_found`  | Unknown generation `id` on poll          |
  | 429    | `rate_limit_error`      | `rate_limit_exceeded` | Burst limit hit                          |
  | 429    | `rate_limit_error`      | `quota_exceeded`      | Monthly generation quota exhausted       |
  | 500    | `api_error`             | `internal_error`      | Unexpected server error                  |

  Note: when a job's *content* is invalid (e.g. it can't be laid out as valid BPMN), the poll returns `200` with `status: "failed"` and `error.code: "validation_failed"` — not an HTTP `422`.
</Accordion>
