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

# Upload a VEX document

> POST /api/v1/vex: send OpenVEX, CSAF VEX or CycloneDX VEX for a product family from CI, so other tools' verdicts reach your matches.

<Info>
  **Plans:** Every plan. Refused with 402 while the workspace is read-only. **Who:** anyone with an API token for the
  product family; owners and admins create tokens.
</Info>

`POST /api/v1/vex` stores one VEX document for the product family the token belongs to, binds its statements to the
family's components and matches the family again. It takes the same CI upload token as
[`POST /api/v1/sboms`](/api-reference/sboms). Send the same file again and Pharus answers with the document it already
has, so retries are safe. What Pharus does with the statements is on [VEX from other tools](/monitoring/vex-documents).

```text theme={null}
POST https://app.pharussecurity.com/api/v1/vex
```

## Example

```bash theme={null}
export PHARUS_TOKEN=phr_...   # the product family's token (Settings → API tokens)

govulncheck -format openvex ./... | gzip | curl --fail-with-body -sS -X POST "https://app.pharussecurity.com/api/v1/vex" \
  -H "Authorization: Bearer $PHARUS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Content-Encoding: gzip" \
  -H "X-Pharus-Version: 4.2" \
  --data-binary @-
```

To send Dependency-Track's analysis with its justifications, export the project's VDR:

```bash theme={null}
curl -sS -H "X-Api-Key: $DTRACK_API_KEY" \
  "$DTRACK_URL/api/v1/bom/cyclonedx/project/$DTRACK_PROJECT?variant=vdr&format=json" \
  | gzip | curl --fail-with-body -sS -X POST "https://app.pharussecurity.com/api/v1/vex" \
      -H "Authorization: Bearer $PHARUS_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Content-Encoding: gzip" \
      -H "X-Pharus-Source: dependency_track" \
      --data-binary @-
```

Trivy and Grype findings travel inside a CycloneDX SBOM. Send that SBOM to `/api/v1/sboms` as usual: Pharus keeps its
vulnerabilities as the SBOM's VEX document.

## Headers

| Header | Required | Value |
| - | - | - |
| `Authorization` | Yes | `Bearer phr_...`: an API token for the product family. See [authentication](/api-reference/introduction#authentication). |
| `Content-Type` | Recommended | `application/json`, `application/vnd.cyclonedx+json` or `application/vnd.openvex+json`. Pharus recognises the format from the content. |
| `Content-Encoding` | When compressed | `gzip` or `zstd`. Any encoding other than `gzip`, `x-gzip`, `zstd` or `identity` is refused. |
| `X-Pharus-Version` | No | The product version the document is about, such as `4.2`. Up to 100 characters. Without it, the document holds for every version the family has. |
| `X-Pharus-Source` | No | Where it came from, shown as **Source**: `github`, `azure_devops`, `dependency_track`, `sbomify` or `api`. Anything else, or none, shows as API. |

The query parameter `version` works when there is no `X-Pharus-Version` header.

## Body

The raw VEX document, plain or compressed. Not a multipart form, and not wrapped in another JSON object.

* OpenVEX, such as govulncheck's `-format openvex` output.
* CSAF 2.0 VEX.
* CycloneDX JSON 1.4–1.7 with a `vulnerabilities` section: a VEX document, Dependency-Track's VDR, or Trivy and Grype
  output.
* At most 4 MB as sent, 50 MB decompressed, and 50 000 statements.

A later document from the same tool for the same version replaces the earlier one. See
[how statements bind](/monitoring/vex-documents#how-statements-bind-to-your-components).

## Responses

### 202 Accepted

A new document was stored. Matching runs after the response.

```json theme={null}
{
  "id": "2a7d9c41-6b3e-4f18-9d02-7e5c1a8b3f64",
  "format": "openvex",
  "statements": 12,
  "findings": 0,
  "unbound": 1,
  "version": "4.2",
  "replaced": 1,
  "url": "https://app.pharussecurity.com/sboms/vex/2a7d9c41-6b3e-4f18-9d02-7e5c1a8b3f64",
  "duplicate": false
}
```

### 200 OK

The family already has a document with exactly these bytes. Nothing new was stored, `replaced` is `0`, and
`duplicate` is `true`.

| Field | Meaning |
| - | - |
| `id` | The document's id in Pharus |
| `format` | `openvex`, `csaf` or `cyclonedx` |
| `statements` | Statements with a verdict |
| `findings` | Scanner findings without a verdict |
| `unbound` | Statements that name nothing Pharus can bind: no purl or CPE, or an unknown reference |
| `version` | The product version it holds for, or `null` for every version |
| `replaced` | Earlier documents from the same source for the same version that this one replaced |
| `url` | The document's page in Pharus, for people who are signed in |
| `duplicate` | `true` when Pharus already had this file for the family |

### Errors

Errors return `{ "error", "message" }`, sometimes with `detail`. See the [error format](/api-reference/introduction#responses).

| Status | `error` | What to do |
| - | - | - |
| 400 | `invalid_json` | The body isn't valid JSON. Check that the file was written completely. |
| 400 | `invalid_vex` | The format was recognised, but its structure is broken. |
| 400 | `empty` | The body is empty. Check the file path and `--data-binary`. |
| 400 | `bad_encoding` | The body couldn't be decompressed. Send gzip, zstd or plain JSON. |
| 401 | `unauthorized` | Check the token, its expiry and its status under **Settings → API tokens**. The response has a `WWW-Authenticate` header. |
| 402 | `billing_lapsed` | The workspace is read-only. An owner subscribes under **Settings → Billing**, or reopens a closed workspace. |
| 404 | `family_not_found` | The token's product family is no longer available. |
| 413 | `too_large` | The document is over 4 MB as sent or 50 MB decompressed. |
| 415 | `unsupported_vex` | Send OpenVEX, CSAF 2.0 VEX, or CycloneDX JSON 1.4–1.7 with a `vulnerabilities` section. An SBOM goes to `/api/v1/sboms`. |
| 422 | `no_statements` | The document has no vulnerability statements or findings. |
| 422 | `too_many_statements` | The document has more than 50 000 statements. |
| 429 | `rate_limited` | Wait the number of seconds in `Retry-After`. Each token has 60 requests per hour, shared with `POST /api/v1/sboms`. |
| 500 | `internal` | Try again. |

## Related

<CardGroup cols={2}>
  <Card title="VEX from other tools" icon="file-check-2" href="/monitoring/vex-documents" />

  <Card title="Upload an SBOM" icon="upload" href="/api-reference/sboms" />

  <Card title="Dependency-Track" icon="webhook" href="/api-reference/dependency-track" />

  <Card title="API introduction" icon="key-round" href="/api-reference/introduction" />
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.