> ## 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.

# Quickstart

> From an API key to your first validated result — the full asynchronous path.

This guide takes you from credentials to a structured result in five steps:
get your `api_secret`, exchange it for a bearer token, create a Task with a
document, poll until the Task finishes, and read the verdict.

The API is **asynchronous**. Creating a Task returns immediately with
`status: "pending"` and no results — you retrieve the outcome by polling
`GET /task/get` until the Task reaches a terminal status.

<Note>
  **Base URLs**

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

  Every path below is relative to one of these. The examples use production.
</Note>

<Steps>
  <Step title="Get your api_secret">
    Your `api_secret` identifies your organization to the API. An admin generates or
    resets it in the Synthpop UI; if you don't have one, ask your Synthpop contact.

    It has the exact form `provider:uid:secret` — three colon-separated parts. Treat
    it like a password: keep it server-side and never commit it.

    ```bash theme={null}
    export SYNTHPOP_API_SECRET="<provider>:<uid>:<secret>"
    ```

    <Warning>
      The `api_secret` is a long-lived credential. Exchange it for a
      bearer token (next step) and use that token for API calls — don't send the
      `api_secret` on every request.
    </Warning>
  </Step>

  <Step title="Exchange it for a bearer token">
    `POST /auth/get_token` takes your `api_secret` and returns a bearer token you
    attach to every other call. The token is long-lived (valid for 365 days), so
    cache it and refresh only when it expires.

    <CodeGroup>
      ```bash cURL theme={null}
      # Capture the token into an env var the next steps reuse:
      export SYNTHPOP_JWT_TOKEN=$(curl -s -X POST "https://app.synthpop.ai/public/v1/auth/get_token" \
        -H "Content-Type: application/json" \
        -d "{\"api_secret\": \"$SYNTHPOP_API_SECRET\"}" | jq -r .token)
      ```

      ```python Python theme={null}
      import os
      import requests

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

      def get_token(api_secret: str) -> str:
          response = requests.post(
              f"{BASE_URL}/auth/get_token",
              json={"api_secret": api_secret},
          )
          response.raise_for_status()
          return response.json()["token"]

      jwt_token = get_token(os.environ["SYNTHPOP_API_SECRET"])
      print(jwt_token)
      ```
    </CodeGroup>

    A successful response returns the token plus your organization's settings:

    ```json theme={null}
    {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "org_settings": {
        "name": "Example Health",
        "external_callback_enabled": false,
        "external_read_enabled": false
      }
    }
    ```

    A malformed secret returns `400`; an invalid or revoked one returns `403`.
  </Step>

  <Step title="Create a Task with a document">
    `POST /task/create` is a `multipart/form-data` request. The only required field is
    `task_type`; attach your document(s) under `uploads`.

    <CodeGroup>
      ```bash cURL theme={null}
      # Capture the returned Task UUID for polling:
      export TASK_UUID=$(curl -s -X POST "https://app.synthpop.ai/public/v1/task/create" \
        -H "Authorization: Bearer $SYNTHPOP_JWT_TOKEN" \
        -F "task_type=task" \
        -F "uploads=@referral-packet.pdf;type=application/pdf" | jq -r .uuid)
      ```

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

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

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

    The response is a `TaskDetails` object. Note that it comes back right away with
    `status: "pending"` and empty `verdicts` — the work has only been queued:

    ```json theme={null}
    {
      "uuid": "0685bb22-eeec-776c-8000-ad631be0e894",
      "external_id": null,
      "task_type": "task",
      "status": "pending",
      "name": "Robert: 7",
      "cleared": null,
      "created": "2026-07-15T08:24:15.571185Z",
      "latest_status_change": "2026-07-15T08:24:15.571185Z",
      "category": null,
      "data_items": [
        {
          "uuid": "0685bb22-f93d-7d65-8000-131db9c02849",
          "group": "inputs",
          "tag": "upload",
          "name": "referral-packet.pdf",
          "created": "2026-07-15T08:24:15.571185Z",
          "data_type": "file"
        }
      ],
      "issues": [],
      "verdicts": []
    }
    ```

    Hold onto `uuid` — you'll poll it in the next step.

    <Note>
      `task_type` is **required** and **provisioned for your organization**; an unknown
      value returns `400 "Task type 'X' not in list of tasks for your organization: [...]."` There is no
      endpoint that lists available types — your Synthpop contact tells you which are
      enabled during onboarding.

      `task` (used here) is an org-provisioned convenience type that auto-classifies
      the uploaded document and routes it to the right validation workflow. Other
      organizations use specific validation task types instead. The capability guides (e.g. [Validate an order](/guides/intake/validate-an-order)) use concrete, provisioned task types like `hst_validation_bin` rather than `task`.
    </Note>

    <Tip>
      Want to correlate the Task with a record in your own system? Add
      `-F "external_id=order-4821"` (cURL) or `data={"task_type": ..., "external_id":
              "order-4821"}` (Python). `external_id` is a correlation key you can later read
      by — it does **not** make creates idempotent. Deduplication is content-based:
      re-uploading identical bytes is a no-op.
    </Tip>
  </Step>

  <Step title="Poll until the Task reaches a terminal status">
    `GET /task/get` returns the current `TaskDetails` for a `uuid` (or an
    `external_id` — supply exactly one). Poll it until the Task settles.

    Statuses are exactly: `pending`, `processing`, `waiting`, `completed`, `failed`,
    `invalid`.

    * **Terminal:** `completed`, `failed`, `invalid` — stop polling.
    * **`waiting`:** the Task needs more input from you before it can finish (see the
      note below).
    * **`pending` / `processing`:** keep polling; the Task is queued or in progress
      (under load it may be parked server-side but still reports as `pending`).

    <CodeGroup>
      ```bash cURL theme={null}
      curl "https://app.synthpop.ai/public/v1/task/get?uuid=$TASK_UUID" \
        -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:
                  return task
              if status == "waiting":
                  # The Task needs more documents. Upload them via
                  # POST /task/{uuid}/upload, then continue polling.
                  return task

              time.sleep(5)

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

    <Note>
      A Task that stops at `waiting` is asking for more input — for example, a required
      document that wasn't in the original upload. Add the missing files with
      `POST /task/{uuid}/upload` and the Task resumes on its own. See
      [Responding to waiting tasks](/guides/responding-to-waiting-tasks).
    </Note>

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

  <Step title="Read the verdict">
    Once the Task is `completed`, the result lives in three fields of `TaskDetails`:

    * **`verdict_summaries`** — a plain-English `conclusion` per `code`.
    * **`verdicts`** — the machine-readable decision per `code`: whether it was
      `approved`, and which `issues` (by index) affected it.
    * **`issues`** — everything Synthpop flagged: missing records, expired documents,
      inconsistent data, and so on, each with a human-readable `message` and a `key`.

    ```json theme={null}
    {
      "status": "completed",
      "verdict_summaries": [
        {
          "code": "G0399",
          "conclusion": "Documentation supports the requested service. Required clinical evidence and a signed order are present."
        }
      ],
      "verdicts": [
        { "code": "G0399", "approved": true, "issues": [] }
      ],
      "issues": []
    }
    ```

    When something is missing, the same shapes tell you what and where — an entry in
    `issues` describes the gap, and any affected `verdict` references it by index:

    ```json theme={null}
    {
      "status": "completed",
      "verdicts": [
        { "code": "G0399", "approved": false, "issues": [0] }
      ],
      "issues": [
        {
          "key": "task.medical_record.missing",
          "medical_record_type": "signed-order",
          "message": "A signed physician order is required but was not found."
        }
      ]
    }
    ```

    That's the full loop: authenticate, create, poll, read. From here, wire the same
    pattern into your intake, coverage, or engagement workflow.
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/authentication">
    Token lifetime, the default-deny model, and rotating your `api_secret`.
  </Card>

  <Card title="Tasks & lifecycle" icon="diagram-project" href="/concepts/tasks">
    The Task resource and every status it moves through.
  </Card>

  <Card title="Results: verdicts & issues" icon="clipboard-check" href="/concepts/results">
    The full shape of verdicts, summaries, and task issues.
  </Card>

  <Card title="Validate an order" icon="file-import" href="/guides/intake/overview">
    An end-to-end intake walkthrough.
  </Card>
</CardGroup>
