diff --git a/README.md b/README.md index 094718e23..197cb5793 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,15 @@ Choose your preferred interface for working with the Aignostics Platform. Each i | **Use when** | Building custom analysis pipeline in Python for repeated usage and processing large datasets (10s-1000s of slides) | | **Get started** | Get started with the Python Library | +### 🔌 REST API (Direct HTTP) + +| | | +|---|---| +| **What it is** | The Platform's REST API at `https://platform.aignostics.com/api/v1`, called directly over HTTPS | +| **Best for** | Developers integrating from another language or an existing pipeline, without depending on the Python SDK | +| **Use when** | Building a service or workflow outside Python, or generating your own client from the OpenAPI document | +| **Get started** | Get started with the API | + ### 🤖 MCP Server (AI Agent Integration) | | | diff --git a/docs/partials/README_main.md b/docs/partials/README_main.md index 3d594e853..b4d3d402c 100644 --- a/docs/partials/README_main.md +++ b/docs/partials/README_main.md @@ -33,6 +33,15 @@ Choose your preferred interface for working with the Aignostics Platform. Each i | **Use when** | Building custom analysis pipeline in Python for repeated usage and processing large datasets (10s-1000s of slides) | | **Get started** | Get started with the Python Library | +### 🔌 REST API (Direct HTTP) + +| | | +|---|---| +| **What it is** | The Platform's REST API at `https://platform.aignostics.com/api/v1`, called directly over HTTPS | +| **Best for** | Developers integrating from another language or an existing pipeline, without depending on the Python SDK | +| **Use when** | Building a service or workflow outside Python, or generating your own client from the OpenAPI document | +| **Get started** | Get started with the API | + ### 🤖 MCP Server (AI Agent Integration) | | | diff --git a/docs/partials/get_started_api.md b/docs/partials/get_started_api.md new file mode 100644 index 000000000..3183c9da5 --- /dev/null +++ b/docs/partials/get_started_api.md @@ -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 +set -euo pipefail + +CLIENT_ID="${1:?usage: $0 }" +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 + +> ⚠️ **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`. diff --git a/docs/source/get_started_api.rst b/docs/source/get_started_api.rst new file mode 100644 index 000000000..16bb49768 --- /dev/null +++ b/docs/source/get_started_api.rst @@ -0,0 +1,2 @@ +.. include:: ../partials/get_started_api.md + :parser: myst_parser.sphinx_ diff --git a/docs/source/index.rst b/docs/source/index.rst index b12c53ab1..7e099487e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,6 +16,7 @@ get_started_launchpad get_started_cli get_started_library + get_started_api get_started_mcp .. toctree::