Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 5
docs: add "Get started with the API" developer guide#713
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,285 @@ | ||
| # Get started with the API | ||
| The Aignostics Platform API is a REST API over HTTPS, rooted at `https://platform.aignostics.com/api/v1`. Call it directly when you integrate the platform into another language or into an existing pipeline. | ||
| This guide covers one full workflow with plain HTTP calls — authenticate, analyze slides with [Atlas H&E-TME](https://www.aignostics.com/products/he-tme-profiling-product), follow progress, download results. Examples use `curl` and `jq`. The complete contract is in the [API reference](https://aignostics.readthedocs.io/en/latest/api_reference_v1.html). | ||
| ```{include} ../partials/_get_started_signup.md | ||
| ``` | ||
| ## Authenticate | ||
| Authentication is tied to a person, not to a machine: every call acts as a user in an organization, and each analysis records a `submitted_by`. There is no anonymous access and no organization-wide API key. You need two things: | ||
| - **A platform account**, created by invitation from your organization's administrator (the section above). If your organization is not on the platform yet, talk to `support@aignostics.com`. | ||
| - **A client ID**, the public identifier of your integration. Ask `support@aignostics.com`; you cannot mint one yourself. There is no matching client *secret*, because a program running on a user's machine cannot keep one safe. | ||
| ### How it works | ||
| The API never sees your password. It accepts a short-lived **access token** — issued by Auth0, the identity service behind the platform — which every call carries in an `Authorization: Bearer …` header. You log in once in a browser; from then on your program renews tokens itself with the long-lived **refresh token** it got alongside the first one. When a call returns `401`, renew and retry. | ||
| This is the standard OAuth 2.0 Device Authorization Grant ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)), so most languages have a library for the three steps below — you supply the endpoints and client ID. | ||
| ### Step 1: start the login | ||
| ```shell | ||
| CLIENT_ID=your-client-id | ||
| curl -s -X POST https://aignostics-platform.eu.auth0.com/oauth/device/code \ | ||
| -d client_id="$CLIENT_ID" \ | ||
| -d scope=offline_access \ | ||
| -d audience=https://aignostics-platform-samia | jq . | ||
| ``` | ||
| - `client_id` — your integration's public identifier. | ||
| - `audience` — which API the token should be valid for. `https://aignostics-platform-samia` is the Aignostics Platform. | ||
| - `scope=offline_access` — "also give me a refresh token". Leave it out and you get an access token that you cannot renew. | ||
| The response carries `verification_uri_complete` (the link for you), `user_code` (the code to compare), `device_code` (your program's secret handle), and `interval` (seconds between polls). | ||
| ### Step 2: approve it, and collect the tokens | ||
| Open `verification_uri_complete` in a browser, log in, and check the code shown matches the `user_code` your program printed — that comparison is what stops someone else's program from being approved with your account. Meanwhile, poll for the tokens every `interval` seconds while the response says `error: authorization_pending` (or `slow_down`, meaning you are asking too often): | ||
| ```shell | ||
| curl -s -X POST https://aignostics-platform.eu.auth0.com/oauth/token \ | ||
| -d grant_type=urn:ietf:params:oauth:grant-type:device_code \ | ||
| -d device_code="$DEVICE_CODE" \ | ||
| -d client_id="$CLIENT_ID" | jq . | ||
| ``` | ||
| Once you approve, the same call returns `access_token` and `refresh_token`. Store the refresh token as a secret — it is what makes the next step possible — and never log or commit either token. | ||
| ### Step 3: renew without a browser | ||
| This is what CI and long-running services do whenever a call returns `401`: | ||
| ```shell | ||
| curl -s -X POST https://aignostics-platform.eu.auth0.com/oauth/token \ | ||
| -d grant_type=refresh_token \ | ||
| -d client_id="$CLIENT_ID" \ | ||
| -d refresh_token="$REFRESH_TOKEN" | jq -r .access_token | ||
| ``` | ||
| Because the refresh token belongs to the person who logged in, an unattended service acts as that user — and stops working if that account does. If you need a true machine identity, ask support; these flows are what the API supports today. | ||
| ### Check that it worked | ||
| ```shell | ||
| export TOKEN=your-access-token | ||
| export API=https://platform.aignostics.com/api/v1 | ||
| curl -s "$API/me" -H "Authorization: Bearer $TOKEN" | jq . | ||
| ``` | ||
| `GET /v1/me` returns your user and your organization — including `aignostics_bucket_name`, the bucket used below. | ||
| ### Hello world, end to end | ||
| All of the above in one script, with your client ID as the only input. It needs `curl` and `jq`. | ||
| ```shell | ||
| #!/usr/bin/env bash | ||
| # hello_aignostics.sh — log in, then confirm the API answers as you. | ||
| # Usage: ./hello_aignostics.sh <client-id> | ||
| set -euo pipefail | ||
| CLIENT_ID="${1:?usage: $0 <client-id>}" | ||
| AUTH0="https://aignostics-platform.eu.auth0.com" | ||
| AUDIENCE="https://aignostics-platform-samia" | ||
| API="https://platform.aignostics.com/api/v1" | ||
| # 1. Ask Auth0 to start a login. | ||
| device=$(curl -sS -X POST "$AUTH0/oauth/device/code" \ | ||
| -d client_id="$CLIENT_ID" \ | ||
| -d scope=offline_access \ | ||
| -d audience="$AUDIENCE") | ||
| device_code=$(jq -r .device_code <<<"$device") | ||
| interval=$(jq -r .interval <<<"$device") | ||
| echo "Open this link: $(jq -r .verification_uri_complete <<<"$device")" | ||
| echo "Confirm it shows code: $(jq -r .user_code <<<"$device")" | ||
| # 2. Poll until you approve it in the browser. | ||
| while :; do | ||
| sleep "$interval" | ||
| tokens=$(curl -sS -X POST "$AUTH0/oauth/token" \ | ||
| -d grant_type=urn:ietf:params:oauth:grant-type:device_code \ | ||
| -d device_code="$device_code" \ | ||
| -d client_id="$CLIENT_ID") | ||
| case "$(jq -r '.error // "ok"' <<<"$tokens")" in | ||
| ok) break ;; | ||
| authorization_pending) ;; # not approved yet — keep waiting | ||
| slow_down) interval=$((interval + 5)) ;; # polling too fast — back off | ||
| *) jq -r '"login failed: " + (.error_description // .error)' <<<"$tokens" >&2; exit 1 ;; | ||
| esac | ||
| done | ||
| ACCESS_TOKEN=$(jq -r .access_token <<<"$tokens") | ||
| REFRESH_TOKEN=$(jq -r .refresh_token <<<"$tokens") | ||
| echo "Got an access token, and a refresh token to store as a secret (${#REFRESH_TOKEN} chars)." | ||
| # 3. Confirm the API answers as you. | ||
| curl -sS "$API/me" -H "Authorization: Bearer $ACCESS_TOKEN" \ | ||
| | jq '{user: .user.email, organization: .organization.display_name, bucket: .organization.aignostics_bucket_name}' | ||
| ``` | ||
| ```text | ||
| Open this link: https://aignostics-platform.eu.auth0.com/activate?user_code=ABCD-EFGH | ||
| Confirm it shows code: ABCD-EFGH | ||
| Got an access token, and a refresh token to store as a secret (64 chars). | ||
| { | ||
| "user": "you@your-organization.example", | ||
| "organization": "Your Organization", | ||
| "bucket": "your-aignostics-bucket" | ||
| } | ||
| ``` | ||
| Keep the refresh token in your secret manager and later runs skip the browser entirely — Step 3 is the whole renewal. | ||
| ## Find out what the application expects | ||
| Two calls: one to see which applications your organization can run, one to read the contract of the version you intend to use. | ||
| ```shell | ||
| curl -s "$API/applications" -H "Authorization: Bearer $TOKEN" | jq . | ||
| curl -s "$API/applications/he-tme/versions/1.3.0" -H "Authorization: Bearer $TOKEN" | jq . | ||
| ``` | ||
| The version response tells you exactly what to send in the next step: | ||
| - `input_artifacts[].name` — the name to give the file you submit for each slide (`input_slide` for Atlas H&E-TME). | ||
| - `input_artifacts[].metadata_schema` — a JSON Schema for the per-slide `metadata`. Validate against it locally instead of guessing; it is versioned with the application, so it is the source of truth for required fields. | ||
| - `output_artifacts[]` — the result files a successful slide produces, with their MIME types. | ||
| ## Give the platform access to your slides | ||
| The platform fetches each slide from a URL you provide, so that URL has to work without your credentials and keep working while the analysis is queued. | ||
| **The preferred method is to store the slide in S3-compliant object storage** — AWS S3, Google Cloud Storage, anything speaking the S3 API — **and mint a signed URL for it**: a link with a temporary key in it, granting read access to that one object for a limited time. **Give it at least seven days**, so it outlives any queueing. Seven days is also the longest a SigV4 signature can live, so that is the number to use. | ||
| **For convenience we provide that storage.** Every organization gets a bucket, plus credentials to upload objects into it and sign download URLs from it. `GET /v1/me` returns all four under `organization`: | ||
| | Field | What it is | | ||
| | --- | --- | | ||
| | `aignostics_bucket_name` | your organization's bucket | | ||
| | `aignostics_bucket_protocol` | the storage backend — `gs`, Google Cloud Storage | | ||
| | `aignostics_bucket_hmac_access_key_id` | access key ID | | ||
| | `aignostics_bucket_hmac_secret_access_key` | secret access key | | ||
| The key pair is an ordinary S3 credential: point any S3 client at the provider's S3-compatible endpoint — `https://storage.googleapis.com` for `gs` — and sign with SigV4. | ||
| ```shell | ||
| export AWS_ACCESS_KEY_ID=your-aignostics-bucket-hmac-access-key-id | ||
| export AWS_SECRET_ACCESS_KEY=your-aignostics-bucket-hmac-secret-access-key | ||
| export BUCKET=your-aignostics-bucket-name | ||
| export GCS=https://storage.googleapis.com | ||
| # upload the slide | ||
| aws s3 --endpoint-url "$GCS" cp slide1.tiff "s3://$BUCKET/slide1.tiff" | ||
| # mint the signed URL to hand to the platform (7 days) | ||
| aws s3 --endpoint-url "$GCS" presign "s3://$BUCKET/slide1.tiff" --expires-in 604800 | ||
| ``` | ||
| Treat the secret like any other credential: it grants access to your organization's slides. | ||
| ## Analyze your slides with Atlas H&E-TME | ||
Collaborator There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd state here explicitly, that this example may get out of date, when newer applications versions are released or different applications are used. CollaboratorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 5bb151b — the submit section now opens with a callout:
I referred back by section name rather than an anchor link, because Posted by Claude claude-opus-5 via Claude Code on behalf of Omid Kokabi CollaboratorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small correction to my reply above: the callout was tightened in 6e374c2 during an editing pass over the whole guide, so the final wording is
Same substance, fewer words. Posted by Claude claude-opus-5 via Claude Code on behalf of Omid Kokabi | ||
| > ⚠️ **This example is specific to Atlas H&E-TME `1.3.0`.** Artifact names, metadata, and outputs differ per application and change between versions, so read the version's own contract — *Find out what the application expects*, above — instead of copying this payload. | ||
| One `POST` describes the whole analysis: which application, which version, and one entry per slide. The API calls those entries **items**, and the files attached to them **artifacts** — here a single input artifact, your slide. Give each item your own `external_id` so you can match results back to your records. Omit `version_number` for the latest version, or pin it as below so a repeat analysis behaves identically. | ||
| ```shell | ||
| curl -s -X POST "$API/runs" \ | ||
| -H "Authorization: Bearer $TOKEN" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{ | ||
| "application_id": "he-tme", | ||
| "version_number": "1.3.0", | ||
| "items": [ | ||
| { | ||
| "external_id": "slide_1", | ||
| "input_artifacts": [ | ||
| { | ||
| "name": "input_slide", | ||
| "download_url": "https://example-bucket.storage.example.com/slide1.tiff?signature=...", | ||
| "metadata": { | ||
| "checksum_base64_crc32c": "64RKKA==", | ||
| "media-type": "image/tiff", | ||
| "width_px": 136223, | ||
| "height_px": 87761, | ||
| "resolution_mpp": 0.2628238, | ||
| "staining_method": "H&E", | ||
| "specimen": {"tissue": "LUNG", "disease": "LUNG_CANCER"} | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| }' | jq . | ||
| ``` | ||
| A `201` returns `{"run_id": "..."}` — the handle you follow the analysis with, so keep it. `custom_metadata` and `scheduling` are optional; the [API reference](https://aignostics.readthedocs.io/en/latest/api_reference_v1.html) lists what your API version accepts. | ||
| A `422` means nothing ran and `detail` names the offending field — usually metadata that fails `metadata_schema`, a download URL the platform cannot fetch, or two slides sharing an `external_id`. | ||
| ## Follow the analysis | ||
| Ask about the analysis as a whole: | ||
| ```shell | ||
| RUN_ID=your-run-id | ||
| curl -s "$API/runs/$RUN_ID" -H "Authorization: Bearer $TOKEN" | jq '{state, termination_reason, statistics}' | ||
| ``` | ||
| `state` moves `PENDING` → `PROCESSING` → `TERMINATED`. Read it together with the next field, because **`TERMINATED` does not mean "succeeded"** — only that the analysis is over: | ||
| - `termination_reason` says why it ended: `ALL_ITEMS_PROCESSED`, `CANCELED_BY_USER`, or `CANCELED_BY_SYSTEM`. | ||
| - `statistics` counts slides per outcome (`item_succeeded_count`, `item_user_error_count`, `item_system_error_count`, `item_skipped_count`, …). An analysis can reach `ALL_ITEMS_PROCESSED` with failed slides in it, so check here. | ||
| Or ask about individual slides — `items` — which finish independently: | ||
| ```shell | ||
| curl -s "$API/runs/$RUN_ID/items?state=TERMINATED" -H "Authorization: Bearer $TOKEN" \ | ||
| | jq '.[] | {external_id, termination_reason, output_artifacts}' | ||
| ``` | ||
| Per slide, `termination_reason` is `SUCCEEDED`, `USER_ERROR` (your input — bad file, wrong metadata), `SYSTEM_ERROR` (ours; `error_code` and `error_message` say more), or `SKIPPED`. Every 30 seconds is a plenty frequent poll for analyses taking minutes to hours. | ||
| ## Download results | ||
| Every succeeded slide lists its result files under `output_artifacts`, each with a `download_url` you can fetch directly. Those URLs expire; if one has gone stale, ask for a fresh one: | ||
| ```shell | ||
| curl -s "$API/runs/$RUN_ID/artifacts/$ARTIFACT_ID/file" -H "Authorization: Bearer $TOKEN" | ||
| ``` | ||
| Since slides finish one by one, the efficient pattern is a loop: poll `/items`, download whatever is newly `SUCCEEDED`, and track what you already have. | ||
| > ⚠️ **Results are kept for 30 days** from the day you started the analysis. After that, re-analyzing the slides is the only way to get them back. | ||
| ## List, cancel, or clean up | ||
| ```shell | ||
| # list your analyses, filtered and paginated | ||
| curl -s "$API/runs?application_id=he-tme&page=1&page_size=20" -H "Authorization: Bearer $TOKEN" | jq . | ||
| # cancel an analysis that is still pending or processing | ||
| curl -s -X POST "$API/runs/$RUN_ID/cancel" -H "Authorization: Bearer $TOKEN" | ||
| # delete an analysis' results before the retention window ends | ||
| curl -s -X DELETE "$API/runs/$RUN_ID/artifacts" -H "Authorization: Bearer $TOKEN" | ||
| ``` | ||
| `GET /v1/runs` also filters by `application_version`, `external_id`, `custom_metadata`, and `for_organization`, and sorts via `sort`. To attach your own metadata after the fact, `PUT /v1/runs/{run_id}/custom-metadata` and `PUT /v1/runs/{run_id}/items/{external_id}/custom-metadata` expect the `custom_metadata_checksum` from your last read, so concurrent updates cannot silently overwrite each other. | ||
| ## Conventions worth knowing | ||
| - **Retries.** Retry `5xx`, timeouts, and connection errors with exponential backoff and jitter; never `4xx`, which fails again. Four attempts backing off from 0.1 s to a 60 s cap is a sane default. | ||
| - **Idempotency.** `POST /v1/runs` is not idempotent — calling it twice analyzes your slides twice. Record the returned `run_id` before retrying, and match `external_id` values via `GET /v1/runs` to spot an analysis you already submitted. | ||
| - **Caching.** Application and version metadata barely changes; the state of a running analysis changes constantly. Caching the former for a few minutes and the latter for seconds at most is a reasonable starting point. | ||
| - **Status.** Live platform status is at [status.platform.aignostics.com](https://status.platform.aignostics.com). | ||
| Questions about the API, or something behaving differently from this guide? Email `support@aignostics.com`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| .. include:: ../partials/get_started_api.md | ||
| :parser: myst_parser.sphinx_ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -16,6 +16,7 @@ | ||
| get_started_launchpad | ||
| get_started_cli | ||
| get_started_library | ||
| get_started_api | ||
| get_started_mcp | ||
| .. toctree:: | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's be explicit in this chapter.
The preferred method to provide this URL, is to store the whole slide image in a S3-compliant storage (e.g. AWS S3, Google Cloud Storage) and mint a signed URL with an expiry of at least seven days.. yada yada..
For convenience we provide such storage and the access key id and secret access key to upload objects and sign download URLs for each individual organization. yada yada
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 5bb151b — rewrote the chapter to say it outright.
It now leads with the preferred method: store the whole slide image in S3-compliant object storage (AWS S3, GCS, anything speaking the S3 API) and mint a signed URL for it, with an expiry of at least seven days so the link outlives queueing. I added that seven days is also the SigV4 maximum, so it is effectively the number rather than a floor to exceed.
Then, explicitly as convenience: every organization gets a bucket from us plus the credentials to upload into it and sign download URLs from it. The four
GET /v1/mefields are now a table (aignostics_bucket_name,aignostics_bucket_protocol,aignostics_bucket_hmac_access_key_id,aignostics_bucket_hmac_secret_access_key), followed by a copy-pasteable upload-and-presign example againsthttps://storage.googleapis.comwith--expires-in 604800.Field names and the
gsprotocol value verified againstbucket/_service.pyand the OpenAPIOrganizationReadResponseschema.Posted by Claude claude-opus-5 via Claude Code on behalf of Omid Kokabi
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: 6e374c2 tightened the prose in this chapter as part of an editing pass over the guide. The substance you asked for is unchanged — preferred method stated outright, the seven-day expiry, the four
GET /v1/mefields as a table, and the upload-and-presign example — just with less padding around it.Posted by Claude claude-opus-5 via Claude Code on behalf of Omid Kokabi