# Pharma Observatory CLI

Read Pharma Observatory data from your terminal with the pharma command: install it, sign in, and run a dataset command or a SQL query.

pharma is a command-line client for the Pharma Observatory REST API. The command is `pharma` in every example. It talks to the REST API only and needs a plan with API access.

- [Install](#install)
- [Quickstart](#quickstart)
- [Sign in and profiles](#sign-in)
- [Dataset commands](#commands)
- [SQL with query](#query)
- [Output formats](#output)
- [Exit codes and quota](#exit-codes)
- [Configuration](#configuration)

## Install

**macOS (Homebrew)**

Install with Homebrew, which also works on Linux.

**Homebrew**

```sh
brew install scientist-labs/pharmaobservatory/pharma
```

**Check the install**

```sh
pharma version
```

**Debian and Ubuntu (apt)**

Add the signed apt repository, then install. The repository metadata is signed; the individual packages are not.

**apt**

```sh
sudo install -d /usr/share/keyrings
curl -fsSL https://packages.pharmaobservatory.com/pharmaobservatory.gpg \
  | sudo tee /usr/share/keyrings/pharmaobservatory-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/pharmaobservatory-archive-keyring.gpg] https://packages.pharmaobservatory.com/apt stable main" \
  | sudo tee /etc/apt/sources.list.d/pharmaobservatory.list
sudo apt-get update && sudo apt-get install pharma
```

**Check the install**

```sh
pharma version
```

**Fedora and RHEL (dnf)**

Add the repository, then install with dnf or yum. The repository metadata is signed; the individual packages are not.

**dnf**

```sh
sudo curl -fsSL -o /etc/yum.repos.d/pharmaobservatory.repo https://packages.pharmaobservatory.com/rpm/pharmaobservatory.repo
sudo dnf install pharma
```

**Check the install**

```sh
pharma version
```

**Windows (release zip)**

Download the Windows zip from the GitHub releases page, verify it, unzip it and put pharma.exe on your PATH.

**Verify the download**

```sh
cosign verify-blob \
  --certificate-identity 'https://github.com/scientist-labs/pharmaobservatory-cli/.github/workflows/release.yml@refs/tags/v0.1.0' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --bundle checksums.txt.sigstore.json checksums.txt
sha256sum --check --ignore-missing checksums.txt
```

**Check the install**

```sh
pharma version
```

**Any OS (release archive)**

Each GitHub release carries archives for Linux, macOS and Windows. Verify the checksums with cosign (keyless) and then check the archive against them; replace the tag with the release you downloaded.

**Verify a release download**

```sh
cosign verify-blob \
  --certificate-identity 'https://github.com/scientist-labs/pharmaobservatory-cli/.github/workflows/release.yml@refs/tags/v0.1.0' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --bundle checksums.txt.sigstore.json checksums.txt
sha256sum --check --ignore-missing checksums.txt
```

**Check the install**

```sh
pharma version
```

- [Releases on GitHub](https://github.com/scientist-labs/pharmaobservatory-cli/releases)
- [Source on GitHub](https://github.com/scientist-labs/pharmaobservatory-cli)

## Quickstart

**1. Sign in**

```sh
pharma login
```

**2. Check who you are**

```sh
pharma whoami
```

**Example output, sample values**

```
$ pharma whoami
profile: prod
origin:  https://pharmaobservatory.com
sub:     user_01EXAMPLE
name:    Ada Example
email:   ada@example.com
org:     Example Biotech
plan:    Individual (active)
roles:   substrate:api
quota:   remaining per bucket, keyed by user
  requests_per_minute  60 of 60 requests left per 1 minute; resets 2026-10-20T09:15:00Z
  requests_per_day     1000 of 1000 requests left per 24 hours; resets 2026-10-21T00:00:00Z
```

**3. Read a dataset**

```sh
pharma biomarkers list --limit 5
```

## Sign in and profiles

**Sign in**

```sh
pharma login                  # opens your browser; loopback redirect on 127.0.0.1:53682
pharma login --no-browser     # prints the URL; sign in anywhere, then paste the callback URL (or just its code)
pharma whoami                 # profile, origin, who, organization, plan, roles and remaining quota
pharma whoami --json          # the raw body
pharma logout                 # removes the stored credential for the profile
```

Profiles: `prod` (the default) and `staging` are built in. Pick one with `--profile`, `$PHARMA_PROFILE` or `default_profile` in `config.yml`. Create a custom profile with `pharma login --profile NAME --origin URL`.

The refresh token lives in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager). Where none is available, or with `PHARMA_CREDENTIAL_STORE=file`, it lives in `credentials.json` (mode 0600) in the config directory. `pharma logout` deletes it locally and does not revoke it server-side.

## Dataset commands

One command group per object type, then the datasets of that type. The page for each object type lists its datasets, columns and source credits.

- [pharma companies: Companies (20 datasets)](https://pharmaobservatory.com/cli/companies)
- [pharma legal-entities: Legal entities (11 datasets)](https://pharmaobservatory.com/cli/legal_entities)
- [pharma biomarkers: Biomarkers (11 datasets)](https://pharmaobservatory.com/cli/biomarkers)
- [pharma cell-lines: Cell lines (1 dataset)](https://pharmaobservatory.com/cli/cell_lines)
- [pharma sites: Sites (8 datasets)](https://pharmaobservatory.com/cli/sites)
- [pharma conditions: Conditions (11 datasets)](https://pharmaobservatory.com/cli/conditions)
- [pharma trials: Trials (12 datasets)](https://pharmaobservatory.com/cli/trials)

**The shapes**

```sh
pharma companies list                                  # the object type's primary dataset
pharma companies get <id>                              # one record and the datasets reachable from it
pharma companies get <id> sponsor-trials               # one dataset of a record
pharma datasets list                                   # every published dataset
pharma datasets get <name>                             # one dataset's grain and columns
```

Command and dataset names are the API names with `-` for `_`; the API spelling also works (`pharma cell_lines list`). Ids are passed through verbatim. Put the id and the dataset before any flags. `pharma <object> --help` lists the datasets and `pharma <object> get <id> <dataset> --help` lists a dataset's flags.

**Examples**

```sh
pharma biomarkers get ESR1 biomarker-evidence
pharma trials get NCT01234567 trial-drugs
pharma conditions get D001943 condition-trials
pharma companies get <id> sponsor-trials --scope group
```

Every dataset read takes `--limit`, `--cursor`, `--sort`, `--group-by` and `--measure` (and `--scope` on company datasets), plus `--filter`, repeatable and keyed by column. `--filter column=value` is equality (a repeated column is an IN list); `column.gt`, `.gte`, `.lt`, `.lte`, `.ne`, `.contains` and `.null` are the other operators.

**Filter, sort and group**

```sh
pharma biomarkers list --filter gene_symbol=EGFR --filter evidence_tier.lte=2 --sort -n_trial_mention
pharma biomarkers list --group-by biomarker_kind --measure count
```

`--all` follows the cursor to the last page and prints one table (or one CSV); it cannot be combined with `--json`. The walk stops early, prints what it has and exits 9 when the operation takes no `--cursor`, the server repeats a cursor, or it reaches 1000 pages.

`pharma api <path>` makes an authenticated GET and prints the body; `-F key=value` adds a query parameter.

**The api command**

```sh
pharma api biomarkers -F kind=gene -F limit=5
pharma api /api/v1/whoami
```

## SQL with query

`pharma query` runs one SQL SELECT over one published dataset, with no JOIN. It needs a signed-in account (`pharma login` first). Pass `-` to read the SQL from stdin.

**Run a query**

```sh
pharma query "SELECT biomarker_kind, count(*) AS n FROM biomarkers GROUP BY biomarker_kind"
pharma query "SELECT display_name, gene_symbol FROM biomarkers WHERE gene_symbol = 'EGFR'" --csv
pharma query - < query.sql
```

The server stores the full result for 24 hours and enforces its own limits on rows, statement length and time. The footer names the stored result and the next page; page it with `--result` and `--cursor`, or add `--all` to follow the cursor to the end.

**Page a stored result**

```sh
pharma query --result <result_id> --cursor <cursor>
pharma query --result <result_id> --all
```

A refused query prints the server's error code, message and hint, and exits with the code in the table below (most refusals exit 2).

## Output formats

The default is a table, with a footer on stderr (source credit, as-of date, paging).

**Table, JSON and CSV**

```sh
pharma biomarkers list --limit 5
pharma biomarkers list --limit 5 --json
pharma biomarkers list --limit 5 --csv
```

`--json` prints the server's body unchanged and `--csv` builds CSV from the response columns; they cannot be combined. `--quiet` (alias `--no-footer`) drops the as-of and paging lines. The source credit always prints: on stderr, and as trailing `# Source:` lines in CSV output. To skip those lines in pandas, read the file with `comment='#'`.

## Exit codes and quota

A 429 exits 6 and says which limit was hit. The CLI does not wait by default; `--wait` sleeps for the Retry-After (at most 2 minutes) and retries once. A paging-depth refusal has no Retry-After: narrow the query. `pharma exit-codes` prints this table.

**Wait out a rate limit**

```sh
pharma biomarkers list --all --wait
```

| Exit | Class | When |
| --- | --- | --- |
| 0 | ok | Success |
| 1 | unexpected | An error the table does not name, or a failure in the CLI itself |
| 2 | usage | A bad flag or argument, an unknown command, or a query the server refuses |
| 3 | auth | Not logged in, or a rejected token |
| 4 | forbidden | Access pending, a missing role, or not entitled |
| 5 | not found | An unknown record or page, or a stored query result that expired |
| 6 | rate limited | A quota was reached; the message says which, when it resets and how long to wait |
| 7 | unavailable | A 5xx, a network failure, a timeout, or a changed warehouse generation |
| 8 | not answered | A 200 whose body says unbuilt or not applicable |
| 9 | partial | `--all` printed some pages and then stopped |
| 10 | removed | A generated dataset or object command got a 404 for its route (not for a missing record): the server no longer has it, so upgrade the CLI |

## Configuration

The config directory is `$PHARMA_CONFIG_DIR`, else `$XDG_CONFIG_HOME/pharma`, else `~/.config/pharma`. It holds `config.yml` and, where no keychain is available, `credentials.json`.

- `PHARMA_PROFILE`: the profile to use
- `PHARMA_CONFIG_DIR`: the config directory
- `PHARMA_CREDENTIAL_STORE=file`: keep the refresh token in `credentials.json`

New API endpoints arrive in the commands automatically: they are generated from the API document.

- [REST API reference](https://pharmaobservatory.com/api-reference)
- [Integration guide](https://pharmaobservatory.com/app/integrate)
- [AI agents and the MCP server](https://pharmaobservatory.com/agents)
