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

# Submit an image for evaluation

> Submit an image generated anywhere for automated quality evaluation, then read the pass verdict, score, and issues. Run-less, with curl and JavaScript.

## What you'll do

Submit an image (generated by any tool, not just a Runflow run) for quality evaluation, then read back a pass verdict, a `0.0`-`1.0` score, and the specific issues found. See [Evaluations](/concepts/evaluations) for the concepts behind this flow.

## Prerequisites

* A Runflow API key with the `evaluations:create` and `evaluations:read` scopes (create to submit, read to poll the result). [Create one.](https://app.runflow.io/settings/api-keys)
* An image to evaluate, reachable as an HTTPS URL, a `runflow://assets/{uuid}` reference, or a `data:` URI.
* Enough credit balance to cover one evaluation (submission pre-flights the balance).

## Steps

<Steps>
  <Step title="Check the price (optional)">
    Job classes carry per-class prices. Read them at runtime instead of hardcoding:

    ```bash theme={"dark"}
    curl https://api.runflow.io/v1/evaluations/job-classes \
      -H "Authorization: Bearer $RUNFLOW_API_KEY"
    ```

    Today `standard` is the only active class. The price is frozen onto your evaluation at submission.
  </Step>

  <Step title="Submit the image">
    `POST /v1/evaluations`. Only `generated_image_url` and `task_type` are required; `generation_prompt` is optional but improves prompt-adherence judging.

    ```bash theme={"dark"}
    curl -X POST https://api.runflow.io/v1/evaluations \
      -H "Authorization: Bearer $RUNFLOW_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: 7e3b1c9a-headshot-42" \
      -d '{
        "generated_image_url": "https://example.com/headshot.png",
        "task_type": "headshot",
        "task_description": "LinkedIn-ready corporate headshot",
        "generation_prompt": "studio lighting, neutral background",
        "client_ref": "customer-job-123"
      }'
    ```

    The response is `202 Accepted` with a pending evaluation. Keep the `id`.

    ```json theme={"dark"}
    {
      "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
      "status_code": "pending",
      "job_class_code": "standard",
      "client_ref": "customer-job-123"
    }
    ```

    The `id` is a UUID. Send the `Idempotency-Key` header so a retried submit does not create (and bill) a second evaluation.
  </Step>

  <Step title="Wait for the verdict">
    Evaluation is asynchronous and usually takes tens of seconds. Choose one:

    <Tabs>
      <Tab title="Poll">
        Fetch the evaluation until `status_code` is terminal (`completed` or `failed`). Replace `{id}` with the id from the previous step.

        ```bash theme={"dark"}
        curl https://api.runflow.io/v1/evaluations/{id} \
          -H "Authorization: Bearer $RUNFLOW_API_KEY"
        ```

        ```javascript theme={"dark"}
        const BASE = "https://api.runflow.io";

        async function waitForVerdict(id) {
          const deadline = Date.now() + 2 * 60 * 1000; // safety cap
          while (Date.now() < deadline) {
            const res = await fetch(`${BASE}/v1/evaluations/${id}`, {
              headers: { Authorization: `Bearer ${process.env.RUNFLOW_API_KEY}` },
            });
            if (!res.ok) {
              const err = await res.json().catch(() => ({}));
              throw new Error(`Polling failed (${res.status}): ${err.message ?? "unknown error"}`);
            }
            const evaluation = await res.json();
            if (["completed", "failed"].includes(evaluation.status_code)) {
              return evaluation;
            }
            await new Promise((r) => setTimeout(r, 5000));
          }
          throw new Error("Timed out waiting for a terminal evaluation status");
        }
        ```
      </Tab>

      <Tab title="Callback">
        Pass `callback_url` on submit and Runflow sends a signed POST when the evaluation terminates. No polling needed.

        ```bash theme={"dark"}
        curl -X POST https://api.runflow.io/v1/evaluations \
          -H "Authorization: Bearer $RUNFLOW_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "generated_image_url": "https://example.com/headshot.png",
            "task_type": "headshot",
            "callback_url": "https://your-server.com/webhook/evaluations"
          }'
        ```

        The signing and retry mechanics match run callbacks (verify `Runflow-Signature`, return `2xx` fast, and be idempotent; see [Verify callback signatures](/guides/verify-callback-signatures) and [Handle async callbacks](/guides/handle-async-callbacks)). The body, however, is evaluation-specific, not the run-callback shape:

        ```json theme={"dark"}
        {
          "event": "evaluation.completed",
          "evaluation_id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
          "status": "completed",
          "client_ref": "customer-job-123",
          "run_id": null,
          "overall_passed": true,
          "weighted_pass_rate": 0.92,
          "top_issues": ["Slight shadow under the chin"],
          "top_strengths": ["identity", "framing"],
          "primary_action_code": null,
          "failure_code": null,
          "completed_at": "2026-06-16T09:30:00+00:00"
        }
        ```

        `event` is `evaluation.completed` or `evaluation.failed`. The payload carries the verdict summary plus correlation handles; fetch the full reasoning tree with `GET /v1/evaluations/{id}`.
      </Tab>
    </Tabs>
  </Step>

  <Step title="Read the result">
    On a `completed` evaluation, read the verdict:

    ```json theme={"dark"}
    {
      "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
      "status_code": "completed",
      "overall_passed": true,
      "weighted_pass_rate": 0.92,
      "top_issues": ["Slight shadow under the chin"],
      "top_strengths": ["identity", "framing"],
      "primary_action_code": null,
      "cost": "0.400000"
    }
    ```

    `weighted_pass_rate` is a number (`0.0`-`1.0`); `cost` is a decimal string of credits charged; `top_issues` is a list of short labels. Use `overall_passed` for a yes/no gate and `weighted_pass_rate` for a threshold. For the per-judge breakdown (each issue with its `category` / `subcategory` / `detail`), request embeds: `GET /v1/evaluations/{id}?embed=judges,action,gate_failures`.
  </Step>
</Steps>

## Verify it worked

```bash theme={"dark"}
curl https://api.runflow.io/v1/evaluations/{id} \
  -H "Authorization: Bearer $RUNFLOW_API_KEY"
```

<Check>`status_code` is `completed` and `overall_passed` / `weighted_pass_rate` are populated. You're done.</Check>

## Add reference images

For identity or garment checks, attach up to 4 reference images. Each accepts the same three URL forms as `generated_image_url`.

```bash theme={"dark"}
curl -X POST https://api.runflow.io/v1/evaluations \
  -H "Authorization: Bearer $RUNFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "generated_image_url": "https://example.com/generated.png",
    "task_type": "headshot",
    "reference_images": [
      { "url": "https://example.com/source-face.png", "kind": "source_face", "description": "The person to match" }
    ]
  }'
```

To avoid sending public URLs, upload the file first and pass a `runflow://assets/{uuid}` reference. See [Upload large files](/guides/upload-large-files).

## Troubleshooting

| Symptom           | Likely cause                      | Fix                                                                                                                          |
| ----------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `402` on submit   | Balance below the job-class price | Top up, then resubmit.                                                                                                       |
| `422` on submit   | Bad body                          | Check `task_type` is set, text is under length caps, `<= 4` reference images, and media URLs are `https://` (not `http://`). |
| `429` on submit   | Too many in-flight evaluations    | Wait for some to finish, then retry.                                                                                         |
| `403` on submit   | Key lacks `evaluations:create`    | Recreate the key with the scope.                                                                                             |
| Terminal `failed` | See `failure_code`                | `invalid_media` means a URL could not be fetched; only `processing_failed` and `completed` are billed.                       |

## Related

<CardGroup cols={2}>
  <Card title="Evaluations concept" icon="circle-check" href="/concepts/evaluations">Lifecycle, scoring, job classes, billing.</Card>
  <Card title="Handle async callbacks" icon="webhook" href="/guides/handle-async-callbacks">Receive terminal evaluations by POST.</Card>
  <Card title="Upload large files" icon="upload" href="/guides/upload-large-files">The runflow:// reference flow for local images.</Card>
  <Card title="API reference" icon="code" href="/api-reference/evaluations/submit-an-image-for-quality-evaluation">POST /v1/evaluations and friends.</Card>
</CardGroup>
