REST with curl

Every route is under https://ptarmigan.app/api/v1 and takes an API key from the API keys page as a bearer token. Requests and responses are JSON; money is a decimal string in dollars. The full list of routes is in the API reference. The examples use jq to pick fields out of responses.

Keep the key in an environment variable, as below, rather than in a script you might share; if it may have leaked, revoke it on the API keys page. Set a descriptive User-Agent as well; Cloudflare can block the default signature of some HTTP clients before a request reaches the API.

export PTARMIGAN_API_KEY=...        # from the API keys page
BASE=https://ptarmigan.app
AUTH="Authorization: Bearer $PTARMIGAN_API_KEY"
UA="acme-screen-curl/1.0"

curl -s -A "$UA" -H "$AUTH" "$BASE/api/v1/balance"

Screen a library

List endpoints return { "data": [...], "next_after": ... }. Set limit for the page size. To read every result, repeat the request with after set to next_after, keeping the same filters, until next_after is null. Timestamps are ISO 8601 in UTC, written with their offset (+00:00).

1. Find a target by gene, name or UniProt accession:

curl -s -A "$UA" -H "$AUTH" "$BASE/api/v1/proteins?search=EGFR" | jq '.data[0]'
PROTEIN_ID=$(curl -s -A "$UA" -H "$AUTH" "$BASE/api/v1/proteins?search=EGFR" | jq -r '.data[0].id')

2. Pick a library whose status is ready:

curl -s -A "$UA" -H "$AUTH" "$BASE/api/v1/compound-collections" \
  | jq '.data[] | select(.status == "ready") | {id, name, size}'
LIBRARY_ID=...                      # an id from the list

3. Estimate the cost. sufficient says whether your balance covers it:

BODY="{\"protein_id\": \"$PROTEIN_ID\", \"collection_id\": \"$LIBRARY_ID\"}"
curl -s -A "$UA" -H "$AUTH" -H "Content-Type: application/json" \
  -d "$BODY" "$BASE/api/v1/screens/estimate"

4. Queue the screen with an Idempotency-Key. If the request fails or times out, send it again with the same key: you get the screen the first request queued, not a second charge.

KEY=$(uuidgen)
SCREEN_ID=$(curl -s -A "$UA" -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" -d "$BODY" "$BASE/api/v1/screens" | jq -r .id)

A new screen answers 202; a retry with a used key answers 200 with Idempotent-Replayed: true. Without the header, every request is a new, charged screen.

5. Wait until status is completed, failed (not charged) or cancelled:

while :; do
  STATUS=$(curl -s -A "$UA" -H "$AUTH" "$BASE/api/v1/screens/$SCREEN_ID" | jq -r .status)
  echo "$STATUS"
  case "$STATUS" in queued|running) sleep 10 ;; *) break ;; esac
done

A queued screen can be cancelled with curl -s -A "$UA" -X POST -H "$AUTH" "$BASE/api/v1/screens/$SCREEN_ID/cancel".

6. Download the full ranking. The route returns a short-lived link (it expires after 60 minutes; ask again for a new one), and the link itself needs no key:

URL=$(curl -s -A "$UA" -H "$AUTH" "$BASE/api/v1/screens/$SCREEN_ID/download?format=parquet" | jq -r .url)
curl -s -o hits.parquet "$URL"

Use format=csv for a CSV. Both have one row per compound: rank, compound_id, name, smiles, input_smiles, engagement, score, predicted_residue, predicted_residue_letter. smiles is the standardized form that was scored and input_smiles the original (see Standardization; screens run before it have no input_smiles column). See Reading the results.

The top of a ranking

To read the top without downloading, page through /hits, up to 1,000 rows at a time. Pass the response's next_after as after for the next page:

curl -s -A "$UA" -H "$AUTH" "$BASE/api/v1/screens/$SCREEN_ID/hits?limit=25" \
  | jq '.data[] | {rank, name, engagement}'

For several screens at once, POST /api/v1/screens/downloads with {"screen_ids": [...]} returns one link per screen.

One compound

curl -s -A "$UA" -H "$AUTH" \
  "$BASE/api/v1/screens/$SCREEN_ID/compounds/COMPOUND_ID?include_residues=true"

returns the compound's ranking fields, the target's coverage and, with include_residues=true, one engagement value per residue (residue_engagement). Reference proteome predictions are in the web app only.

Upload a library

Send the file as multipart form data, with an optional name:

curl -s -A "$UA" -H "$AUTH" -F "[email protected]" -F "name=My compounds" \
  "$BASE/api/v1/compound-collections"

The response is the new library with status embedding. Poll GET /api/v1/compound-collections/{collection_id} until it is ready; see Library files for the format and limits. Delete an upload with curl -s -A "$UA" -X DELETE -H "$AUTH" "$BASE/api/v1/compound-collections/$LIBRARY_ID".

When something goes wrong

Errors from the API are JSON with a code and a message; branch on code. A 503 is temporary: wait the Retry-After header's seconds (or a minute without one) and retry. So is a 502 or 504, which may come from a proxy between you and the API as an HTML page rather than JSON. The full list is on Errors. A Cloudflare 403 with error code: 1010 is a browser signature block before the API; check your User-Agent rather than retrying the same request unchanged.