references/api-endpoints.md
# Adaptyv Bio Foundry API — Complete Endpoint Reference
Base URL: `https://foundry-api-public.adaptyvbio.com/api/v1`
OpenAPI spec: `GET /openapi.json`
## Table of Contents
- [Experiments](#experiments)
- [Sequences](#sequences)
- [Results](#results)
- [Targets](#targets)
- [Quotes](#quotes)
- [Tokens](#tokens)
- [Updates](#updates)
- [Feedback](#feedback)
---
## Experiments
### POST /experiments — Create experiment
Creates a new experiment. Starts in `Draft` status by default.
**Request body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Human-readable name |
| `experiment_spec` | ExperimentSpec | Yes | Experiment definition (see below) |
| `skip_draft` | boolean | No (default false) | Bypass Draft, go straight to WaitingForConfirmation |
| `auto_accept_quote` | boolean | No (default false) | Auto-accept quote and create invoice |
| `webhook_url` | string/null | No | URL for status-change POST notifications |
**ExperimentSpec:**
| Field | Type | Required | Description |
|---|---|---|---|
| `experiment_type` | string | Yes | `affinity`, `screening`, `thermostability`, `fluorescence`, or `expression` |
| `method` | string | Required for binding types | `bli` or `spr` |
| `target_id` | uuid | Required for binding types | Target UUID from catalog |
| `sequences` | object | Yes | Map of name → amino acid string or rich object |
| `n_replicates` | integer | Recommended (default 3) | Technical replicates (min 1) |
| `antigen_concentrations` | number[] | No (affinity only) | Defaults to `[1000.0, 316.2, 100.0, 31.6, 0.0]` nM |
| `parameters` | object | No | Experiment-specific settings |
**Field requirements by experiment type:**
| Field | Affinity | Screening | Thermostability | Fluorescence | Expression |
|---|---|---|---|---|---|
| `experiment_type` | required | required | required | required | required |
| `method` | required | required | — | — | — |
| `target_id` | required | required | — | — | — |
| `sequences` | required | required | required | required | required |
| `n_replicates` | recommended | recommended | optional | optional | optional |
| `antigen_concentrations` | optional | — | — | — | — |
**Response (201):**
| Field | Type | Description |
|---|---|---|
| `experiment_id` | string | UUID of new experiment |
| `error` | string/null | Error message if validation fails |
| `stripe_hosted_invoice_url` | string/null | Present when `auto_accept_quote` created an invoice |
| `stripe_invoice_id` | string/null | Stripe invoice ID |
**Status codes:** 201, 400, 401, 403, 404
---
### GET /experiments — List experiments
Lists experiments accessible to caller, sorted by creation date (newest first).
**Query params:** `limit`, `offset`, `filter`, `search`, `sort`
**Response item:**
| Field | Type | Description |
|---|---|---|
| `id` | uuid | Unique identifier |
| `code` | string | e.g., "EXP-2024-001" |
| `name` | string/null | Human-readable name |
| `status` | ExperimentStatus | Current lifecycle status |
| `experiment_type` | ExperimentType | affinity/screening/thermostability/fluorescence/expression |
| `results_status` | ResultsStatus | none/partial/all |
| `created_at` | datetime | ISO 8601 |
| `experiment_url` | string | URL to Foundry portal |
| `stripe_invoice_url` | string/null | Invoice URL |
| `stripe_quote_url` | string/null | Quote URL |
**Status codes:** 200, 401
---
### GET /experiments/{experiment_id} — Get experiment
Returns full metadata for a single experiment.
**Path param:** `experiment_id` (uuid)
**Response:**
| Field | Type | Description |
|---|---|---|
| `id` | uuid | Unique identifier |
| `code` | string | Experiment code |
| `status` | ExperimentStatus | Current status |
| `experiment_spec` | ExperimentSpec | Full experiment definition |
| `results_status` | ResultsStatus | none/partial/all |
| `created_at` | datetime | ISO 8601 |
| `experiment_url` | string | Portal URL |
| `costs` | object | Cost breakdown |
**Status codes:** 200, 401, 404, 500
---
### PATCH /experiments/{experiment_id} — Update experiment
Modify an existing experiment. Draft experiments allow full edits; after quote generation, only `name`, `description`, and `webhook_url` are editable.
**Path param:** `experiment_id` (uuid)
**Request body:** All fields optional — only provided fields are updated.
**Status codes:** 200, 400, 401, 404, 409
---
### POST /experiments/{experiment_id}/submit — Submit experiment
Submits a draft experiment for review. Advances from `Draft` to `WaitingForConfirmation`.
**Path param:** `experiment_id` (uuid)
**Response:**
| Field | Type | Description |
|---|---|---|
| `experiment_id` | string | Experiment UUID |
**Status codes:** 200, 401, 403, 404, 409, 500
---
### POST /experiments/cost-estimate — Estimate cost
Calculates cost without creating an experiment.
**Request body:**
```json
{
"experiment_spec": {
"experiment_type": "screening",
"method": "bli",
"target_id": "...",
"sequences": {"seq1": "MKTL..."},
"n_replicates": 3
}
}
```
**Response:**
| Field | Type | Description |
|---|---|---|
| `pricing_version` | string | e.g., "v1_2026-01-20" |
| `assay` | object | Per-type costs with base and replicate pricing |
| `materials` | object | Target material costs (binding experiments) |
| `total_cents` | integer | Sum in USD cents |
All prices exclude VAT; taxes calculated at invoicing. Targets without self-service pricing return incomplete estimates.
**Status codes:** 200, 400, 401
---
### GET /experiments/{experiment_id}/quote — Get quote
Returns quote metadata (totals, currency, status, expiration).
**Path param:** `experiment_id` (uuid)
**Response:**
| Field | Type | Description |
|---|---|---|
| `experiment_id` | string | Experiment UUID |
| `stripe_quote_url` | string | Stripe quote URL |
| `amount_total` | int64 | Total in smallest currency unit |
| `amount_subtotal` | int64 | Subtotal |
| `currency` | string | ISO currency code (e.g., "usd") |
| `status` | string | Quote status |
| `expires_at` | datetime/null | Expiration time |
**Status codes:** 200, 401, 403, 404, 500
---
### GET /experiments/{experiment_id}/quote/pdf — Get quote PDF
Returns the quote as a PDF file (`application/pdf`).
**Path param:** `experiment_id` (uuid)
**Status codes:** 200, 401, 403, 404, 500
---
### POST /experiments/{experiment_id}/quote/confirm — Accept quote (by experiment)
Accepts Stripe quote, creates draft invoice, transitions to `WaitingForMaterials`.
**Path param:** `experiment_id` (uuid)
**Request body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `purchase_order_number` | string/null | No | PO number for your records |
| `notes` | string/null | No | Reserved |
**Response:**
| Field | Type | Description |
|---|---|---|
| `id` | string | Quote ID |
| `status` | StripeQuoteStatus | New status |
| `hosted_invoice_url` | string/null | Stripe payment URL |
| `invoice_id` | string/null | Generated invoice ID |
**Status codes:** 200, 401, 403, 404, 409
---
### GET /experiments/{experiment_id}/invoice — Get invoice
Returns invoice metadata including hosted payment URL.
**Path param:** `experiment_id` (uuid)
**Status codes:** 200, 401, 403, 404, 500
---
### GET /experiments/{experiment_id}/results — List results for experiment
Returns all analysis results for a specific experiment.
**Path param:** `experiment_id` (uuid)
**Query params:** `limit`, `offset`, `filter`, `sort`
**Status codes:** 200, 400, 401, 403, 404
---
### GET /experiments/{experiment_id}/sequences — List sequences for experiment
Returns all sequences for a specific experiment, sorted newest first.
**Path param:** `experiment_id` (uuid)
**Query params:** `limit`, `offset`, `search`, `sort`
**Status codes:** 200, 400, 401, 403, 404
---
### GET /experiments/{experiment_id}/updates — List experiment updates
Returns updates for one experiment, oldest first. Types: `status_change`, `progress`, `error`.
**Path param:** `experiment_id` (uuid)
**Query params:** `limit`, `offset`, `filter`, `sort`
Filter example: `filter=eq(type,status_change)`
---
## Sequences
### GET /sequences — List sequences
Returns sequences from all experiments, sorted newest first.
**Query params:** `limit`, `offset`, `search`, `sort`, `experiment_id` (filter by experiment UUID)
**Response item:**
| Field | Type | Description |
|---|---|---|
| `id` | uuid | Unique identifier |
| `name` | string/null | Optional name |
| `aa_preview` | string/null | Truncated preview (first 50 chars) |
| `length` | int32 | Sequence length in amino acids |
| `experiment_id` | uuid | Parent experiment |
| `experiment_code` | string | Human-readable experiment code |
| `is_control` | boolean | Whether this is a control |
| `created_at` | datetime | Creation timestamp |
**Status codes:** 200, 401
---
### GET /sequences/{sequence_id} — Get sequence
Returns full details including complete amino acid string.
**Path param:** `sequence_id` (uuid)
**Response:**
| Field | Type | Description |
|---|---|---|
| `id` | uuid | Unique identifier |
| `aa_string` | string/null | Complete amino acid sequence |
| `length` | int32 | Length in amino acids |
| `is_control` | boolean | Control flag |
| `metadata` | object | Sequence-level annotations |
| `experiment` | object | Parent experiment reference |
| `created_at` | datetime | Creation timestamp |
**Status codes:** 200, 401, 403, 404, 500
---
### POST /sequences — Add sequences to experiment
Appends sequences to a **Draft** experiment identified by its human-readable code.
**Request body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `experiment_code` | string | Yes | e.g., "PROJ-001" |
| `sequences` | array | Yes | Array of sequence entries |
**Each sequence entry:**
| Field | Type | Required | Description |
|---|---|---|---|
| `aa_string` | string | Yes | Amino acid sequence |
| `name` | string | No | Human-readable name |
| `control` | boolean | No | Whether this is a control |
| `metadata` | object | No | Annotations |
**Response (201):**
| Field | Type | Description |
|---|---|---|
| `added_count` | int32 | Number of sequences added |
| `experiment_id` | string | Experiment UUID |
| `experiment_code` | string | Experiment code |
| `sequence_ids` | array | IDs of added sequences |
**Status codes:** 201, 400, 404, 409 (experiment not in Draft), 500
---
## Results
### GET /results — List results
Lists completed analysis results, sorted newest first. Results appear when `results_status` reaches `partial` or `all`.
**Query params:** `limit`, `offset`, `filter`, `search`, `sort`
**Response item:**
| Field | Type | Description |
|---|---|---|
| `id` | uuid | Result identifier |
| `title` | string | Human-readable title |
| `experiment_id` | uuid | Associated experiment |
| `result_type` | string | e.g., "affinity", "thermostability" |
| `summary` | array | Key results (type-specific, see below) |
| `metadata` | object | Extended metadata (e.g., instrument info) |
| `data_package_url` | string/null | Download URL for raw data package |
| `created_at` | datetime | When result was generated |
**AffinityResult summary fields:** `kd_mean`, `kd_std`, `kon_mean`, `kon_log_std`, `koff_mean`, `koff_std`, `replicates` (array with per-replicate `kd`, `kon`, `koff`, `binding_strength`, `kon_method`, `koff_method`, `replicate` index), `sequence`, `target_id`
**ThermostabilityResult summary fields:** Tm values and melting curves
**Status codes:** 200, 401
---
### GET /results/{result_id} — Get result
Returns detailed result data including full summary array.
**Path param:** `result_id` (uuid)
**Status codes:** 200, 401, 403, 404, 500
---
## Targets
### GET /targets — List targets
Lists validated antigens available for experiments.
**Query params:**
| Parameter | Type | Description |
|---|---|---|
| `limit` | int | Max items (1-100, default 50) |
| `offset` | int | Skip count |
| `search` | string | Free-text search on product name |
| `sort` | string | Sort expression |
| `selfservice_only` | boolean | Only targets with self-service pricing |
| `show_conjugated` | boolean | Include conjugated targets (default: unconjugated only) |
| `detailed` | boolean | Populate `details` block with enrichment data |
**Response item:**
| Field | Type | Description |
|---|---|---|
| `id` | uuid | Target UUID (use as `experiment_spec.target_id`) |
| `name` | string | Target name |
| `vendor_name` | string | Vendor name |
| `catalog_number` | string | Vendor catalog/SKU number |
| `url` | string | Target URL |
| `pricing` | object/null | Self-service pricing (null = custom quote required) |
| `details` | object/null | Enrichment data (gene names, structures, sequence, bioactivity) |
**Status codes:** 200, 401
---
### GET /targets/{target_id} — Get target
Returns catalog record for a single target.
**Path param:** `target_id` (uuid)
**Status codes:** 200, 400, 401, 403, 404, 500
---
### POST /targets/request-custom — Submit custom target request
Submit a new custom target for staff review. At least one of `sequence` or `pdb_id` must be provided.
**Request body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | Yes | Display name |
| `product_id` | string | Yes | Must be unique within organization |
| `sequence` | string/null | At least one | Amino acid sequence |
| `pdb_id` | string/null | At least one | PDB identifier |
| `pdb_file` | string/null | No | PDB file content |
| `molecular_weight` | number/null | No | Weight in kDa |
| `note` | string/null | No | Additional notes |
**Status codes:** 201, 400, 401, 403, 500
---
### GET /targets/request-custom — List custom target requests
Returns custom target requests for your organization, sorted newest first.
**Query params:** `limit`, `offset`, `filter`, `sort`
Filter example: `filter=eq(status,pending_review)`
---
### GET /targets/request-custom/{request_id} — Get custom target request
**Path param:** `request_id` (uuid)
**Response:**
| Field | Type | Description |
|---|---|---|
| `id` | uuid | Request identifier |
| `name` | string | Target name |
| `product_id` | string | Your product ID |
| `status` | string | e.g., "pending_review" |
| `material_id` | string/null | Linked catalog ID if approved |
| `molecular_weight` | number/null | Weight in kDa |
| `note` | string/null | User notes |
| `created_at` | datetime | Created |
| `updated_at` | datetime | Last updated |
**Status codes:** 200, 401, 403, 404, 500
---
## Quotes
### GET /quotes — List quotes
Returns all quotes for caller's organization.
**Query params:** `limit`, `offset`, `filter`, `sort`
**Response item:**
| Field | Type | Description |
|---|---|---|
| `id` | string | Quote identifier |
| `quote_number` | string | Human-readable quote number |
| `organization_id` | uuid | Organization |
| `amount_cents` | int | Amount in cents |
| `currency` | string | ISO 4217 code |
| `status` | StripeQuoteStatus | Quote status |
| `valid_until` | datetime | Expiration |
| `created_at` | datetime | Creation timestamp |
---
### GET /quotes/{quote_id} — Get quote
Returns full quote document with itemized pricing.
**Path param:** `quote_id` (string, e.g., "qt_1Abc2DefGhi")
**Response:**
| Field | Type | Description |
|---|---|---|
| `id` | string | Quote identifier |
| `quote_number` | string | Reference number |
| `organization_id` | uuid | Organization |
| `organization_name` | string | Organization name |
| `line_items` | array | Itemized pricing |
| `subtotal_cents` | int | Subtotal in cents |
| `tax_cents` | int | Tax in cents |
| `total_cents` | int | Total in cents |
| `currency` | string | ISO 4217 |
| `status` | StripeQuoteStatus | Current status |
| `valid_until` | datetime | Expiration |
| `notes` | string | Special pricing info |
| `terms_and_conditions` | string | Terms |
| `stripe_quote_url` | string | Stripe URL |
| `created_at` | datetime | Created |
**Status codes:** 200, 401, 403, 404, 500
---
### POST /quotes/{quote_id}/confirm — Accept quote
Finalizes quote, creates draft invoice, advances experiment to `WaitingForMaterials`.
**Path param:** `quote_id` (string)
**Request body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `purchase_order_number` | string/null | No | PO number |
| `notes` | string/null | No | Reserved |
**Response:** `id`, `status`, `hosted_invoice_url`, `invoice_id`
**Status codes:** 200, 403, 404, 409, 500
---
### POST /quotes/{quote_id}/reject — Reject quote
Cancels quote; linked experiment reverts to `Draft`.
**Path param:** `quote_id` (string)
**Request body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `reason` | QuoteRejectionReason | Yes | Primary reason |
| `feedback` | string/null | No | Additional feedback |
**Response:** `id`, `status` (canceled)
**Status codes:** 200, 403, 404, 409, 500
---
## Tokens
### GET /tokens — List tokens
Returns all tokens (root and attenuated) the caller owns.
**Query params:** `limit`, `offset`
**Response item:**
| Field | Type | Description |
|---|---|---|
| `id` | string | Token identifier |
| `name` | string | Human-readable label |
| `kind` | string | "root" or "attenuated" |
| `created_at` | datetime | Created |
| `expires_at` | datetime/null | Expiration (null = no expiry) |
| `revoked_at` | datetime/null | Revocation timestamp |
| `parent_token_id` | string/null | Parent (null for root) |
| `root_token_id` | string/null | Root of derivation tree |
| `attenuation_spec` | object/null | Restrictions (null for root) |
---
### POST /tokens/attenuate — Attenuate token
Creates a restricted version of an existing token using Biscuit cryptographic attenuation.
**Request body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `token` | string | Yes | Existing token (`abs0_{slug}{biscuit_base64}`) |
| `attenuation` | AttenuationSpec | Yes | Restrictions to apply |
| `name` | string | Yes | Human-readable label |
| `attenuated_parent_token_id` | uuid/null | No | Parent ID for chained attenuation |
**Restriction types:** Organization, Resource (experiments/results), Action (read/create/update), Expiry
**Response (201):** `id` (database ID), `token` (new attenuated token string)
**Status codes:** 201, 400, 401, 403
---
### POST /tokens/revoke — Revoke token and lineage
Revokes the calling token's root and all attenuated descendants. Idempotent.
**Response:**
| Field | Type | Description |
|---|---|---|
| `token_id` | string | Root token ID revoked |
| `revoked_at` | datetime | Revocation timestamp |
| `children_revoked` | int64 | Child tokens newly revoked |
**Status codes:** 200, 403, 404
---
## Updates
### GET /updates — List updates
Returns the experiment update feed (newest first): status changes, progress, errors.
**Query params:** `limit`, `offset`, `filter`, `sort`
**Filter examples:**
- `filter=eq(experiment_id,<uuid>)`
- `filter=in(experiment_id,uuid1,uuid2)`
- `filter=eq(type,status_change)`
**Response item:**
| Field | Type | Description |
|---|---|---|
| `id` | string | Update identifier |
| `experiment_id` | uuid | Associated experiment |
| `experiment_code` | string | Human-readable code |
| `name` | string | Update description |
| `timestamp` | datetime | When the update occurred |
---
## Feedback
### POST /feedback/submit — Submit feedback
For bug reports, feature requests, or general feedback.
**Request body:**
| Field | Type | Required | Description |
|---|---|---|---|
| `request_uuid` | uuid | Yes | UUID from the problematic API request |
| `feedback_type` | FeedbackType | Yes | `feature_request`, `feedback`, or `bug_report` |
| `title` | string/null | No | Short title |
| `json_body` | object/null | At least one | Structured error details |
| `human_note` | string/null | At least one | Free-form description |
**Response (201):** `reference` (feedback reference), `message` (confirmation)
**Status codes:** 201, 400, 401, 500