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

# Validate an order

> End-to-end: create a validation Task, poll it to a terminal status, and interpret the verdict, summaries, and issues.

This is the complete intake flow. You'll create a validation Task with a
document, poll it until it finishes, handle the case where it needs more input,
and read the structured decision back out. Every request is grounded in the
public API — there are no intake-specific endpoints.

<Note>
  **Base URLs**

  * Production: `https://app.synthpop.ai/public/v1`
  * Staging: `https://stage.synthpop.ai/public/v1`

  The examples use production and assume `$SYNTHPOP_JWT_TOKEN` holds a valid
  bearer token. If you don't have one yet, see the
  [Quickstart](/quickstart) and [Authentication](/authentication).
</Note>

<Note>
  **Example task type.** These examples use `hst_validation_bin` — validating a
  sleep-study (HST) referral packet — as a concrete, real value. Your
  organization's validation task types are provisioned during onboarding; swap in
  the one you were given. There is no endpoint that lists them.
</Note>

<Steps>
  <Step title="Create the validation Task">
    `POST /task/create` is a `multipart/form-data` request. Set `task_type` to your
    provisioned validation type and attach the referral or order document(s) under
    `uploads`. You can attach several files in one request — a whole packet at once.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST "https://app.synthpop.ai/public/v1/task/create" \
        -H "Authorization: Bearer $SYNTHPOP_JWT_TOKEN" \
        -F "task_type=hst_validation_bin" \
        -F "uploads=@referral-packet.pdf;type=application/pdf" \
        -F "external_id=order-4821"
      ```

      ```python Python theme={null}
      from pathlib import Path
      import requests

      BASE_URL = "https://app.synthpop.ai/public/v1"

      def create_validation_task(
          jwt_token: str, file_path: Path, task_type: str, external_id: str | None = None
      ) -> dict:
          data = {"task_type": task_type}
          if external_id is not None:
              data["external_id"] = external_id
          with file_path.open("rb") as f:
              response = requests.post(
                  f"{BASE_URL}/task/create",
                  headers={"Authorization": f"Bearer {jwt_token}"},
                  data=data,
                  files={"uploads": (file_path.name, f, "application/pdf")},
              )
          response.raise_for_status()
          return response.json()

      task = create_validation_task(
          jwt_token,
          Path("referral-packet.pdf"),
          task_type="hst_validation_bin",
          external_id="order-4821",
      )
      print(task["uuid"], task["status"])  # -> "<uuid>" "pending"
      ```
    </CodeGroup>

    The response is a `TaskDetails` object. It comes back right away with
    `status: "pending"` and empty `verdicts` — nothing has been evaluated yet:

    ```json theme={null}
    {
      "uuid": "06862491-0dc0-77b7-8000-0539870235af",
      "external_id": "order-4821",
      "task_type": "hst_validation_bin",
      "status": "pending",
      "name": "Order 4821",
      "cleared": null,
      "created": "2026-07-15T08:21:37.396333Z",
      "latest_status_change": "2026-07-15T08:21:37.396333Z",
      "category": null,
      "data_items": [
        {
          "uuid": "06862491-168e-7d50-8000-539ce3c38468",
          "group": "inputs",
          "tag": "upload",
          "name": "referral-packet.pdf",
          "created": "2026-07-15T08:21:37.396333Z",
          "data_type": "file"
        }
      ],
      "issues": [],
      "verdicts": [],
      "verdict_summaries": []
    }
    ```

    Hold onto `uuid`. If you passed an `external_id`, you can also read the Task by
    that instead.

    <Tip>
      `external_id` is a **correlation key** — a handle from your own system you can
      later read the Task by. It does **not** make creates idempotent. Deduplication
      is content-based: uploading identical bytes again is a no-op.
    </Tip>
  </Step>

  <Step title="Poll until the Task reaches a terminal status">
    `GET /task/get` returns the current `TaskDetails`. Supply exactly one of `uuid`
    or `external_id` (neither or both returns `400`). Poll until the Task settles.

    The terminal statuses are `completed`, `failed`, and `invalid`. A Task can also
    stop at `waiting`, which means it needs more input from you (next step);
    `pending` and `processing` both mean "keep polling."

    <CodeGroup>
      ```bash cURL theme={null}
      curl "https://app.synthpop.ai/public/v1/task/get?uuid=$TASK_UUID" \
        -H "Authorization: Bearer $SYNTHPOP_JWT_TOKEN"

      # Or read it back by your own correlation id:
      curl "https://app.synthpop.ai/public/v1/task/get?external_id=order-4821" \
        -H "Authorization: Bearer $SYNTHPOP_JWT_TOKEN"
      ```

      ```python Python theme={null}
      import time

      TERMINAL = {"completed", "failed", "invalid"}

      def poll_task(jwt_token: str, uuid: str) -> dict:
          headers = {"Authorization": f"Bearer {jwt_token}"}
          while True:
              response = requests.get(
                  f"{BASE_URL}/task/get",
                  headers=headers,
                  params={"uuid": uuid},
              )
              response.raise_for_status()
              task = response.json()
              status = task["status"]
              print(f"status: {status}")

              if status in TERMINAL or status == "waiting":
                  return task
              time.sleep(5)

      task = poll_task(jwt_token, task["uuid"])
      ```
    </CodeGroup>

    <Tip>
      Prefer not to poll? If your organization has callbacks enabled, pass a
      `callback_template` URL on create and Synthpop issues a GET to it when the Task
      finishes. Callbacks are opt-in and gated per organization — see
      [Working with async results](/guides/async-results).
    </Tip>
  </Step>

  <Step title="Handle a Task that stops at `waiting`">
    Validation often finds a required document missing. Instead of failing, the Task
    stops at `status: "waiting"` and lists what it needs in `issues` — here, a missing
    face-to-face visit note and insurance carrier:

    ```json theme={null}
    {
      "uuid": "06862491-0dc0-77b7-8000-0539870235af",
      "external_id": "order-4821",
      "task_type": "hst_validation_bin",
      "status": "waiting",
      "name": "Order 4821",
      "category": "HST",
      "issues": [
        {
          "key": "task.medical_record.missing",
          "medical_record_type": "face to face visit note",
          "message": "Upload a face-to-face visit note."
        },
        {
          "key": "task.data.missing",
          "property": "insurance_carrier",
          "message": "Missing insurance carrier."
        }
      ],
      "verdicts": [
        { "code": "hst", "approved": false, "issues": [0, 1] }
      ],
      "verdict_summaries": []
    }
    ```

    Add the missing files with `POST /task/{uuid}/upload` (the Task must be in
    `waiting` to accept them), then go back to polling. The Task resumes on its own.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST "https://app.synthpop.ai/public/v1/task/$TASK_UUID/upload" \
        -H "Authorization: Bearer $SYNTHPOP_JWT_TOKEN" \
        -F "uploads=@visit-note.pdf;type=application/pdf"
      ```

      ```python Python theme={null}
      def upload_more(jwt_token: str, uuid: str, file_path: Path) -> dict:
          with file_path.open("rb") as f:
              response = requests.post(
                  f"{BASE_URL}/task/{uuid}/upload",
                  headers={"Authorization": f"Bearer {jwt_token}"},
                  files={"uploads": (file_path.name, f, "application/pdf")},
              )
          response.raise_for_status()
          return response.json()

      upload_more(jwt_token, task["uuid"], Path("visit-note.pdf"))
      task = poll_task(jwt_token, task["uuid"])  # resume polling
      ```
    </CodeGroup>

    <Note>
      For the full resume loop — including what to do when the Task keeps asking for
      more — see [Responding to waiting tasks](/guides/responding-to-waiting-tasks).
    </Note>
  </Step>

  <Step title="Interpret the completed result">
    Once the Task is `completed`, read the decision from three fields. Here is a
    completed validation result for our example packet:

    ```json theme={null}
    {
      "uuid": "06862491-0dc0-77b7-8000-0539870235af",
      "external_id": "order-4821",
      "task_type": "hst_validation_bin",
      "status": "completed",
      "name": "Order 4821 (12/31/1999): HST",
      "cleared": null,
      "created": "2026-07-15T08:21:37.396333Z",
      "latest_status_change": "2026-07-15T08:24:29.954317Z",
      "category": "HST",
      "data_items": [
        {
          "uuid": "0686249b-de84-7ae9-8000-9d974cde3912",
          "group": "outputs",
          "tag": "referral-text",
          "name": "Home Sleep Test Analysis",
          "created": "2026-07-15T08:24:29.954317Z",
          "data_type": "text",
          "data": "Issues: No issues found — service supported. Conclusion: The packet supports the requested home sleep test. Documented daytime sleepiness, fatigue, habitual loud snoring, and an Epworth Sleepiness Scale score of 11 support medical necessity."
        }
      ],
      "issues": [],
      "verdicts": [
        { "code": "hst", "approved": true, "issues": [] }
      ],
      "verdict_summaries": [
        {
          "code": "hst",
          "conclusion": "Documentation supports the requested service. Required clinical evidence and a signed order are present."
        }
      ]
    }
    ```

    Read the three fields together:

    * **`verdict_summaries`** — the plain-English `conclusion` per `code`. Show this
      to a human reviewer.
    * **`verdicts`** — the machine-readable decision per `code`: `approved` true or
      false, and an `issues` list.
    * **`issues`** — the flat list of everything flagged. Each entry has a `key`
      (its type) and a human-readable `message`.

    A `verdict`'s `issues` are **indices into the top-level `issues` array**, not the
    issues themselves. When a code is not approved, resolve each index to explain why:

    ```python theme={null}
    def explain_verdicts(task: dict) -> None:
        issues = task["issues"]
        for verdict in task["verdicts"]:
            status = "APPROVED" if verdict["approved"] else "NOT APPROVED"
            print(f"{verdict['code']}: {status}")
            for i in verdict["issues"]:
                print(f"  - {issues[i]['message']} ({issues[i]['key']})")

    explain_verdicts(task)
    ```

    For a not-approved code, this prints the exact blocking reasons — for example:

    ```text theme={null}
    hst: NOT APPROVED
      - Upload a face-to-face visit note. (task.medical_record.missing)
      - Missing insurance carrier. (task.data.missing)
    ```

    <Warning>
      A completed Task is not necessarily an approved one. Always check each
      `verdict.approved` — a Task can reach `completed` with unmet criteria and a
      `false` verdict. `completed` means "Synthpop finished evaluating," not "the
      order is supported."
    </Warning>

    That's the full intake loop: create, poll, resolve `waiting`, and interpret the
    verdict. Wire the same pattern behind your own intake queue.
  </Step>
</Steps>

## Related

<CardGroup cols={2}>
  <Card title="Intake overview" icon="file-import" href="/guides/intake/overview">
    How the capability maps to the API and what a result contains.
  </Card>

  <Card title="Multi-patient submissions" icon="users" href="/guides/intake/multi-patient">
    When one packet holds several patients and splits into child Tasks.
  </Card>

  <Card title="Results: verdicts & issues" icon="clipboard-check" href="/concepts/results">
    Every issue `key` and the full verdict shape.
  </Card>

  <Card title="Working with async results" icon="clock" href="/guides/async-results">
    Polling versus callbacks, and load backpressure.
  </Card>
</CardGroup>
