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.