-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
arne-aignx marked this conversation as resolved.
|
||
|
|
||
| > ⚠️ **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`. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| .. include:: ../partials/get_started_api.md | ||
| :parser: myst_parser.sphinx_ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.