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

# Authentication

> Exchange your API secret for a bearer token, then authenticate every request with it.

The Synthpop API is **default-deny**: every endpoint requires a valid bearer
token, with the sole exceptions of the documentation endpoints
(`/docs`, `/redoc`, `/openapi.json`) and the token-exchange endpoint itself
(`/auth/get_token`). You obtain a token by exchanging a long-lived **API secret**
for a **JSON Web Token (JWT)** (valid for 365 days), then send that JWT as a
`Authorization: Bearer <token>` header on all other calls.

<Steps>
  <Step title="Get your API secret">
    An administrator in your organization creates the API secret from the
    Synthpop UI, or your Synthpop contact provides one. Secrets are per
    environment — see [Environments](#environments) below. Synthpop does not
    store your secret after it is shown, so keep it somewhere safe. Generating a
    new secret immediately invalidates the previous one.
  </Step>

  <Step title="Exchange it for a token">
    `POST /auth/get_token` with the secret in the request body. You get back a
    bearer JWT (`token`) and your organization's settings (`org_settings`).
  </Step>

  <Step title="Call the API with the token">
    Send the token as `Authorization: Bearer <token>` on every request. The
    token is valid for **365 days** (about a year); re-run the exchange when it
    expires.
  </Step>
</Steps>

## The API secret format

An API secret is a single string with **three colon-separated parts**:

```
provider:uid:secret
```

<Warning>
  Some older material describes the secret as having an `api-key:` prefix. That
  is the same thing: `api-key` is just the `provider` segment, so a secret
  minted for API access looks like `api-key:<uid>:<secret>`. Always send the
  **entire** string — all three parts, colons included — exactly as it was
  issued to you. Do not trim, split, or add a prefix yourself.
</Warning>

## Environments

Staging and production are **separate systems** with **separate hosts** and
**separate secrets**. A secret issued for one environment will not work against
the other.

| Environment | Base URL                              |
| ----------- | ------------------------------------- |
| Production  | `https://app.synthpop.ai/public/v1`   |
| Staging     | `https://stage.synthpop.ai/public/v1` |

<Warning>
  Never send a production secret to the staging host (or vice versa). Because
  the two environments have distinct credentials, a mismatched secret is
  rejected as invalid — and you risk leaking a production secret onto a
  non-production system. Keep the environment's base URL and its secret paired
  in your configuration.
</Warning>

## Requesting a token

<ParamField body="api_secret" type="string" required>
  Your full API secret in `provider:uid:secret` form (for example
  `api-key:<uid>:<secret>`). Send the entire string.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  # Production. For staging, use https://stage.synthpop.ai/public/v1
  curl -X POST "https://app.synthpop.ai/public/v1/auth/get_token" \
    -H "accept: application/json" \
    -H "Content-Type: application/json" \
    -d '{"api_secret": "'"$SYNTHPOP_API_SECRET"'"}'
  ```

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

  import requests

  # Choose the environment. For staging, use https://stage.synthpop.ai/public/v1
  BASE_URL = "https://app.synthpop.ai/public/v1"


  def get_jwt_token(api_secret: str, base_url: str) -> str | None:
      headers = {"accept": "application/json", "Content-Type": "application/json"}
      # Send the entire secret, in "provider:uid:secret" form.
      data = {"api_secret": api_secret}

      url = f"{base_url}/auth/get_token"
      response = requests.post(url, headers=headers, json=data)

      if response.status_code == 200:
          return response.json().get("token")
      print(f"Error {response.status_code}: {response.text}")
      return None


  if __name__ == "__main__":
      api_secret = os.environ["SYNTHPOP_API_SECRET"]
      token = get_jwt_token(api_secret, BASE_URL)
      if token:
          print(f"Your JWT token is:\n{token}")
  ```
</CodeGroup>

### Response

A `200` returns the token and, when available, your organization's settings.

<ResponseField name="token" type="string" required>
  The bearer JWT. Present it as `Authorization: Bearer <token>` on every
  authenticated request. Valid for 365 days (about a year).
</ResponseField>

<ResponseField name="org_settings" type="object">
  Settings for the organization the token belongs to.

  <Expandable title="org_settings">
    <ResponseField name="name" type="string" required>
      The organization's name.
    </ResponseField>

    <ResponseField name="external_callback_enabled" type="boolean" required>
      Whether your organization may register a `callback_template` so Synthpop
      notifies you when a task completes. See
      [Async results](/guides/async-results).
    </ResponseField>

    <ResponseField name="external_read_enabled" type="boolean" required>
      Whether auto-read of results is enabled for your organization. See
      [Async results](/guides/async-results).
    </ResponseField>
  </Expandable>
</ResponseField>

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

<Info>
  `org_settings` tells you which asynchronous-delivery features are turned on
  for your org. Callbacks and auto-read are **opt-in per organization** — if the
  flags are `false`, retrieve results by polling instead. Contact Synthpop to
  have a flag enabled.
</Info>

## Using the token

Send the token on every subsequent request. Below, a call to
`GET /task/list` confirms the token works.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://app.synthpop.ai/public/v1/task/list" \
    -H "accept: application/json" \
    -H "Authorization: Bearer $SYNTHPOP_JWT_TOKEN"
  ```

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

  import requests

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

  jwt_token = os.environ["SYNTHPOP_JWT_TOKEN"]
  headers = {
      "accept": "application/json",
      "Authorization": f"Bearer {jwt_token}",
  }

  response = requests.get(f"{BASE_URL}/task/list", headers=headers)
  response.raise_for_status()
  print(response.json())
  ```
</CodeGroup>

## Failure modes

<ResponseField name="400 — Malformed API secret" type="error">
  The `api_secret` is not a well-formed `provider:uid:secret` string. Confirm
  you are sending the entire secret, colons included, with nothing added or
  removed.
</ResponseField>

<ResponseField name="403 — Invalid or obsolete API secret" type="error">
  The secret is well-formed but not accepted — it was revoked, superseded by a
  newer secret, or belongs to a different environment than the host you called.
  Regenerate the secret if needed and make sure it matches the target
  environment.
</ResponseField>

<ResponseField name="401 — Unauthorized" type="error">
  Returned by other endpoints when the bearer token is missing, malformed, or
  expired. Because the API is default-deny, every path outside the small
  allowlist requires the header. Exchange your secret for a fresh token and
  retry.
</ResponseField>

For the full error model across endpoints, see [Errors](/guides/errors).

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" href="/quickstart" icon="rocket">
    Go from an API secret to your first task result.
  </Card>

  <Card title="Async results" href="/guides/async-results" icon="clock">
    Poll for results, or receive a callback when a task completes.
  </Card>

  <Card title="API reference" href="/api-reference/introduction" icon="code">
    Every endpoint, request shape, and response field.
  </Card>

  <Card title="Versioning" href="/reference/versioning" icon="tag">
    How the `v1` in the base URL defines the contract.
  </Card>
</CardGroup>
