Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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** | <a href="https://aignostics.readthedocs.io/en/latest/get_started_library.html">Get started with the Python Library</a> |

### 🔌 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** | <a href="https://aignostics.readthedocs.io/en/latest/get_started_api.html">Get started with the API</a> |

### 🤖 MCP Server (AI Agent Integration)

| | |
Expand Down
9 changes: 9 additions & 0 deletions docs/partials/README_main.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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** | <a href="https://aignostics.readthedocs.io/en/latest/get_started_library.html">Get started with the Python Library</a> |

### 🔌 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** | <a href="https://aignostics.readthedocs.io/en/latest/get_started_api.html">Get started with the API</a> |

### 🤖 MCP Server (AI Agent Integration)

| | |
Expand Down
285 changes: 285 additions & 0 deletions docs/partials/get_started_api.md
Original file line numberDiff line numberDiff 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

Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
CollaboratorAuthor

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/me fields 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 against https://storage.googleapis.com with --expires-in 604800.

Field names and the gs protocol value verified against bucket/_service.py and the OpenAPI OrganizationReadResponse schema.


Posted by Claude claude-opus-5 via Claude Code on behalf of Omid Kokabi

Copy link
Copy Markdown
CollaboratorAuthor

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/me fields 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


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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.
Refer back to ## Find out what the application expects

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5bb151b — the submit section now opens with a callout:

⚠️This example is specific to Atlas H&E-TME 1.3.0. Artifact names, required metadata, and outputs differ from one application to the next, and can change when a new version of the same application is released. Read the version's own contract first — the Find out what the application expects section above — rather than copying this payload verbatim.

I referred back by section name rather than an anchor link, because myst_heading_anchors is not configured in docs/source/conf.py, so an in-page #find-out-what-the-application-expects link would render dead in the Sphinx build (and myst.xref_missing is suppressed, so it would fail silently).


Posted by Claude claude-opus-5 via Claude Code on behalf of Omid Kokabi

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The 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

⚠️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.

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`.
2 changes: 2 additions & 0 deletions docs/source/get_started_api.rst
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
.. include:: ../partials/get_started_api.md
:parser: myst_parser.sphinx_
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,6 +16,7 @@
get_started_launchpad
get_started_cli
get_started_library
get_started_api
get_started_mcp

.. toctree::
Expand Down
Loading