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

# Advisory API

export const GCell = ({children, className}) => <div className={`grid-table-cell ${className || ""}`} role="cell">
    {children}
  </div>;

export const GTH = ({children, className}) => <div className={`grid-table-th ${className || ""}`} role="columnheader">
    {children}
  </div>;

export const GRow = ({children}) => <div className="grid-table-row" role="row">{children}</div>;

export const GBody = ({children}) => <div className="grid-table-body" role="rowgroup">{children}</div>;

export const GHead = ({children}) => <div className="grid-table-head" role="rowgroup">{children}</div>;

export const GTable = ({children, className, cols}) => <div className={`grid-table not-prose overflow-hidden rounded-2xl ${className || ""}`} style={{
  "--grid-table-cols": cols
}} role="table">
    {children}
  </div>;

export const Comments = ({children}) => {
  return <div class="my-4 px-5 py-4 overflow-hidden rounded-2xl flex gap-3 border border-zinc-500/20 bg-zinc-50/50 dark:border-zinc-500/30 dark:bg-zinc-500/10" data-callout-type="comments">
      <div class="w-4">
        <svg width="14" height="14" viewBox="0 0 640 640" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" aria-label="Comments">
            <path d="M320 112C434.9 112 528 205.1 528 320C528 434.9 434.9 528 320 528C205.1 528 112 434.9 112 320C112 205.1 205.1 112 320 112zM320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM280 400C266.7 400 256 410.7 256 424C256 437.3 266.7 448 280 448L360 448C373.3 448 384 437.3 384 424C384 410.7 373.3 400 360 400L352 400L352 312C352 298.7 341.3 288 328 288L280 288C266.7 288 256 298.7 256 312C256 325.3 266.7 336 280 336L304 336L304 400L280 400zM320 256C337.7 256 352 241.7 352 224C352 206.3 337.7 192 320 192C302.3 192 288 206.3 288 224C288 241.7 302.3 256 320 256z" />
        </svg>
      </div>
      <div class="text-sm prose min-w-0 w-full">
        {children}
      </div>
    </div>;
};

<Badge shape="pill" stroke color="blue">Open Beta</Badge>

The Advisory API gives you programmatic access to Anaconda's curated security advisory data for conda packages. Use it to sync vulnerability information into your Security Information and Event Management (SIEM) system, IT Service Management (ITSM) platform, or audit pipeline.

Each advisory is a Common Vulnerabilities and Exposures (CVE) record, and the advisory ID is typically the CVE ID.

Advisories apply to artifacts, not to packages as a whole. A package has many versions, and each version can have many builds. Each build of each version is a separate artifact. An advisory identifies exactly which artifacts are affected.

For example, suppose a vulnerability affects version 1.2.3 of a package. The advisory identifies the affected builds of 1.2.3, such as `py39h1234567_0` on `linux-64`. If Anaconda patches the vulnerability in a later build of 1.2.3, the patched build isn't identified as affected.

Anaconda produces advisories through the same curation process that powers the CVE information shown in Anaconda Platform. For more information about CVEs and the curation process, see [Common Vulnerabilities and Exposures (CVEs)](/docs/anaconda-platform/admin/cve).

## Authentication

### Obtaining a token

Exchange your [service account](/docs/anaconda-platform/admin/service-accounts) credentials for an access token:

<CodeGroup>
  ```sh Terminal theme={null}
  curl --request POST \
      --url "https://anaconda.com/api/iam/token" \
      --header "Content-Type: application/x-www-form-urlencoded" \
      --data "grant_type=client_credentials&client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>"
  ```

  ```powershell PowerShell theme={null}
  Invoke-WebRequest -Method POST `
      -Uri "https://anaconda.com/api/iam/token" `
      -ContentType "application/x-www-form-urlencoded" `
      -Body "grant_type=client_credentials&client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>"
  ```
</CodeGroup>

<Comments>
  Replace \<CLIENT\_ID> and \<CLIENT\_SECRET> with your service account credentials.
</Comments>

The response includes an `access_token`:

```json theme={null}
{
  "access_token": "<ACCESS_TOKEN>",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "HttpOnly"
}
```

<Note>
  Access tokens expire after 15 minutes (900 seconds). If your sync job runs longer, request a new token when the current one expires.
</Note>

### Making authenticated requests

Include the token in the `Authorization` header:

<CodeGroup>
  ```sh Terminal theme={null}
  curl --request GET \
      --url "https://anaconda.com/api/v1/advisories/feed" \
      --header "Authorization: Bearer <ACCESS_TOKEN>"
  ```

  ```powershell PowerShell theme={null}
  Invoke-WebRequest -Method GET `
      -Uri "https://anaconda.com/api/v1/advisories/feed" `
      -Headers @{"Authorization" = "Bearer <ACCESS_TOKEN>"}
  ```
</CodeGroup>

<Comments>
  Replace \<ACCESS\_TOKEN> with the token from the previous step.
</Comments>

## Base URL

```
https://anaconda.com
```

## Endpoints

<GTable cols="15% 40% 45%">
  <GHead>
    <GRow>
      <GTH>Method</GTH>
      <GTH>Path</GTH>
      <GTH>Description</GTH>
    </GRow>
  </GHead>

  <GBody>
    <GRow>
      <GCell>`GET`</GCell>
      <GCell>[/api/v1/advisories/feed](/docs/anaconda-platform/admin/advisory-api/get-advisory-feed)</GCell>
      <GCell>Retrieve a paginated feed of security advisories</GCell>
    </GRow>

    <GRow>
      <GCell>`GET`</GCell>
      <GCell>[/api/v1/advisories/{advisory_id}](/docs/anaconda-platform/admin/advisory-api/get-advisory-by-id)</GCell>
      <GCell>Retrieve a single advisory by ID</GCell>
    </GRow>
  </GBody>
</GTable>

## Syncing the advisory feed

Sync the feed to keep your own up-to-date copy of Anaconda's advisory data. The first sync downloads every advisory Anaconda has published; later syncs download only what changed. With a local copy of the advisory data, your tooling can check every package you run against the latest CVEs, as often as you want, without an API call for each check.

On your first sync, request the feed with no parameters to get every advisory Anaconda has published. Run a full sync only once. The response returns a `watermark`; save it. On later syncs, pass that watermark as the `modified_since` parameter to get only the advisories that changed since your last sync.

The response contains advisories and pagination metadata:

<Tip>
  To view the command output cleanly in your terminal, pipe the response through `jq`.
</Tip>

```json Response pagination metadata example highlight={7-10} theme={null}
{
  "feed_version": 1,
  "feed_format": "nvdv2",
  "modified_since": null,
  "count": 50,
  "limit": 50,
  "watermark": "2026-09-10T04:33:52.945000+00:00",
  "continuation": "eyJmZWVkX3ZlcnNpb24iOjEs...",
  "next_url": "/api/v1/advisories/feed?continuation=eyJmZWVkX3ZlcnNpb24iOjEs...",
  "has_more": true
}
```

The fields work as follows:

* `watermark`: The most recent change included in the results. The watermark is the same on every page of a sync. Save it when you reach the last page; it's your starting point for the next sync.
* `continuation` and `next_url`: Present while more pages remain. To get the next page, follow the `next_url`, or pass the `continuation` value back as the `continuation` query parameter. When you pass `continuation`, the API ignores all other query parameters. Continuation tokens stay valid for at least 24 hours, but don't persist them long term like the `watermark`.
* `has_more`: Displays `true` while more pages remain.

When you finish a sync, index the advisories locally by package URL (PURL) or by the affected artifacts' SHA256 hashes (`artifact_sha256s`) so your tooling can match them against installed packages. Each PURL corresponds to one of the SHA256 hashes in `artifact_sha256s`; either one works as a lookup key.

For a working implementation of this pattern, continue to [Monitoring for new advisories with Python](#monitoring-for-new-advisories-with-python).

## Monitoring for new advisories with Python

The following script shows the full sync pattern. It gets an access token, downloads the advisories that changed since the last run, saves the new watermark, and prints a summary. On the first run, when no watermark exists, it downloads the full feed.

The script requires the `requests` package, and it reads your [service account](/docs/anaconda-platform/admin/service-accounts) credentials from environment variables.

To use the script:

<Steps>
  <Step title="Set the environment variables">
    <CodeGroup>
      ```sh Terminal theme={null}
      export ANACONDA_CLIENT_ID="<CLIENT_ID>"
      export ANACONDA_CLIENT_SECRET="<CLIENT_SECRET>"
      ```

      ```powershell PowerShell theme={null}
      $env:ANACONDA_CLIENT_ID = "<CLIENT_ID>"
      $env:ANACONDA_CLIENT_SECRET = "<CLIENT_SECRET>"
      ```
    </CodeGroup>

    <Comments>
      Replace \<CLIENT\_ID> and \<CLIENT\_SECRET> with your service account credentials.
    </Comments>
  </Step>

  <Step title="Save the script">
    Save the following script as `sync.py`:

    ```py expandable theme={null}
    import os

    import requests

    BASE_URL = "https://anaconda.com"
    WATERMARK_FILE = "last_watermark.txt"


    def get_token():
        """Exchange service account credentials for an access token."""
        response = requests.post(
            f"{BASE_URL}/api/iam/token",
            data={
                "grant_type": "client_credentials",
                "client_id": os.environ["ANACONDA_CLIENT_ID"],
                "client_secret": os.environ["ANACONDA_CLIENT_SECRET"],
            },
        )
        response.raise_for_status()
        return response.json()["access_token"]


    def load_watermark():
        """Load the watermark saved by the last run, if one exists."""
        try:
            with open(WATERMARK_FILE) as f:
                return f.read().strip() or None
        except FileNotFoundError:
            return None


    def sync_advisories():
        """Return the advisories that changed since the last run."""
        headers = {"Authorization": f"Bearer {get_token()}"}
        watermark = load_watermark()
        params = {"limit": 100}
        if watermark:
            # Incremental sync: return only what changed since the watermark.
            params["modified_since"] = watermark
        # With no watermark, the API returns every advisory.

        advisories = []
        url = f"{BASE_URL}/api/v1/advisories/feed"
        while True:
            response = requests.get(url, headers=headers, params=params)
            response.raise_for_status()
            page = response.json()
            advisories.extend(page["advisories"])
            if not page["has_more"]:
                break
            # next_url carries the token for the next page. The API ignores
            # the other query parameters when the page token is present.
            url = BASE_URL + page["next_url"]
            params = None

        # The watermark is null when nothing changed. Keep the old one.
        if page["watermark"]:
            with open(WATERMARK_FILE, "w") as f:
                f.write(page["watermark"])

        return advisories


    if __name__ == "__main__":
        changed = sync_advisories()
        print(f"{len(changed)} advisories changed since the last run")
        for advisory in changed:
            print(advisory["advisory_id"], "-", advisory["canonical"]["description"][:80])
    ```
  </Step>

  <Step title="Run the script">
    ```sh theme={null}
    python sync.py
    ```

    The first run downloads the full feed, which can take several minutes. Later runs download only the advisories that changed since the previous run.

    <Note>
      The first full sync can take long enough that the access token expires. If a request returns a `401` error, get a new token and continue paging from the last `next_url`.
    </Note>
  </Step>

  <Step title="Schedule the script">
    Optional. To keep monitoring over time, schedule the script to run regularly, for example, with cron or a scheduled CI job.
  </Step>
</Steps>

## Matching advisories against your environments

What you do with changed advisories depends on your systems. One common approach is to compare the packages each advisory affects against the packages you have installed.

The following example extends the script above. *Add it to the same file.*

The example parses the PURLs of the affected artifacts and compares them with the packages installed in the active conda environment. The match is exact: the name, version, build, and platform must all match. It checks only the advisories from the current sync (`changed`), so it doesn't call the API again.

<Note>
  The example requires the `packageurl-python` package (`conda install packageurl-python`).
</Note>

```py expandable theme={null}
import json
import subprocess

from packageurl import PackageURL


def load_inventory():
    """Return the artifacts installed in the active environment."""
    result = subprocess.run(["conda", "list", "--json"], capture_output=True, check=True)
    return {
        (pkg["name"], pkg["version"], pkg["build_string"], pkg["platform"])
        for pkg in json.loads(result.stdout)
    }


def find_matches(advisories, inventory):
    """Return the advisories that affect an artifact in the inventory."""
    for advisory in advisories:
        for purl in advisory["canonical"]["purls"]:
            parsed = PackageURL.from_string(purl)
            qualifiers = parsed.qualifiers or {}
            artifact = (
                parsed.name,
                parsed.version,
                qualifiers.get("build"),
                qualifiers.get("subdir"),
            )
            if artifact in inventory:
                yield advisory
                break


inventory = load_inventory()
for match in find_matches(changed, inventory):
    print("Environment affected by", match["advisory_id"])
```

<Note>
  The example checks the environment that's active when the script runs. To check a different environment, pass its name to `conda list`, for example `conda list --name <ENV_NAME> --json`.

  Keep this in mind when you schedule the script: cron jobs and CI runners don't activate environments, so the active environment is `base` unless the job activates another one or targets it by name.

  ***

  Matching on the exact artifact matters: Anaconda sometimes fixes a vulnerability in a new build of the same package version, and some CVEs affect only certain platforms. Advisories identify only artifacts that Anaconda builds and maintains, so packages installed from other channels, such as conda-forge, don't match. For the strictest comparison, match on the advisory's `artifact_sha256s` instead of the PURL qualifiers.
</Note>
