{"openapi":"3.1.0","info":{"title":"Adaptyv Foundry API","description":"\nThe Foundry API enables programmatic access to Adaptyv Bio's protein characterization services.\n\n## Getting Started\n\n1. **Obtain an API token** from the [Adaptyv Portal](https://foundry.adaptyvbio.com) or by\n   contacting Adaptyv directly\n2. **Authenticate requests** using Bearer token in the Authorization header\n3. **Create experiments** to submit protein sequences for characterization\n4. **Monitor status** via webhooks or polling the experiment detail endpoint\n\n## Authentication\n\nAll requests require a Bearer token:\n\n```\nAuthorization: Bearer <your-token>\n```\n\nTokens can be attenuated (restricted) to specific organizations or capabilities via the `/tokens` endpoint.\n\n## Pagination\n\nList endpoints support offset-based pagination:\n\n| Parameter | Description | Default |\n|-----------|-------------|---------|\n| `limit` | Maximum items per page | 50 |\n| `offset` | Number of items to skip | 0 |\n\n## Filtering\n\nMost list endpoints support filtering via query parameters using comparison operators:\n\n- `equ(field)=value` — equals\n- `geq(field)=value` — greater than or equal\n- `gtr(field)=value` — greater than\n- `leq(field)=value` — less than or equal\n- `lss(field)=value` — less than\n\nExample: `GET /experiments?geq(created_at)=2025-01-01&status=draft`\n\n## Environments\n\n| Environment | Base URL |\n|-------------|----------|\n| Production | `https://devs.adaptyvbio.com` |\n\n## Support\n\n- **Email**: support@adaptyvbio.com\n- **Documentation**: [docs.adaptyvbio.com](https://docs.adaptyvbio.com)\n","version":"0.0.2"},"servers":[{"url":"https://devs.adaptyvbio.com/","description":"Production API (Public)"}],"paths":{"/api/v1/experiments":{"get":{"tags":["experiments"],"summary":"List experiments","description":"Lists experiments accessible to the caller, sorted by creation date\n(newest first). Experiments progress from submission through review,\nproduction, and analysis until results are available.\n\nThe response includes only experiments within the caller's organization\nscope. Filter by `status` for specific workflow stages, by `state` for\nbroader groupings, or by `search` for name/code substring matches.\nDate operators (`lte`, `gte`, `lt`, `gt`, `eq`) accept ISO 8601 dates\nor timestamps via `filter=` s-expression. Paginate with `limit` and\n`offset`; fetch pages until fewer than `limit` items return.","operationId":"list_experiments","parameters":[{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"filter","in":"query","description":"Filter expression in s-expression syntax.\n\n**Comparison:** `eq(field,value)`, `neq(field,value)`, `gt(field,value)`,\n`lt(field,value)`, `gte(field,value)`, `lte(field,value)`,\n`contains(field,substring)`\n\n**Range/set:** `between(field,lo,hi)`, `in(field,v1,v2,...)`\n\n**Logical:** `and(expr1,expr2,...)`, `or(expr1,expr2,...)`, `not(expr)`\n\n**Null checks:** `is_null(field)`, `is_not_null(field)`\n\n**JSONB access:** `at(field,key)` — e.g. `eq(at(metadata,score),42)`\n\n**Cast functions:** `float(expr)`, `int(expr)`, `text(expr)`,\n`timestamp(expr)`, `date(expr)`\n\nExample: `and(gte(created_at,2026-01-01),eq(status,draft))`","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Free-text search term applied to searchable columns.","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Experiment list","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Experiment record returned by the list endpoint.","required":["id","code","status","created_at","results_status","experiment_url"],"properties":{"code":{"type":"string","description":"Unique experiment code (e.g., \"EXP-2024-001\")"},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp of experiment creation"},"experiment_type":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ExperimentType","description":"Experiment discriminator from the spec"}]},"experiment_url":{"type":"string","description":"URL to view the experiment in the Foundry portal"},"id":{"type":"string","format":"uuid","description":"Unique identifier for the experiment"},"name":{"type":["string","null"],"description":"Human-readable name for the experiment (nullable in database)"},"results_status":{"$ref":"#/components/schemas/ResultsStatus","description":"Indicates whether results are available for this experiment"},"status":{"$ref":"#/components/schemas/ExperimentStatus","description":"Current lifecycle status of the experiment"},"stripe_invoice_url":{"type":["string","null"],"description":"Invoice URL (not populated in list/detail responses; use the dedicated invoice endpoint)."},"stripe_quote_url":{"type":["string","null"],"description":"Quote reference URL (may require provider account access)."}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"400":{"description":"Invalid experiment_type filter value","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to list experiments","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["experiments"],"summary":"Create experiment","description":"Creates a new experiment request for the Adaptyv Foundry platform.\n\nThe payload captures the target (from the catalog), antibody sequences,\nreplicate plan, assay parameters, optional metadata, and an optional webhook\nfor notifications. By default, experiments are created\nin Draft status so clients can review inputs before submission.\n\nSet `skip_draft: true` to bypass Draft status and submit directly for processing.\nThis is useful for automated pipelines with pre-validated payloads that\ndon't require manual review. The experiment will be created in\n\"Waiting for confirmation\" status instead of Draft.\n\nAPI-originated experiments are automatically assigned to the organization's\nAPI submissions project for traceability.\n\n# ExperimentSpec fields\n\nValidation is strict: fields that are not applicable to a given experiment\ntype (e.g., `method` on a non-binding type) cause a 400 rejection. Multiple\nvalidation errors are accumulated and returned together so callers can fix\neverything in one round-trip.\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `experiment_type` | string | **Required.** One of `affinity`, `screening`, `thermostability`, `fluorescence`, `expression`, `epitope_binning`, `enzyme_activity`. |\n| `method` | string | Required for `affinity`/`screening` (`bli` or `spr`). **Rejected** for all other types. |\n| `target_id` | string | Target UUID from the catalog. Required for `affinity`, `screening`, and `epitope_binning`. **Rejected** for non-binding types. |\n| `sequences` | object | **Required.** Map of sequence name to amino acid string. `epitope_binning` requires exactly 4-28 sequences in multiples of 4. |\n| `n_replicates` | integer | Optional for most types (default 3, range 1-5). **Rejected** for `epitope_binning`. |\n| `antigen_concentrations` | array | `affinity` only; optional, defaults to `[1000.0, 316.2, 100.0, 31.6, 0.0]` nM. |\n| `parameters` | object | Optional. Experiment-specific settings |\n\n# Sequence Formats\n\nThe `sequences` field accepts two formats:\n\n**Simple format** — amino acid string only:\n```json\n\"sequences\": {\n  \"seq1\": \"EVQLVESGGGLVQPGGSLRLSCAAS\"\n}\n```\n\n**Object format** — with metadata:\n```json\n\"sequences\": {\n  \"seq1\": {\n    \"aa_string\": \"EVQLVESGGGLVQPGGSLRLSCAAS\",\n    \"control\": false,\n    \"metadata\": { \"type\": \"scfv\", \"tag_location\": \"C\" }\n  }\n}\n```\n\n# Example: Affinity (BLI)\n\nFull kinetic characterization measuring on/off rates and KD.\nTarget ID `019a03da-b87f-7e15-8b02-cef171c9871d` is Human PD-L1 from the catalog.\n\n```json\n{\n  \"name\": \"PD-L1 affinity panel\",\n  \"experiment_spec\": {\n    \"experiment_type\": \"affinity\",\n    \"method\": \"bli\",\n    \"target_id\": \"019a03da-b87f-7e15-8b02-cef171c9871d\",\n    \"sequences\": {\n      \"pembrolizumab_vh\": \"QVQLVQSGVEVKKPGASVKVSCKASGYTFTNYYMYWVRQAPGQGLEWMGGINPSNGGTNFNEKFKNRVTLTTDSSTTTAYMELKSLQFDDTAVYYCARRDYRFDMGFDYWGQGTTVTVSS\",\n      \"pembrolizumab_vl\": \"EIVLTQSPATLSLSPGERATLSCRASKGVSTSGYSYLHWYQQKPGQAPRLLIYLASYLESGVPARFSGSGSGTDFTLTISSLEPEDFAVYYCQHSRDLPLTFGGGTKVEIK\"\n    },\n    \"n_replicates\": 3,\n    \"antigen_concentrations\": [1000.0, 316.2, 100.0, 31.6, 0.0]\n  },\n  \"webhook_url\": \"https://example.com/webhook\"\n}\n```\n\n# Example: Screening (SPR)\n\nHigh-throughput yes/no binding assessment using SPR.\n\n```json\n{\n  \"name\": \"Library screening round 1\",\n  \"experiment_spec\": {\n    \"experiment_type\": \"screening\",\n    \"method\": \"spr\",\n    \"target_id\": \"019a03da-b87f-7e15-8b02-cef171c9871d\",\n    \"sequences\": {\n      \"clone_A1\": \"EVQLVESGGGLVQPGGSLRLSCAASGFTFSSYAMSWVRQAPGKGLEWVSAISGSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCAKDRLSITIRPRYYGLDVWGQGTLVTVSS\",\n      \"clone_A2\": \"QVQLVQSGAEVKKPGASVKVSCKASGYTFTSYGISWVRQAPGQGLEWMGWISAYNGNTNYAQKLQGRVTMTTDTSTSTAYMELRSLRSDDTAVYYCARDVGYCTDYSCYFDYWGQGTLVTVSS\",\n      \"clone_A3\": \"EVQLLESGGGLVQPGGSLRLSCAASGFTFSTYAMSWVRQAPGKGLEWVSSISSGGSYIYYADSVKGRFTISRDNAKNSLYLQMNSLRAEDTAVYYCARRPWGYYALDIWGQGTTVTVSS\"\n    },\n    \"n_replicates\": 2\n  }\n}\n```\n\n# Example: Thermostability\n\nMeasures melting temperature (Tm) via differential scanning fluorimetry.\nNo target required.\n\n```json\n{\n  \"name\": \"Lead candidates stability\",\n  \"experiment_spec\": {\n    \"experiment_type\": \"thermostability\",\n    \"sequences\": {\n      \"candidate_1\": \"QVQLVQSGAEVKKPGASVKVSCKASGYTFTSYAMHWVRQAPGQRLEWMGWINAGNGNTKYSQKFQGRVTITRDTSASTAYMELSSLRSEDTAVYYCARAKFGATGAFDIWGQGTMVTVSS\",\n      \"candidate_2\": \"EVQLVESGGGLVQPGGSLRLSCAASGFNIKDTYIHWVRQAPGKGLEWVARIYPTNGYTRYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCSRWGGDGFYAMDYWGQGTLVTVSS\"\n    },\n    \"n_replicates\": 3,\n    \"parameters\": {\n      \"buffer\": \"PBS\",\n      \"ph\": 7.4\n    }\n  }\n}\n```\n\n# Example: Fluorescence\n\nFluorescence-based protein characterization measuring intrinsic properties.\nNo target required.\n\n```json\n{\n  \"name\": \"Fluorescence characterization\",\n  \"experiment_spec\": {\n    \"experiment_type\": \"fluorescence\",\n    \"sequences\": {\n      \"variant_1\": \"QVQLVQSGAEVKKPGASVKVSCKASGYTFTSYDINWVRQATGQGLEWMGWMNPNSGNTGYAQKFQGRVTMTRDTSISTAYMELRSLRSDDTAVYYCARGGFYGSTIWFDYWGQGTLVTVSS\",\n      \"variant_2\": \"EVQLVESGGGLVQPGGSLRLSCAASGFTFSSYWMSWVRQAPGKGLEWVANIKQDGSEKYYVDSVKGRFTISRDNAKNSLYLQMNSLRAEDTAVYYCARDRYGNYVDYWGQGTLVTVSS\"\n    },\n    \"n_replicates\": 3\n  }\n}\n```\n\n# Example: Expression\n\nProtein expression screening measuring yield and quality.\nNo target required.\n\n```json\n{\n  \"name\": \"Expression screening\",\n  \"experiment_spec\": {\n    \"experiment_type\": \"expression\",\n    \"sequences\": {\n      \"construct_A\": \"QVQLVQSGAEVKKPGASVKVSCKASGYTFTSYGISWVRQAPGQGLEWMGWISAYNGNTNYAQKLQGRVTMTTDTSTSTAYMELRSLRSDDTAVYYCARDVGYCTDYSCYFDYWGQGTLVTVSS\",\n      \"construct_B\": \"EVQLVESGGGLVQPGGSLRLSCAASGFNIKDTYIHWVRQAPGKGLEWVARIYPTNGYTRYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCSRWGGDGFYAMDYWGQGTLVTVSS\"\n    },\n    \"n_replicates\": 2\n  }\n}\n```\n\n# Example: With skip_draft=true (Auto-Submit)\n\nBypass Draft status and submit directly for processing:\n\n```json\n{\n  \"name\": \"Pre-validated batch\",\n  \"skip_draft\": true,\n  \"experiment_spec\": {\n    \"experiment_type\": \"thermostability\",\n    \"sequences\": {\n      \"seq1\": \"EVQLVESGGGLVQPGGSLRLSCAASGFTFSSYAMSWVRQAPGKGLEWVSAISGSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCAKDRLSITIRPRYYGLDVWGQGTLVTVSS\"\n    },\n    \"n_replicates\": 2\n  }\n}\n```","operationId":"create_exp","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExpRequest"}}},"required":true},"responses":{"200":{"description":"Atomic create-and-pay for a machine rail (auto_accept_quote): the (open, unpaid) invoice is finalized and the `payment` block points at `/invoices/{id}/pay` where the client settles. Never settles or challenges at create.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmQuoteResponse"}}}},"201":{"description":"Experiment created (no payment, or async-invoice auto-accept)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExpResponse"}}}},"400":{"description":"Invalid request parameters (e.g. malformed target_id), or a duplicate sequence — two sequences sharing normalized residues and tag location (Fabs exempt)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"target_id references a target that is not in the catalog","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Unsupported media type — the request body must be sent as application/json","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Unprocessable request body (well-formed JSON that fails semantic validation, e.g. an invalid amino-acid sequence)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/experiments/cost-estimate":{"post":{"tags":["experiments"],"summary":"Estimate experiment cost","description":"Calculates the estimated cost for an experiment without creating it.\nUseful for previewing costs before submission.\n\n# Request Body\n\nAccepts an `experiment_spec` matching the creation endpoint format:\n\n```json\n{\n  \"experiment_spec\": {\n    \"experiment_type\": \"screening\",\n    \"method\": \"bli\",\n    \"target_id\": \"019a03da-b87f-7e15-8b02-cef171c9871d\",\n    \"sequences\": {\n      \"seq1\": \"MKTL...\",\n      \"seq2\": \"EVQL...\"\n    },\n    \"n_replicates\": 3\n  }\n}\n```\n\n# Response\n\nReturns a detailed cost breakdown including:\n- `pricing_version`: The pricing rules applied (e.g., \"v1_2026-01-20\")\n- `assay`: Per-experiment-type costs with base and replicate pricing\n- `materials`: Target material costs (for binding experiments)\n- `total_cents`: Sum of all costs in USD cents\n\n# Complete Estimate Response\n\nWhen the target has self-service pricing:\n```json\n{\n  \"breakdown\": {\n    \"pricing_version\": \"v1_2026-01-20\",\n    \"assay\": { \"name\": \"screening\", \"base_cents\": 14900, \"replicate_addon_cents\": 5800, \"subtotal_cents\": 103500 },\n    \"materials\": { \"target\": { \"name\": \"Human PD-L1\", \"target_catalog_id\": \"019a03da-b87f-7e15-8b02-cef171c9871d\" }, \"price_per_sequence_cents\": 500, \"subtotal_cents\": 2500 },\n    \"total_cents\": 106000\n  },\n  \"incomplete\": null,\n  \"warnings\": []\n}\n```\n\n# Incomplete Estimate Response\n\nWhen the target lacks self-service pricing (e.g., custom proteins), you'll\nreceive an incomplete estimate with assay costs but `materials_unavailable`:\n```json\n{\n  \"breakdown\": null,\n  \"incomplete\": {\n    \"pricing_version\": \"v1_2026-01-20\",\n    \"assay\": { \"name\": \"screening\", \"base_cents\": 14900, \"replicate_addon_cents\": 5800, \"subtotal_cents\": 103500 },\n    \"materials_unavailable\": {\n      \"target\": { \"name\": \"This target\", \"target_catalog_id\": \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\" },\n      \"reason\": \"This target is not yet onboarded to self-service pricing. You'll receive a quote with full price information.\"\n    }\n  },\n  \"warnings\": []\n}\n```\n\nUse `GET /targets?selfservice_only=true` to list targets with complete pricing.\n\n# Notes\n\n- **All prices exclude VAT**; taxes calculated at invoicing based on jurisdiction\n- Requires authentication with experiment read permission\n- Does not create an experiment\n- Pricing is based on the current date (not a future experiment date)\n- Targets without self-service pricing return incomplete estimates (not errors)\n- Experiments created before pricing v1 (2026-01-20) cannot be estimated","operationId":"cost_estimate","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostEstimateRequest"}}},"required":true},"responses":{"200":{"description":"Cost estimate calculated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostEstimateResponse"}}}},"400":{"description":"Malformed request body, or an invalid experiment specification","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"Request body was not sent as `application/json`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body did not match this operation's schema, or pricing is unavailable (unknown target or pre-v1 date). The `error` field distinguishes the two.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-action":"Read"}},"/api/v1/experiments/{experiment_id}":{"get":{"tags":["experiments"],"summary":"Get experiment","description":"Returns full metadata for an experiment: workflow status, target and\nsequence details, assay parameters, and billing references.\n\nPoll this endpoint to track progress. When `stripe_quote_url` appears,\nfetch quote details via `/experiments/{id}/quote`. After confirmation,\n`stripe_invoice_url` provides a direct link to the invoice. The `results_status` field\nsignals data availability: `None` (pending), `Partial` (preliminary),\nor `All` (complete). Query `/results` filtered by experiment to retrieve\ndata once results are available.","operationId":"get_exp_info","parameters":[{"name":"experiment_id","in":"path","description":"Unique experiment request identifier","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Experiment details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExpInfo"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Experiment not found or access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"patch":{"tags":["experiments"],"summary":"Update experiment","description":"Modify an existing experiment's configuration or status.\n\nUpdates experiment metadata or inputs. Most fields can only be changed while\nthe experiment is in Draft or InReview status; naming one on a later status\nreturns 409 Conflict. `webhook_url` is the exception and stays editable for\nthe life of the experiment, so a customer can repoint delivery after\nsubmitting.","operationId":"modify_exp","parameters":[{"name":"experiment_id","in":"path","description":"Unique experiment request identifier to modify","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModifyExpRequest"}}},"required":true},"responses":{"200":{"description":"Experiment modified successfully or no action taken (e.g., already fulfilled)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModifyExpResponse"}}}},"400":{"description":"Invalid modification request, including a replacement sequence list containing duplicates — two sequences sharing normalized residues and tag location (Fabs exempt); also a malformed body, or `Content-Type: application/json` declared with no body sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Experiment request not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Modification not allowed (e.g., request already fulfilled)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"No body was sent, or a body was sent under a content type other than `application/json`. A JSON body is required; every field is optional, so send `{}` if you have nothing to change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Well-formed JSON that does not fit `ModifyExpRequest`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/experiments/{experiment_id}/invoice":{"get":{"tags":["experiments"],"summary":"Get experiment invoice","description":"Returns invoice metadata for a confirmed experiment, including the\nhosted payment URL. Available once `stripe_invoice_url` appears in the\nexperiment detail; timing depends on organization billing settings.\n\nAn invoice exists only after the quote has been accepted. Until then this\nreturns `404`, and the message names the confirm call to make instead of\npolling here.","operationId":"get_invoice_metadata","parameters":[{"name":"experiment_id","in":"path","description":"Experiment identifier","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Stripe invoice metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentInvoiceResponse"}}}},"400":{"description":"Unknown or inaccessible experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Caller does not have access to this experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Invoice not yet available. The body names the next call: `POST /api/v1/experiments/{experiment_id}/quote/confirm` when a quote exists but has not been accepted, or `POST /api/v1/experiments/{experiment_id}/submit` first when no quote exists yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/experiments/{experiment_id}/quote":{"get":{"tags":["experiments"],"summary":"Get experiment quote","description":"Returns quote metadata: totals, currency, status, and expiration.\nThe `expires_at` field shows when the quote expires if not confirmed.\n\n**The quote is generated asynchronously, so poll this endpoint.**\n`POST /experiments/{experiment_id}/submit` returns before the quote exists;\ngenerating it involves a call to the payment provider. Until it lands this\nendpoint answers `404`, which means *not yet* rather than *never* — the same\ncontract `GET /experiments/{experiment_id}/invoice` follows. A `404` here is\nan expected step in the normal flow, not an error to report.\n\nPoll until `200`, or equivalently until `stripe_quote_id` appears on\n`GET /experiments/{experiment_id}`. A second or two is typical. Distinguish\nthe two `404` causes by the experiment's own status: an experiment still in\n`draft` has not been submitted, so no quote is coming until it is.","operationId":"get_quote_metadata","parameters":[{"name":"experiment_id","in":"path","description":"Experiment identifier","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Quote metadata retrieved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentQuoteResponse"}}}},"400":{"description":"Unknown or inaccessible experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Caller does not have access to this experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The quote has not been generated yet. Expected while it is being created after submit — poll until 200. Also returned when the experiment was never submitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/experiments/{experiment_id}/quote/confirm":{"post":{"tags":["experiments"],"summary":"Accept an experiment's quote and create an invoice","description":"A JSON body is required. Every field of `ConfirmQuoteRequest` is optional,\nso a caller with no purchase-order number and no notes sends `{}` — the\nrefusal says so if they forget (BUG-0059).\n\nConvenience endpoint that does the same thing as `POST /quotes/{id}/confirm`\nbut addressed by experiment ID — the server resolves the quote ID internally.\nLike `/quotes/{id}/confirm`, it is **categorically non-settling**: it\nfinalizes the (open, unpaid) invoice and returns `200` + the invoice + a\nHATEOAS `payment` pointer (the Stripe-hosted page for async; `/invoices/{id}/pay`\nfor a machine rail). It never settles or emits a payment `402`.","operationId":"confirm_experiment_quote","parameters":[{"name":"experiment_id","in":"path","description":"Experiment identifier","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmQuoteRequest"}}},"required":true},"responses":{"200":{"description":"Quote accepted; the (open, unpaid) invoice is finalized and the `payment` block points at where/how to pay","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmQuoteResponse"}}}},"400":{"description":"Unknown or inaccessible experiment, malformed request body, or `Content-Type: application/json` declared with no body sent. Every field is optional, so send `{}` if you have nothing to add","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Caller does not have access to this experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The quote has not been generated yet — poll until 200. Also returned when the experiment was never submitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Quote cannot be accepted (wrong status or already invoiced)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"No body was sent, or a body was sent under a content type other than `application/json`. A JSON body is required; send `{}` if you have nothing to add","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Unprocessable request body (well-formed JSON that fails semantic validation)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-action":"Update"}},"/api/v1/experiments/{experiment_id}/quote/pdf":{"get":{"tags":["experiments"],"summary":"Get experiment quote PDF","description":"Returns the quote as a PDF containing itemized pricing, terms, and\nexperiment configuration. Response content-type is `application/pdf`.","operationId":"get_quote_pdf","parameters":[{"name":"experiment_id","in":"path","description":"Experiment identifier","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Quote PDF stream","content":{"application/pdf":{}}},"400":{"description":"Unknown or inaccessible experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Caller does not have access to this experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"The quote has not been generated yet — poll until 200. Also returned when the experiment was never submitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/experiments/{experiment_id}/results":{"get":{"tags":["experiments"],"summary":"List results for an experiment","description":"Returns all analysis results associated with a specific experiment.\nResults appear when the experiment's `results_status` field reaches\n`Partial` or `All`. Supports pagination via `limit` and `offset` query\nparameters.\n\nUse this endpoint when you need results for a single experiment without\nfiltering the global `/results` list. The response includes the same\n`ResultInfo` structure as the global endpoint, scoped to the\nspecified experiment.","operationId":"get_experiment_results","parameters":[{"name":"experiment_id","in":"path","description":"Experiment identifier (UUID)","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"filter","in":"query","description":"Filter expression in s-expression syntax.\n\n**Comparison:** `eq(field,value)`, `neq(field,value)`, `gt(field,value)`,\n`lt(field,value)`, `gte(field,value)`, `lte(field,value)`,\n`contains(field,substring)`\n\n**Range/set:** `between(field,lo,hi)`, `in(field,v1,v2,...)`\n\n**Logical:** `and(expr1,expr2,...)`, `or(expr1,expr2,...)`, `not(expr)`\n\n**Null checks:** `is_null(field)`, `is_not_null(field)`\n\n**JSONB access:** `at(field,key)` — e.g. `eq(at(metadata,score),42)`\n\n**Cast functions:** `float(expr)`, `int(expr)`, `text(expr)`,\n`timestamp(expr)`, `date(expr)`\n\nExample: `and(gte(created_at,2026-01-01),eq(status,draft))`","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Free-text search term applied to searchable columns.","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Results for the experiment","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"A completed experiment result: its summarized measurements and a link to the\nraw data.","required":["id","title","experiment_id","result_type","created_at","summary","metadata"],"properties":{"created_at":{"type":"string","format":"date-time","description":"When this result was generated (ISO 8601)."},"data_package_url":{"type":["string","null"],"description":"Download URL for the raw data package, the same one available in the\nFoundry portal. Omitted when no package is available."},"experiment_id":{"type":"string","format":"uuid","description":"Identifier of the experiment this result belongs to."},"id":{"type":"string","format":"uuid","description":"Identifier for this result."},"metadata":{"type":"object","description":"Additional metadata beyond the summary."},"result_type":{"type":"string","description":"Assay type for this result (e.g. \"affinity\", \"thermostability\")."},"summary":{"type":"array","items":{"$ref":"#/components/schemas/ResultSummary"},"description":"Per-readout measurements, each tagged by assay type."},"title":{"type":"string","description":"Human-readable title for this result."}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"400":{"description":"Invalid, unknown, or inaccessible experiment_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/experiments/{experiment_id}/sequences":{"get":{"tags":["experiments"],"summary":"List sequences for an experiment","description":"Returns all sequences associated with a specific experiment. Results are sorted\nby creation date (newest first) and can be searched by name or amino acid content.\nSupports pagination via `limit` and `offset` query parameters.\n\nFor sequences from a single experiment without filtering the global\n`/sequences` list. Returns `SequenceListItem` records scoped to the\nspecified experiment.","operationId":"get_experiment_sequences","parameters":[{"name":"experiment_id","in":"path","description":"Experiment identifier (UUID)","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"search","in":"query","description":"Free-text search term applied to searchable columns.","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sequences for the experiment","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Sequence record returned by the list endpoint.\n\nIncludes a truncated preview and experiment reference; use the detail\nendpoint to retrieve the full amino acid string.","required":["id","length","experiment_id","experiment_code","is_control","created_at"],"properties":{"aa_preview":{"type":["string","null"],"description":"Truncated sequence preview (first 50 characters)"},"created_at":{"type":"string","format":"date-time","description":"When the sequence was created (experiment submission time)"},"experiment_code":{"type":"string","description":"Human-readable experiment code"},"experiment_id":{"type":"string","format":"uuid","description":"ID of the experiment containing this sequence"},"id":{"type":"string","format":"uuid","description":"Unique identifier for the sequence"},"is_control":{"type":"boolean","description":"Whether this sequence is marked as a control"},"length":{"type":"integer","format":"int32","description":"Full sequence length in amino acids","minimum":0},"name":{"type":["string","null"],"description":"Optional name assigned to the sequence"}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"400":{"description":"Invalid, unknown, or inaccessible experiment_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/experiments/{experiment_id}/submit":{"post":{"tags":["experiments"],"summary":"Submit experiment","description":"Submits a draft experiment for review. Advances from Draft to\nWaitingForConfirmation by finalizing the Stripe quote. To accept an\nalready-submitted quote, use `POST /experiments/{id}/quote/confirm`.","operationId":"submit_experiment","parameters":[{"name":"experiment_id","in":"path","description":"Experiment identifier","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Experiment submitted for review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExperimentConfirmationResponse"}}}},"400":{"description":"Unknown or inaccessible experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Caller does not have access to this experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Experiment cannot transition out of its current state. When a prior `POST /experiments` attempted to create a Stripe quote and Stripe rejected it, the response includes `error_kind` and `hint` fields identifying the failure class (e.g. `customer_quote_cap`). Otherwise those fields are omitted (legacy shape).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitConflictError"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-action":"Update"}},"/api/v1/experiments/{experiment_id}/updates":{"get":{"tags":["experiments"],"summary":"List experiment updates","description":"Returns updates for one experiment, oldest first (chronological).\nUpdate types include `status_change`, `progress`, and `error`.\nFilter by type using `filter=eq(type,status_change)`.","operationId":"get_experiment_updates","parameters":[{"name":"experiment_id","in":"path","description":"Experiment identifier","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"filter","in":"query","description":"Filter expression in s-expression syntax.\n\n**Comparison:** `eq(field,value)`, `neq(field,value)`, `gt(field,value)`,\n`lt(field,value)`, `gte(field,value)`, `lte(field,value)`,\n`contains(field,substring)`\n\n**Range/set:** `between(field,lo,hi)`, `in(field,v1,v2,...)`\n\n**Logical:** `and(expr1,expr2,...)`, `or(expr1,expr2,...)`, `not(expr)`\n\n**Null checks:** `is_null(field)`, `is_not_null(field)`\n\n**JSONB access:** `at(field,key)` — e.g. `eq(at(metadata,score),42)`\n\n**Cast functions:** `float(expr)`, `int(expr)`, `text(expr)`,\n`timestamp(expr)`, `date(expr)`\n\nExample: `and(gte(created_at,2026-01-01),eq(status,draft))`","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Experiment update list","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Summary of a single update event","required":["id","experiment_id","experiment_code","name","timestamp"],"properties":{"experiment_code":{"type":"string","description":"Experiment code for grouping and display"},"experiment_id":{"type":"string","format":"uuid","description":"ID of the experiment that generated this update"},"id":{"type":"string","description":"Unique identifier for this update event (UUID)"},"name":{"type":"string","description":"Name/title of the update"},"timestamp":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the update occurred"}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"400":{"description":"Unknown or inaccessible experiment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/feedback/submit":{"post":{"tags":["feedback"],"summary":"Submit feedback, bug reports, or feature requests","description":"Use this endpoint to give us feedback, report bugs that are not already caught by our\nobservability, or request features (or have your agents do it).\n\n# Request\n\nProvide:\n- `request_uuid`: The UUID from the problematic API request\n- `feedback_type`: `feature_request`, `feedback`, or `bug_report`\n- `title`: Optional short title (defaults to a timestamp + organization name)\n- At least one of (can provide both):\n  - `json_body`: Structured error details as JSON\n  - `human_note`: Free-form text description","operationId":"submit_feedback","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitFeedbackRequest"}}},"required":true},"responses":{"201":{"description":"Feedback submitted successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitFeedbackResponse"}}}},"400":{"description":"Invalid request (neither json_body nor human_note provided)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/info/health":{"get":{"tags":["info"],"summary":"Liveness probe","description":"Returns 200 when the service is alive. No authentication required. No database interaction.","operationId":"health","responses":{"200":{"description":"Service is alive","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/v1/info/health-db":{"get":{"tags":["info"],"summary":"Database connectivity probe","description":"Verifies database connectivity by executing a trivial query. Returns 200 when the database is reachable, 503 when it is not.","operationId":"health_db","responses":{"200":{"description":"Database is connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthDbResponse"}}}},"503":{"description":"Database is unreachable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthDbResponse"}}}}}}},"/api/v1/invoices":{"get":{"tags":["invoices"],"summary":"List invoices","description":"Lists every invoice generated for the caller's organization so clients\ncan display billing history, monitor outstanding balances, and reconcile\npayments. Results are sorted by creation date by default and include due dates,\ncurrency, and current status for each invoice.","operationId":"list_invoices","parameters":[{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"filter","in":"query","description":"Filter expression in s-expression syntax.\n\n**Comparison:** `eq(field,value)`, `neq(field,value)`, `gt(field,value)`,\n`lt(field,value)`, `gte(field,value)`, `lte(field,value)`,\n`contains(field,substring)`\n\n**Range/set:** `between(field,lo,hi)`, `in(field,v1,v2,...)`\n\n**Logical:** `and(expr1,expr2,...)`, `or(expr1,expr2,...)`, `not(expr)`\n\n**Null checks:** `is_null(field)`, `is_not_null(field)`\n\n**JSONB access:** `at(field,key)` — e.g. `eq(at(metadata,score),42)`\n\n**Cast functions:** `float(expr)`, `int(expr)`, `text(expr)`,\n`timestamp(expr)`, `date(expr)`\n\nExample: `and(gte(created_at,2026-01-01),eq(status,draft))`","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Invoice list","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Invoice record returned by the list endpoint.","required":["id","invoice_number","organization_id","amount_cents","currency","status","due_date","created_at"],"properties":{"amount_cents":{"type":"integer","format":"int64","description":"Total invoice amount in smallest currency unit (cents)"},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when invoice was created"},"currency":{"type":"string","description":"ISO 4217 currency code (e.g., \"usd\")"},"due_date":{"type":"string","format":"date-time","description":"ISO 8601 date when payment is due"},"hosted_invoice_url":{"type":["string","null"],"description":"Stripe-hosted URL where the customer can view and pay the invoice","example":"https://invoice.stripe.com/i/acct_1234/test_5678"},"id":{"type":"string","description":"Unique invoice identifier (Stripe invoice ID, e.g. \"in_xxx\")"},"invoice_number":{"type":"string","description":"Human-readable invoice number for reference"},"organization_id":{"type":"string","format":"uuid","description":"Organization ID that this invoice belongs to"},"status":{"$ref":"#/components/schemas/StripeInvoiceStatus","description":"Current payment status from Stripe"}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to view invoices","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/invoices/{invoice_id}":{"get":{"tags":["invoices"],"summary":"Get invoice","description":"Fetches a single invoice by its Foundry UUID or Stripe invoice ID\n(`in_xxx`). The caller's org membership scopes visibility — an invoice\nbelonging to an org the caller cannot access returns 404.","operationId":"get_invoice","parameters":[{"name":"invoice_id","in":"path","description":"Foundry invoice UUID or Stripe invoice ID (`in_xxx`)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Invoice detail","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceDetailResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to view invoices","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Invoice not found or not accessible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/invoices/{invoice_id}/pay":{"post":{"tags":["invoices"],"summary":"Pay invoice","description":"Settles the given invoice. How it settles is chosen by the\n`X-Adaptyv-Payment-Method` request header:\n\n| Header value | Behaviour |\n|---|---|\n| _(absent)_ | Returns the current invoice state; pay via the hosted invoice URL. If the quote was pinned to a crypto rail at experiment creation, returns `409` — retry with the `X-Adaptyv-Payment-Method` header used at creation. |\n| `mpp-spt` | Settles from a Stripe shared payment token |\n| `x402-exact` | Settles on-chain with an x402 `exact` USDC transfer |\n\nThe payment rail is pinned when the experiment is created, and an *explicit*\n`X-Adaptyv-Payment-Method` cannot switch it: a header naming a rail other\nthan the pinned one is rejected with `409` — retry with the pinned rail.\nHeader-less discovery is the deliberate exception: a caller with a machine\npayment policy is offered every machine rail this deployment serves and\ncommits by sending one rail's credential, which may be a machine rail other\nthan the pinned one (they share a Stripe account, so the pin has already\ndecided everything it decides). Omitting the header is therefore not a way\nto reach the pinned rail, and a caller with no machine policy falls through\nto the hosted invoice — which on a crypto-pinned quote is the absent-header\n`409` in the table above.\n\nThis is the only endpoint that settles an invoice. The flow is two steps:\n`POST /quotes/{quote_id}/confirm` issues the invoice and returns a `payment`\npointer to this route, then `POST /invoices/{invoice_id}/pay` settles it.\n\nSend the first call with the method header and no credential. It answers\n`402`, and **the challenge is carried in the response headers, not the\nbody** — `PAYMENT-REQUIRED` for `x402-exact`, `WWW-Authenticate` for\n`mpp-spt`.\n\nThat challenge is the whole handshake: it carries every parameter needed to\nproduce the credential — for `mpp-spt`, the amount, currency, seller-network\nid, expiry, and accepted payment-method types to mint the shared payment\ntoken against. There is no separate endpoint to fetch them from, and there\ncannot usefully be one: the credential must echo the challenge it answers,\nso parameters obtained without taking a `402` cannot be settled here.\n\nBoth `mpp-spt` and `x402-exact` settle over the API directly with these\nheaders; the MCP server's `pay_invoice` tool is one client of this route,\nnot the only way to reach the rail.\n\nRepeat the call with the signed credential to settle — `PAYMENT-SIGNATURE`\nfor `x402-exact`, `Authorization` for `mpp-spt` — and the response is `200`\nwith the paid invoice. Repeating it again is safe: an invoice that is\nalready paid answers `200` with `already_paid: true` and settles nothing.\n\nWhen the settlement can be named, the response carries\n`settlement_reference`. Treat it as optional even on a settling response.\n\nThe `payment.methods` array on the confirm response lists the methods this\nenvironment accepts; a value outside that list is rejected with `400`.","operationId":"pay_invoice","parameters":[{"name":"invoice_id","in":"path","description":"Foundry invoice UUID or Stripe invoice ID (`in_xxx`)","required":true,"schema":{"type":"string"}},{"name":"X-Adaptyv-Payment-Method","in":"header","description":"Settlement method: `mpp-spt` or `x402-exact`. Must name the rail pinned at experiment creation; naming a different one is a `409`. Omit to read the current invoice state and pay via the hosted URL instead — but a quote pinned to a crypto rail at experiment creation returns `409` when the header is absent, so retry with the value used at creation. The methods an environment accepts are listed in `payment.methods` on the confirm response.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Invoice paid (or already paid)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayInvoiceResponse"}}}},"400":{"description":"Unrecognised payment method, or a malformed credential","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to pay invoices","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Invoice not found or not accessible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"The `X-Adaptyv-Payment-Method` header names a rail other than the one pinned at experiment creation (retry with the pinned rail — it is named in the error), a settlement for this invoice is already in flight, or the session is closed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Invoice is not in a payable state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"501":{"description":"This environment does not provide the requested payment method","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-action":"Pay"}},"/api/v1/invoices/{invoice_id}/pdf":{"get":{"tags":["invoices"],"summary":"Get invoice PDF","description":"Redirects (307) to the Stripe-hosted PDF for the given invoice. The PDF is\nonly available after the invoice has been finalized (status `open` or\n`paid`). Requesting a PDF for a `draft` invoice returns 404 — finalize the\ninvoice first by submitting and confirming the associated quote.\n\nThe PDF URL is fetched live from Stripe on every request; clients should\nfollow the redirect immediately rather than caching it.","operationId":"get_invoice_pdf","parameters":[{"name":"invoice_id","in":"path","description":"Foundry invoice UUID or Stripe invoice ID (`in_xxx`)","required":true,"schema":{"type":"string"}}],"responses":{"307":{"description":"Redirect to the Stripe-hosted invoice PDF"},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to view invoices","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Invoice not found, not accessible, or PDF not yet available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/quotes":{"get":{"tags":["quotes"],"summary":"List quotes","description":"Returns every quote recorded for the caller's organization. Use this list\nto monitor pending proposals, track which quotes have already been accepted\nor expired, and review pricing history before moving to the detail or\nacceptance endpoints.","operationId":"list_quotes","parameters":[{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"filter","in":"query","description":"Filter expression in s-expression syntax.\n\n**Comparison:** `eq(field,value)`, `neq(field,value)`, `gt(field,value)`,\n`lt(field,value)`, `gte(field,value)`, `lte(field,value)`,\n`contains(field,substring)`\n\n**Range/set:** `between(field,lo,hi)`, `in(field,v1,v2,...)`\n\n**Logical:** `and(expr1,expr2,...)`, `or(expr1,expr2,...)`, `not(expr)`\n\n**Null checks:** `is_null(field)`, `is_not_null(field)`\n\n**JSONB access:** `at(field,key)` — e.g. `eq(at(metadata,score),42)`\n\n**Cast functions:** `float(expr)`, `int(expr)`, `text(expr)`,\n`timestamp(expr)`, `date(expr)`\n\nExample: `and(gte(created_at,2026-01-01),eq(status,draft))`","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Quote list","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Quote record returned by the list endpoint.\n\nRepresents a formal price proposal for an experiment.","required":["id","quote_number","organization_id","amount_cents","currency","status","valid_until","created_at","stripe_quote_url"],"properties":{"amount_cents":{"type":"integer","format":"int64","description":"Total quoted amount in smallest currency unit (cents)"},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when quote was created"},"currency":{"type":"string","description":"ISO 4217 currency code (e.g., \"USD\", \"EUR\", \"GBP\")"},"id":{"type":"string","description":"Unique quote identifier (format: \"qt-YYYY-XXX\")"},"organization_id":{"type":"string","format":"uuid","description":"Organization ID that this quote is prepared for"},"quote_number":{"type":"string","description":"Human-readable quote number for reference"},"status":{"$ref":"#/components/schemas/StripeQuoteStatus","description":"Current quote status from Stripe"},"stripe_quote_url":{"type":"string","description":"Quote reference URL (may require provider account access).","example":"https://billing.example.com/quotes/qt_1234567890"},"valid_until":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the quote expires"}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to view quotes","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/quotes/{quote_id}":{"get":{"tags":["quotes"],"summary":"Get quote","description":"Returns the full quote document with itemized pricing, totals, expiration,\nand status, combined with experiment metadata (org name, notes).","operationId":"get_quote_info","parameters":[{"name":"quote_id","in":"path","description":"Unique identifier of the quote to retrieve","required":true,"schema":{"type":"string"},"example":"qt_1Abc2DefGhi"}],"responses":{"200":{"description":"Quote details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteInfo"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to view this quote","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Quote not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/quotes/{quote_id}/confirm":{"post":{"tags":["quotes"],"summary":"Accept quote","description":"Finalizes and accepts a Stripe quote, creating the (open, unpaid)\ninvoice and advancing the linked experiment to `WaitingForMaterials`.\nOptionally attach a purchase order number for your records.\n\nA JSON body is required. Every field of `ConfirmQuoteRequest` is\noptional, so a caller with no purchase-order number and no notes sends\n`{}` — the refusal says so if they forget (BUG-0059).\n\nThis endpoint is **categorically non-settling**: it never settles a\npayment and never emits a payment `402`. The `200` response carries the\nfinalized invoice plus a HATEOAS `payment` pointer telling you where and\nhow to pay — the Stripe-hosted page for an async-invoice quote, or\n`POST /invoices/{invoice_id}/pay` for a machine rail (x402-exact /\nmpp-spt), where the `402` challenge is answered.\n\nThe quote must be in a confirmable state (\"Waiting for confirmation\" or\n\"Quote sent\") and must not already have an invoice. Expired or otherwise\nnon-confirmable quotes return 409.","operationId":"confirm_quote","parameters":[{"name":"quote_id","in":"path","description":"Unique identifier of the quote to accept","required":true,"schema":{"type":"string"},"example":"qt_1Abc2DefGhi"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmQuoteRequest"}}},"required":true},"responses":{"200":{"description":"Quote accepted; the (open, unpaid) invoice is finalized and the `payment` block points at where/how to pay","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmQuoteResponse"}}}},"400":{"description":"Malformed request body, or `Content-Type: application/json` was declared and no body sent. Every field is optional, so send `{}` if you have nothing to add","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to accept this quote","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Quote not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Quote cannot be accepted (wrong status, already accepted, or a payment method whose Stripe tier does not match the one pinned at creation)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"No body was sent, or a body was sent under a content type other than `application/json`. A JSON body is required; send `{}` if you have nothing to add","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Well-formed JSON that does not fit `ConfirmQuoteRequest`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-action":"Update"}},"/api/v1/quotes/{quote_id}/reject":{"post":{"tags":["quotes"],"summary":"Reject quote","description":"Cancels the quote on Stripe and records the rejection reason and\noptional feedback in the database. The linked experiment reverts to\n`Draft` so it can be modified and re-submitted.","operationId":"reject_quote","parameters":[{"name":"quote_id","in":"path","description":"Unique identifier of the quote to reject","required":true,"schema":{"type":"string"},"example":"qt_1Abc2DefGhi"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectQuoteRequest"}}},"required":true},"responses":{"200":{"description":"Quote successfully rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectQuoteResponse"}}}},"403":{"description":"Insufficient permissions to reject this quote","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Quote not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Quote cannot be rejected (wrong status or already has invoice)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-action":"Update"}},"/api/v1/results":{"get":{"tags":["results"],"summary":"List results","description":"Lists completed analysis results, sorted by creation date (newest first).\nResults appear when an experiment's `results_status` field reaches\n`Partial` or `All`. Paginate with `limit` (default 50) and `offset`;\nthe response includes `total` for page calculation.\n\nEach result includes its full `summary` (kinetics for BLI, readouts for\nthermostability) and `metadata`, matching the detail endpoint shape.\nSupports filtering on result fields; invalid filter fields return 400.","operationId":"list_results","parameters":[{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"filter","in":"query","description":"Filter expression in s-expression syntax.\n\n**Comparison:** `eq(field,value)`, `neq(field,value)`, `gt(field,value)`,\n`lt(field,value)`, `gte(field,value)`, `lte(field,value)`,\n`contains(field,substring)`\n\n**Range/set:** `between(field,lo,hi)`, `in(field,v1,v2,...)`\n\n**Logical:** `and(expr1,expr2,...)`, `or(expr1,expr2,...)`, `not(expr)`\n\n**Null checks:** `is_null(field)`, `is_not_null(field)`\n\n**JSONB access:** `at(field,key)` — e.g. `eq(at(metadata,score),42)`\n\n**Cast functions:** `float(expr)`, `int(expr)`, `text(expr)`,\n`timestamp(expr)`, `date(expr)`\n\nExample: `and(gte(created_at,2026-01-01),eq(status,draft))`","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","description":"Free-text search term applied to searchable columns.","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Result list","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"A completed experiment result: its summarized measurements and a link to the\nraw data.","required":["id","title","experiment_id","result_type","created_at","summary","metadata"],"properties":{"created_at":{"type":"string","format":"date-time","description":"When this result was generated (ISO 8601)."},"data_package_url":{"type":["string","null"],"description":"Download URL for the raw data package, the same one available in the\nFoundry portal. Omitted when no package is available."},"experiment_id":{"type":"string","format":"uuid","description":"Identifier of the experiment this result belongs to."},"id":{"type":"string","format":"uuid","description":"Identifier for this result."},"metadata":{"type":"object","description":"Additional metadata beyond the summary."},"result_type":{"type":"string","description":"Assay type for this result (e.g. \"affinity\", \"thermostability\")."},"summary":{"type":"array","items":{"$ref":"#/components/schemas/ResultSummary"},"description":"Per-readout measurements, each tagged by assay type."},"title":{"type":"string","description":"Human-readable title for this result."}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/results/{result_id}":{"get":{"tags":["results"],"summary":"Get result","description":"Returns a single result with its full `summary`: mean kinetics and the\nper-replicate measurements for an affinity result, or the per-sample\nmelting readouts for a thermostability result. The `data_package_url`\nfield links to the raw data package, the same one available in the\nFoundry portal.","operationId":"get_result_info","parameters":[{"name":"result_id","in":"path","description":"Unique result identifier","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Result details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultInfo"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Result not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/sequences":{"get":{"tags":["sequences"],"summary":"List sequences","description":"Returns sequences from all experiments the caller has access to. Results\nare sorted by creation date (newest first). Filter by `experiment_id` to\nretrieve sequences for a specific experiment, or use `search` to match\nagainst sequence names or amino acid content.","operationId":"list_sequences","parameters":[{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"search","in":"query","description":"Free-text search term applied to searchable columns.","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}},{"name":"experiment_id","in":"query","description":"Filter by experiment UUID","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sequence list","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Sequence record returned by the list endpoint.\n\nIncludes a truncated preview and experiment reference; use the detail\nendpoint to retrieve the full amino acid string.","required":["id","length","experiment_id","experiment_code","is_control","created_at"],"properties":{"aa_preview":{"type":["string","null"],"description":"Truncated sequence preview (first 50 characters)"},"created_at":{"type":"string","format":"date-time","description":"When the sequence was created (experiment submission time)"},"experiment_code":{"type":"string","description":"Human-readable experiment code"},"experiment_id":{"type":"string","format":"uuid","description":"ID of the experiment containing this sequence"},"id":{"type":"string","format":"uuid","description":"Unique identifier for the sequence"},"is_control":{"type":"boolean","description":"Whether this sequence is marked as a control"},"length":{"type":"integer","format":"int32","description":"Full sequence length in amino acids","minimum":0},"name":{"type":["string","null"],"description":"Optional name assigned to the sequence"}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to view sequences","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["sequences"],"summary":"Add sequences to experiment","description":"Appends sequences to a draft experiment identified by its human-readable code\n(e.g., \"PROJ-001\"). The experiment must be in Draft status; confirmed or later-stage\nexperiments cannot accept new sequences. Create a new experiment to submit\nadditional sequences if the original has progressed past draft.","operationId":"add_sequences","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SequenceAddRequest"}}},"required":true},"responses":{"201":{"description":"Sequences added","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SequenceAddResponse"}}}},"400":{"description":"Invalid request: empty sequences array, malformed data, or a duplicate sequence — two sequences sharing normalized residues and tag location, either within this request or against one already stored on the experiment (Fabs exempt)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Experiment not found or access denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Experiment is not in Draft status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/sequences/{sequence_id}":{"get":{"tags":["sequences"],"summary":"Get sequence","description":"Returns full details for a specific sequence including the complete amino\nacid string, metadata, and a reference to the containing experiment.","operationId":"get_sequence_info","parameters":[{"name":"sequence_id","in":"path","description":"Unique identifier of the sequence","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Sequence details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SequenceInfo"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions to view this sequence","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Sequence not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/targets":{"get":{"tags":["targets"],"summary":"List targets","description":"Lists validated antigens available for experiments. Each entry includes\nvendor and catalog identifiers, plus `pricing` where self-service pricing\nis available.\n\n**Pricing:** Targets with `pricing` support instant cost estimation via\n`/experiments/cost-estimate`. Targets without this field require a custom\nquote — contact support for pricing on these targets.\n\nUse `selfservice_only=true` to filter to only targets with pricing configured.\nSearch by product name with `search` (case-insensitive substring). Filter and\nsort by `name`, `vendor_name`, `catalog_number`, or `purity` via the\n`filter` and `sort` parameters (e.g. `?filter=gte(purity,90)&sort=-name`).\nPaginate with `limit` (default 50, max 100) and `offset` (default 0); the response\nincludes `total` for computing remaining pages. Pass the returned `id` as\n`experiment_spec.target_id` when creating experiments.","operationId":"list_targets","parameters":[{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"search","in":"query","description":"Free-text search term applied to searchable columns.","required":false,"schema":{"type":"string"}},{"name":"filter","in":"query","description":"Filter expression in s-expression syntax.\n\n**Comparison:** `eq(field,value)`, `neq(field,value)`, `gt(field,value)`,\n`lt(field,value)`, `gte(field,value)`, `lte(field,value)`,\n`contains(field,substring)`\n\n**Range/set:** `between(field,lo,hi)`, `in(field,v1,v2,...)`\n\n**Logical:** `and(expr1,expr2,...)`, `or(expr1,expr2,...)`, `not(expr)`\n\n**Null checks:** `is_null(field)`, `is_not_null(field)`\n\n**JSONB access:** `at(field,key)` — e.g. `eq(at(metadata,score),42)`\n\n**Cast functions:** `float(expr)`, `int(expr)`, `text(expr)`,\n`timestamp(expr)`, `date(expr)`\n\nExample: `and(gte(created_at,2026-01-01),eq(status,draft))`","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}},{"name":"selfservice_only","in":"query","description":"When true, returns only targets with self-service pricing configured.\nTargets without pricing require a custom quote flow.","required":false,"schema":{"type":"boolean"}},{"name":"show_conjugated","in":"query","description":"When true, includes conjugated targets (biotinylated, fluorophore-labeled,\netc.) in results. By default, only unconjugated targets are returned.","required":false,"schema":{"type":"boolean"}},{"name":"detailed","in":"query","description":"When true, populates the `details` block on each target with\nenrichment data (gene names, structures, sequence, bioactivity, etc.).\nDefaults to false for lightweight listing.","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Target list","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Target catalog entry. Used by both list and detail endpoints.\n\nThe list endpoint returns this with `details` omitted (or populated when\n`?detailed=true`). The detail endpoint always populates `details`.","required":["id","name","vendor_name","catalog_number","url"],"properties":{"catalog_number":{"type":"string","description":"Vendor's catalog/SKU number"},"details":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TargetDetails","description":"Detailed target data. Populated on the detail endpoint and on the\nlist endpoint when `?detailed=true` is passed."}]},"id":{"type":"string","format":"uuid","description":"Unique identifier for the target from the catalog"},"name":{"type":"string","description":"Product name of the target antigen"},"pricing":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TargetPricing","description":"Material pricing for this target, if self-service pricing is available.\nWhen absent, this target requires a custom quote.\n\nThe `type` field discriminates the pricing model:\n- `per_sequence`: fixed cost per sequence\n- `per_broken_lot`: customer pays for the full vendor lot"}]},"uniprot_id":{"type":["string","null"],"description":"UniProt accession for the primary protein component.\nPresent on ~82% of targets; viral/non-standard proteins lack this\n(they have `details.ncbi_id` instead).","example":"Q15116"},"url":{"type":"string","description":"URL to view this target in the catalog web UI","example":"https://targets.adaptyvbio.com/protein/f3b2afd0-f70b-5191-a90a-ae1e0545c744"},"vendor_name":{"type":"string","description":"Vendor/supplier name (e.g., \"ACRO Biosystems\")"}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/targets/request-custom":{"get":{"tags":["targets"],"summary":"List custom target requests","description":"Returns custom target requests for your organization, sorted by creation\ndate (newest first). Supports filtering by status using the `filter`\nparameter (e.g. `filter=eq(status,pending_review)`) and pagination.","operationId":"list_custom_target_requests","parameters":[{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"filter","in":"query","description":"Filter expression in s-expression syntax.\n\n**Comparison:** `eq(field,value)`, `neq(field,value)`, `gt(field,value)`,\n`lt(field,value)`, `gte(field,value)`, `lte(field,value)`,\n`contains(field,substring)`\n\n**Range/set:** `between(field,lo,hi)`, `in(field,v1,v2,...)`\n\n**Logical:** `and(expr1,expr2,...)`, `or(expr1,expr2,...)`, `not(expr)`\n\n**Null checks:** `is_null(field)`, `is_not_null(field)`\n\n**JSONB access:** `at(field,key)` — e.g. `eq(at(metadata,score),42)`\n\n**Cast functions:** `float(expr)`, `int(expr)`, `text(expr)`,\n`timestamp(expr)`, `date(expr)`\n\nExample: `and(gte(created_at,2026-01-01),eq(status,draft))`","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Custom target request list","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Summary information for a custom target request in list views.","required":["id","name","product_id","status","created_at"],"properties":{"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the request was created"},"id":{"type":"string","format":"uuid","description":"Unique identifier for the request"},"name":{"type":"string","description":"Display name of the target"},"product_id":{"type":"string","description":"User-provided product identifier"},"status":{"$ref":"#/components/schemas/CustomTargetRequestStatus","description":"Current review status"}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["targets"],"summary":"Submit custom target request","description":"Submit a new custom target (antigen) for staff review. At least one of\n`sequence` or `pdb_id` must be provided. The `product_id` must be unique\nwithin your organization.\n\nNew requests are created with `pending_review` status. Staff will review\nand approve or reject the request. Approved targets receive a `material_id`\nlinking them to the catalog.","operationId":"create_custom_target_request","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomTargetRequest"}}},"required":true},"responses":{"201":{"description":"Request submitted successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomTargetResponse"}}}},"400":{"description":"Validation failed (missing sequence/pdb, invalid sequence format, duplicate product_id)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/targets/request-custom/{request_id}":{"get":{"tags":["targets"],"summary":"Get custom target request","description":"Retrieves full details of a custom target request, including sequence data,\nPDB information, and metadata. Returns 404 if the request doesn't exist\nor belongs to a different organization.","operationId":"get_custom_target_request","parameters":[{"name":"request_id","in":"path","description":"Unique identifier of the custom target request","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Custom target request details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomTargetRequestInfo"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Request not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/targets/{target_id}":{"get":{"tags":["targets"],"summary":"Get target","description":"Returns the catalog record for a single target by UUID.\n\nRetrieves target details including vendor name, catalog number, and\n`pricing` where self-service pricing is available.\n\n**Pricing:** Targets with `pricing` support instant cost estimation via\n`/experiments/cost-estimate`. Targets without this field require a custom\nquote — contact support for pricing on these targets.\n\nReturns 404 if the target doesn't exist.","operationId":"get_target_info","parameters":[{"name":"target_id","in":"path","description":"Unique identifier of the target to retrieve","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Target details retrieved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TargetInfo"}}}},"400":{"description":"Invalid target_id format (not a UUID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Target not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/tokens":{"get":{"tags":["tokens"],"summary":"List tokens","description":"Returns a flat, paginated list of all tokens (root and attenuated) the\ncaller owns. Each item carries lineage fields (`parent_token_id`,\n`root_token_id`, `kind`) so callers can reconstruct the derivation tree.\nAdmins with global scope see all tokens across all users.","operationId":"list_tokens","parameters":[{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}}],"responses":{"200":{"description":"Token list with lineage","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Unified token record returned by the list endpoint, covering both root\ntokens and their attenuated derivatives. Lineage fields (`parent_token_id`,\n`root_token_id`) allow callers to reconstruct the derivation tree.","required":["id","name","kind","created_at"],"properties":{"attenuation_spec":{"description":"Attenuation restrictions applied to this token. Null for root tokens."},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the token was created."},"expires_at":{"type":["string","null"],"format":"date-time","description":"ISO 8601 timestamp when the token expires. Null means no expiration."},"id":{"type":"string","description":"Token identifier: the root's `token_id` fact for root tokens, or the\n`attenuated_tokens.id` primary key for attenuated tokens."},"kind":{"type":"string","description":"Discriminator: `\"root\"` for tokens in `api_tokens`, `\"attenuated\"` for\ntokens in `attenuated_tokens`.","example":"root"},"name":{"type":"string","description":"Human-readable label."},"parent_token_id":{"type":["string","null"],"description":"Immediate parent in the derivation chain. Null for root tokens."},"revoked_at":{"type":["string","null"],"format":"date-time","description":"ISO 8601 timestamp when the token was revoked. Null if still active."},"root_token_id":{"type":["string","null"],"description":"The family root's `token_id`. Null for root tokens themselves."},"token_type":{"type":["string","null"],"description":"Token variant (only for root tokens): `admin`, `user`,\n`admin_impersonating`, `multi_org`. Null for attenuated tokens."}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/tokens/attenuate":{"post":{"tags":["tokens"],"summary":"Attenuate token","description":"Add restrictions to an existing token without needing the private key.\n\nAttenuation is a Biscuit cryptographic feature that allows anyone holding\na token to add restriction blocks. The resulting token can only do a\nsubset of what the original token could do—restrictions cannot be removed.\n\n# Security Model\n\nThis endpoint requires the `token:read` capability.\n\n1. **Cryptographic guarantee**: Attenuation can only *reduce* permissions,\n   never expand them. The Biscuit format ensures restriction blocks are\n   append-only and cannot be removed without invalidating the signature.\n\n2. **Self-service by design**: Users can only attenuate tokens they already\n   possess. If you have a token, you can create a weaker version of it—this\n   is analogous to being able to share read-only access to something you own.\n\n3. **No privilege escalation**: The attenuated token inherits all existing\n   restrictions from the source token, plus any new ones added. A read-only\n   token cannot be attenuated into a read-write token.\n\n# Use Cases\n\n- Create a read-only version of your token for delegation\n- Restrict a token to specific resources or actions\n- Generate time-limited tokens for external services\n- Scope a broad token down to a single organization or project\n\n# Timezone\n\nAll timestamps use UTC in ISO 8601 / RFC 3339 format.\n\n# Further Reading\n\nFor the cryptographic properties of token attenuation, see the\n[Biscuit documentation](https://doc.biscuitsec.org/).","operationId":"attenuate_token","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttenuateTokenRequest"}}},"required":true},"responses":{"201":{"description":"Token attenuated and persisted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttenuateTokenResponse"}}}},"400":{"description":"Invalid token format or attenuation spec","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-action":"Read"}},"/api/v1/tokens/revoke":{"post":{"tags":["tokens"],"summary":"Revoke the calling token and its entire lineage","description":"Revokes the root token used to authenticate this request and all of its\nattenuated descendants, making the entire token family invalid for future\nAPI calls.\n\n**Idempotent**: If the token family is already revoked, returns the\noriginal revocation timestamp and `children_revoked: 0`.\n\n**Requires token:revoke capability.**","operationId":"revoke_token","responses":{"200":{"description":"Token family revoked successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeTokenResponse"}}}},"403":{"description":"Insufficient permissions - token:revoke capability required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Token not found in database (stateless token)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/updates":{"get":{"tags":["updates"],"summary":"List updates","description":"Returns the experiment update feed, newest first: status changes,\nprogress markers, and completion events. Use `limit` and `offset`\nquery parameters for pagination. Filter by experiment or type using\nthe `filter` parameter, e.g. `filter=eq(experiment_id,<uuid>)` or\n`filter=in(experiment_id,uuid1,uuid2)` or `filter=eq(type,status_change)`.","operationId":"list_updates","parameters":[{"name":"limit","in":"query","description":"Maximum number of items to return (1-100, default 50).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"offset","in":"query","description":"Number of items to skip (default 0).","required":false,"schema":{"type":"integer","format":"int64"}},{"name":"filter","in":"query","description":"Filter expression in s-expression syntax.\n\n**Comparison:** `eq(field,value)`, `neq(field,value)`, `gt(field,value)`,\n`lt(field,value)`, `gte(field,value)`, `lte(field,value)`,\n`contains(field,substring)`\n\n**Range/set:** `between(field,lo,hi)`, `in(field,v1,v2,...)`\n\n**Logical:** `and(expr1,expr2,...)`, `or(expr1,expr2,...)`, `not(expr)`\n\n**Null checks:** `is_null(field)`, `is_not_null(field)`\n\n**JSONB access:** `at(field,key)` — e.g. `eq(at(metadata,score),42)`\n\n**Cast functions:** `float(expr)`, `int(expr)`, `text(expr)`,\n`timestamp(expr)`, `date(expr)`\n\nExample: `and(gte(created_at,2026-01-01),eq(status,draft))`","required":false,"schema":{"type":"string"}},{"name":"sort","in":"query","description":"Sort expression. Supports multi-column sort (comma-separated, up to 8),\nJSONB path access, and type casts.\n\nThree accepted surface forms produce the same ordering:\n\n- **Canonical** — `desc(field)` / `asc(field)`; also wraps casts and\n  `at(...)` JSONB path access. Example: `asc(date(at(metadata,start_date)))`.\n- **Prefix short form** — `-field` (descending), `+field` or bare\n  `field` (ascending). GitHub / Stripe / Jira convention. Bare field\n  only; no casts.\n- **Suffix short form** — `field:asc` / `field:desc`. Also bare field.\n\nExamples: `-created_at`, `-created_at,+name`, `created_at:desc`,\n`desc(created_at),asc(name)`, `asc(at(metadata,score))`.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Update list","content":{"application/json":{"schema":{"type":"object","description":"Paginated list response with offset-based navigation metadata.\n\nAll list endpoints return this shape. Use `offset` and `limit` query\nparameters to page through results; `total` reports how many items\nmatch the query across all pages.","required":["items","total","count","offset"],"properties":{"count":{"type":"integer","format":"int64","description":"Number of items in this response."},"items":{"type":"array","items":{"type":"object","description":"Summary of a single update event","required":["id","experiment_id","experiment_code","name","timestamp"],"properties":{"experiment_code":{"type":"string","description":"Experiment code for grouping and display"},"experiment_id":{"type":"string","format":"uuid","description":"ID of the experiment that generated this update"},"id":{"type":"string","description":"Unique identifier for this update event (UUID)"},"name":{"type":"string","description":"Name/title of the update"},"timestamp":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the update occurred"}}},"description":"The page of results."},"offset":{"type":"integer","format":"int64","description":"Offset used for this page (mirrors the `offset` query parameter)."},"total":{"type":"integer","format":"int64","description":"Total number of items matching the query (across all pages)."}}}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/v1/webhooks":{"get":{"tags":["webhooks"],"summary":"List the organization's webhook registrations.","operationId":"list_webhooks","responses":{"200":{"description":"Registered webhooks for your organization","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrgWebhook"}}}}},"401":{"description":"Missing or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks webhook:list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"bearer_auth":[]}]},"post":{"tags":["webhooks"],"summary":"Register a webhook for the organization.","operationId":"create_webhook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrgWebhookRequest"}}},"required":true},"responses":{"201":{"description":"Webhook registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgWebhook"}}}},"400":{"description":"Invalid URL or secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks webhook:create","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"bearer_auth":[]}]}},"/api/v1/webhooks/{webhook_id}":{"delete":{"tags":["webhooks"],"summary":"Remove a webhook registration.","operationId":"delete_webhook","parameters":[{"name":"webhook_id","in":"path","description":"Registration id to delete","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Registration deleted"},"401":{"description":"Missing or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks webhook:delete","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such registration in your organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"bearer_auth":[]}]},"patch":{"tags":["webhooks"],"summary":"Modify a webhook registration, including rotating or clearing its secret.","operationId":"update_webhook","parameters":[{"name":"webhook_id","in":"path","description":"Registration id to modify","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrgWebhookRequest"}}},"required":true},"responses":{"200":{"description":"Updated registration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgWebhook"}}}},"400":{"description":"Invalid URL or secret, a malformed body, or `Content-Type: application/json` declared with no body sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Token lacks webhook:update","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such registration in your organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"415":{"description":"No body was sent, or a body was sent under a content type other than `application/json`. A JSON body is required; every field is optional, so send `{}` if you have nothing to change","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Well-formed JSON that does not fit `UpdateOrgWebhookRequest`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"bearer_auth":[]}]}},"/api/v1/whoami":{"get":{"tags":["whoami"],"summary":"Who am I","description":"Identify yourself from your API token. Returns the organizations your token\ncan access — with the organization you are currently acting as marked\n`active: true` — along with your user id, the permissions your token grants,\nand when the token expires.\n\nThis is a safe, read-only call: it does not create or change anything. Use\nit to look up your organization's name and id (needed by other endpoints)\nor to check what your token is allowed to do.","operationId":"whoami","responses":{"200":{"description":"Caller identity and access summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhoAmIResponse"}}}},"401":{"description":"Invalid or missing authentication token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Insufficient permissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"x-required-action":"Read"}}},"components":{"schemas":{"AAString":{"type":"string","description":"A validated amino acid sequence string.\n\nContains only the 20 natural amino acids (single-letter codes: A, C, D, E,\nF, G, H, I, K, L, M, N, P, Q, R, S, T, V, W, Y) and colons for multichain\nnotation. Input is case-insensitive; stored uppercase.\n\nMultichain sequences use colon separators (e.g., `MVLS:EVQL` for a\ntwo-chain construct). Invalid characters or empty strings are rejected.","example":"EVQLVESGGGLVQPGGSLRLSCAAS"},"AffinityReplicate":{"type":"object","description":"One replicate of a binding-kinetics measurement for an antibody–antigen pair.\n\nAn affinity result aggregates one or more replicates; each carries its own\nfitted kinetics and the model used to fit them.","required":["replicate"],"properties":{"binding":{"type":["string","null"],"description":"Categorical binding outcome for this replicate (\"true\", \"false\",\n\"unknown\", \"in_progress\")."},"binding_strength":{"type":["string","null"],"description":"Qualitative binding call for this replicate (e.g. \"strong\", \"medium\",\n\"weak\", \"none\", \"in_progress\")."},"confidence":{"type":["string","null"],"description":"Confidence grade for the fit. Omitted when the binding call is unknown."},"expression":{"type":["string","null"],"description":"Expression level for the construct in this replicate (e.g. \"high\",\n\"medium\", \"low\")."},"fit_quality":{"type":["string","null"],"description":"Overall fit quality, one of \"good\" / \"medium\" / \"poor\".","example":"good"},"kd":{"type":["number","null"],"format":"double","description":"Equilibrium dissociation constant (M). Omitted when not measurable."},"kd_app":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/KineticInterval","description":"Apparent KD (M), comparable across binding models, with its 95% CI."}]},"koff":{"type":["number","null"],"format":"double","description":"Dissociation rate constant (s⁻¹). Omitted when not measurable."},"koff_1to1":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/KineticInterval","description":"Dissociation rate (s⁻¹) from the 1:1 (standard) model, with its 95% CI."}]},"koff_method":{"type":["string","null"],"description":"Fitting model used for the koff fit (e.g. \"1:1\", \"2:1\")."},"kon":{"type":["number","null"],"format":"double","description":"Association rate constant (M⁻¹s⁻¹). Omitted when not measurable."},"kon_1to1":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/KineticInterval","description":"Association rate (M⁻¹s⁻¹) from the 1:1 (standard) model, with its 95% CI."}]},"kon_method":{"type":["string","null"],"description":"Fitting model used for the kon fit (e.g. \"1:1\", \"2:1\")."},"method":{"type":["string","null"],"description":"Fitting model used for this replicate (e.g. \"1:1\", \"2:1\")."},"replicate":{"type":"integer","format":"int64","description":"Replicate number, starting at 1."},"rmse_max_signal_pct":{"type":["number","null"],"format":"double","description":"RMSE divided by the maximum measured signal, expressed as a percentage.","example":1.2}}},"AffinityResult":{"type":"object","description":"Aggregated binding results for a single antibody–antigen pair.\n\nReports mean kinetics (KD, kon, koff) across the replicates alongside an\noverall binding call and the individual per-replicate measurements.","required":["sequence","kd_units","binding_strength","positive_control","performance","replicates"],"properties":{"binding":{"type":["string","null"],"description":"Overall binding outcome, rolled up from the replicates (\"true\", \"false\",\n\"unknown\", \"in_progress\")."},"binding_model":{"type":["array","null"],"items":{"type":"string"},"description":"The selected binding model(s), e.g. `[\"standard\"]`, `[\"bivalent_analyte\"]`,\n`[\"conformational\"]`. Populated only for results carrying the v1 payload.","example":["standard"]},"binding_strength":{"type":"string","description":"Overall binding call, rolled up from the replicates (e.g. \"strong\",\n\"medium\", \"weak\", \"none\", \"unknown\")."},"concentration_display":{"type":["string","null"],"description":"Expression concentration for display, in nM (e.g. \"12.3\", \"<5.0\", \">100\")."},"concentration_value":{"type":["number","null"],"format":"double","description":"Expression concentration as a number, in nM, when a point estimate is\navailable."},"expression":{"type":["string","null"],"description":"Expression level for the construct (e.g. \"high\", \"medium\", \"low\")."},"fit_quality":{"type":["string","null"],"description":"Overall fit quality, one of \"good\" / \"medium\" / \"poor\".","example":"good"},"kd_app":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/KineticInterval","description":"Apparent KD (M), comparable across binding models, with its 95% CI."}]},"kd_log_std":{"type":["number","null"],"format":"double","description":"Log-scale standard deviation of KD across the replicates."},"kd_mean":{"type":["number","null"],"format":"double","description":"Mean equilibrium dissociation constant (M), averaged across the\nstrong-binding replicates."},"kd_units":{"type":"string","description":"Unit for the KD values; always molar (M)."},"koff_1to1":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/KineticInterval","description":"Dissociation rate (s⁻¹) from the 1:1 (standard) model, with its 95% CI."}]},"koff_log_std":{"type":["number","null"],"format":"double","description":"Log-scale standard deviation of koff across the replicates."},"koff_mean":{"type":["number","null"],"format":"double","description":"Mean dissociation rate constant (s⁻¹) across the replicates."},"kon_1to1":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/KineticInterval","description":"Association rate (M⁻¹s⁻¹) from the 1:1 (standard) model, with its 95% CI."}]},"kon_log_std":{"type":["number","null"],"format":"double","description":"Log-scale standard deviation of kon across the replicates."},"kon_mean":{"type":["number","null"],"format":"double","description":"Mean association rate constant (M⁻¹s⁻¹) across the replicates."},"method":{"type":["array","null"],"items":{"type":"string"},"description":"Distinct fitting models used across the replicates (e.g. [\"1:1\", \"2:1\"])."},"performance":{"type":"object","description":"How this result compares to each of the experiment's positive controls,\nkeyed by control name. Each value is \"better\", \"worse\", or null when the\ncomparison is indeterminate.","additionalProperties":{"type":["string","null"]},"propertyNames":{"type":"string"},"example":{"Positive Control":"better"}},"place":{"type":["integer","null"],"format":"int64","description":"Rank of this result within its experiment."},"positive_control":{"type":"boolean","description":"Whether this result is a positive control."},"replicates":{"type":"array","items":{"$ref":"#/components/schemas/AffinityReplicate"},"description":"The individual replicate measurements behind these aggregates."},"rmse_max_signal_pct":{"type":["number","null"],"format":"double","description":"RMSE divided by the maximum measured signal, expressed as a percentage.","example":1.2},"sequence":{"$ref":"#/components/schemas/SequenceEntry","description":"The antibody sequence that was evaluated."},"target":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TargetReference","description":"The experiment's resolved target — display name plus, for catalog\ntargets, the `target_catalog_id` to fetch full details via\n`GET /targets/{target_catalog_id}`. Absent when the experiment records no\ntarget (e.g. a custom target with no catalog match)."}]}}},"AssayCost":{"type":"object","description":"Assay cost breakdown for a specific experiment type.\n\n# Pricing Formula\n\n```text\nper_sequence_cost = unit_price + max(0, n_replicates - 1) × replicate_price\nsubtotal = sequence_count × per_sequence_cost\n```\n\nThe first replicate is included in the base unit price; additional replicates\nare charged at `replicate_price` each. For experiment types without replicate\npricing (e.g., thermostability), `replicate_price_cents` is 0.","required":["experiment_type","sequence_count","n_replicates","unit_price_cents","replicate_price_cents","subtotal_cents"],"properties":{"experiment_type":{"type":"string","description":"Experiment type (e.g., \"screening\", \"affinity\", \"thermostability\",\n\"fluorescence\", \"expression\")","example":"screening"},"n_replicates":{"type":"integer","format":"int32","description":"Number of technical replicates","example":3,"minimum":0},"replicate_price_cents":{"type":"integer","format":"int64","description":"Price per additional replicate (beyond first) in USD cents.\nZero for experiment types without replicate pricing.","example":2900},"sequence_count":{"type":"integer","format":"int32","description":"Number of sequences in the experiment","example":5,"minimum":0},"subtotal_cents":{"type":"integer","format":"int64","description":"Subtotal for assay costs in USD cents.\nCalculated as: `sequence_count × (unit_price + max(0, n_replicates - 1) × replicate_price)`","example":103500},"unit_price_cents":{"type":"integer","format":"int64","description":"Base price per sequence in USD cents.\nIncludes the first replicate.","example":14900}}},"AttenuateTokenRequest":{"type":"object","description":"Request to attenuate (restrict) an existing token.\n\nAttenuation is a Biscuit cryptographic feature that adds restriction blocks\nwithout needing the private signing key. Any authenticated user can attenuate\ntheir tokens to create limited-scope versions for delegation.","required":["token","attenuation","name"],"properties":{"attenuated_parent_token_id":{"type":["string","null"],"format":"uuid","description":"If attenuating an already-attenuated token (chained attenuation),\nprovide the `id` of the parent attenuated token record. Omit when\nattenuating a root token directly."},"attenuation":{"$ref":"#/components/schemas/AttenuationSpec","description":"Restrictions to apply to the token."},"name":{"type":"string","description":"Human-readable label for this attenuated token.\n\nNames are not unique — they are purely for display purposes."},"token":{"type":"string","description":"Existing token string (format: `abs0_{slug}{biscuit_base64}`) to attenuate."}}},"AttenuateTokenResponse":{"type":"object","description":"Response after attenuating a token.","required":["token","id"],"properties":{"id":{"type":"string","description":"Database ID of the attenuated token record (for revocation/management)."},"token":{"type":"string","description":"The attenuated token string. Format: `abs0_{slug}{biscuit_base64}`."}}},"AttenuationSpec":{"type":"object","description":"Restriction options for token attenuation.\n\nCreate limited-scope versions of your token for delegation or integration\nwith external services. All restrictions are additive—the resulting token\ncan only perform a subset of what the original could do.\n\n# Quick Options\n\nFor common use cases, use the convenience flags:\n- `read_only: true` — Restrict to read operations only (list, read)\n- `non_destructive: true` — Allow all operations except delete\n\n# Fine-Grained Control\n\nFor precise control, use explicit whitelists:\n- `allowed_actions` — Permit only specific operations\n- `allowed_resources` — Permit only specific resource types\n\n# Timezone\n\nAll timestamps use UTC in ISO 8601 / RFC 3339 format.\n\n# Examples\n\n**Read-only delegation** for a reporting dashboard:\n```json\n{\n  \"token\": \"abs0_FLOWBIO1...\",\n  \"attenuation\": { \"read_only\": true }\n}\n```\n\n**Time-limited token** expiring in 1 hour:\n```json\n{\n  \"token\": \"abs0_FLOWBIO1...\",\n  \"attenuation\": { \"expires_at\": \"2026-01-20T13:00:00Z\" }\n}\n```\n\n**Restrict to specific actions and resources**:\n```json\n{\n  \"token\": \"abs0_FLOWBIO1...\",\n  \"attenuation\": {\n    \"allowed_actions\": [\"list\", \"read\"],\n    \"allowed_resources\": [\"experiment\", \"result\"]\n  }\n}\n```\n\n**Combine multiple restrictions** (read-only, time-limited, org-scoped):\n```json\n{\n  \"token\": \"abs0_FLOWBIO1...\",\n  \"attenuation\": {\n    \"read_only\": true,\n    \"expires_at\": \"2026-01-21T00:00:00Z\",\n    \"allowed_org_ids\": [\"56a01500-1f17-4908-a6a8-472e349e5733\"]\n  }\n}\n```\n\n# Further Reading\n\nFor details on the cryptographic properties of token attenuation, see the\n[Biscuit documentation](https://doc.biscuitsec.org/).","properties":{"allowed_actions":{"type":["array","null"],"items":{"type":"string"},"description":"Explicit list of permitted operations.\n\nValues: `list`, `read`, `create`, `update`, `delete`, `issue`, `revoke`, `mint_token`\n\nIf specified, only these operations are allowed. Applied alongside `read_only` and\n`non_destructive`; effective permissions are the intersection of all constraints."},"allowed_org_ids":{"type":["array","null"],"items":{"type":"string","format":"uuid"},"description":"Restrict token to only these organization IDs.\n\nThe token must already have access to these orgs; attenuation cannot expand\naccess. The token's append-only property enforces this cryptographically."},"allowed_resources":{"type":["array","null"],"items":{"type":"string"},"description":"Explicit list of permitted resource types.\n\nCommon values: `experiment`, `sequence`, `target`, `result`, `project`, `organization`, `user`, `token`, `webhook`, `update`, `quote`, `invoice`, `billing`, `feedback`, `partner`, `credit`, `usage`, `product`, `info`, `admin`, `policy`\n\nIf specified, the token can only access these resource types."},"expires_at":{"type":["string","null"],"format":"date-time","description":"Shorten token expiration to this timestamp (cannot extend past original expiry).\n\nFormat: ISO 8601 / RFC 3339 (e.g., \"2026-01-20T00:00:00Z\")"},"non_destructive":{"type":"boolean","description":"Allow all operations except destructive ones (delete).\n\nWhen enabled, the token can list, read, create, and update but not delete."},"payment_policy":{"type":["array","null"],"items":{"type":"string"},"description":"Restrict which payment rails the token may be offered.\n\nValues: `invoice`, `machine`, `both` — each naming a rail set the\nattenuation permits. Multiple entries are the sets `query_all` unions\nacross the block; the extractor intersects their bounds, so the\neffective policy is the authority block's rail set intersected with what\nthese permit.\n\nLike every other field here this only narrows. A token issued for the\nlegacy invoice flow cannot attenuate itself into machine payment: the\ncapability lives in the token's sealed authority block, and an appended\nblock can only intersect with it. Restricting a `machine` or `both`\ntoken to `invoice` is the useful direction — it hands a delegate a token\nthat can read and act but never open a machine-payment session."},"read_only":{"type":"boolean","description":"Restrict to list and read operations only.\n\nWhen enabled, the token cannot create, update, or delete resources."}}},"Bioactivity":{"type":"object","description":"Vendor bioactivity data availability for a target.\n\nEach boolean indicates whether the vendor provides binding data\nfrom that assay type. `None` for this field means the vendor\ndid not report bioactivity for this product (distinct from all-false,\nwhich would mean tested but negative).","required":["elisa","spr","bli"],"properties":{"bli":{"type":"boolean","description":"BLI (biolayer interferometry) data available from vendor"},"elisa":{"type":"boolean","description":"ELISA bioactivity data available from vendor"},"spr":{"type":"boolean","description":"SPR (surface plasmon resonance) data available from vendor"}}},"ConfirmQuoteRequest":{"type":"object","description":"Request payload for accepting a quote\n\nUsed when a customer decides to accept a quoted price.","properties":{"notes":{"type":["string","null"],"description":"Reserved for future use. Currently accepted but not acted upon.","example":"Please expedite processing"},"purchase_order_number":{"type":["string","null"],"description":"Purchase order number from your organization (optional)","example":"PO-2026-00142"}}},"ConfirmQuoteResponse":{"type":"object","description":"Response after accepting a quote\n\nConfirms the quote acceptance and provides invoice information if applicable.","required":["id","status"],"properties":{"hosted_invoice_url":{"type":["string","null"],"description":"Stripe-hosted URL where the customer can view and pay the generated invoice","example":"https://invoice.stripe.com/i/acct_1234/test_5678"},"id":{"type":"string","description":"Quote ID that was accepted"},"invoice_id":{"type":["string","null"],"description":"ID of the invoice generated from this quote (if applicable)"},"payment":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PaymentLink","description":"Hypermedia pointer to where and how to pay the finalized (open, unpaid)\ninvoice. Present on the non-settling confirm response — `hosted_invoice`\nfor an async-invoice quote, `machine` for a machine rail. Absent on\nother uses of this DTO (e.g. an already-settled response)."}]},"status":{"$ref":"#/components/schemas/StripeQuoteStatus","description":"New status (accepted after confirmation)"}}},"CostBreakdown":{"type":"object","description":"Complete cost breakdown for an experiment.\n\nAggregates assay costs and optional material costs into a total estimate.\nUsed for both cost estimation endpoint responses and dynamic experiment costs.\nAll amounts are in USD cents and **exclude VAT**.","required":["pricing_version","assay","total_cents"],"properties":{"assay":{"$ref":"#/components/schemas/AssayCost","description":"Assay-related costs"},"materials":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/MaterialCost","description":"Material costs for target antigens.\n\nPresent only for binding experiments (screening, affinity) that have an\nassociated target. Non-binding experiments (thermostability, fluorescence,\nexpression) do not incur material costs and this field is omitted."}]},"pricing_version":{"$ref":"#/components/schemas/String","description":"Pricing version applied (e.g., \"v1_2026-01-20\")"},"total_cents":{"type":"integer","format":"int64","description":"Total estimated cost in USD cents","example":106000}}},"CostEstimateRequest":{"type":"object","description":"Request payload for cost estimation.\n\nContains the same experiment specification fields as creation,\nbut does not require a name or project association.\n\n# Example\n\n```json\n{\n  \"experiment_spec\": {\n    \"experiment_type\": \"screening\",\n    \"method\": \"bli\",\n    \"target_id\": \"019a03da-b87f-7e15-8b02-cef171c9871d\",\n    \"sequences\": {\n      \"seq1\": \"EVQLVESGGGLVQPGGSLRLSCAASGFTFS...\",\n      \"seq2\": \"MKTLVLLALLVGAALA...\"\n    },\n    \"n_replicates\": 3\n  }\n}\n```","required":["experiment_spec"],"properties":{"experiment_spec":{"$ref":"#/components/schemas/ExperimentSpec","description":"Experiment specification for cost calculation (see ExperimentSpec for field details)"}}},"CostEstimateResponse":{"type":"object","description":"Response wrapper for cost estimation endpoint.\n\nContains either a complete breakdown (when all pricing is available) or an\nincomplete estimate (when target lacks self-service pricing).","properties":{"breakdown":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/CostBreakdown","description":"Detailed cost breakdown. Present when pricing is fully available."}]},"incomplete":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/IncompleteCostEstimate","description":"Partial estimate. Present when target lacks self-service pricing."}]},"warnings":{"type":"array","items":{"type":"string"},"description":"Warnings about the estimate (e.g., unknown target, pricing limitations)"}}},"CreateCustomTargetRequest":{"type":"object","description":"Request payload for submitting a custom target with sequence or PDB data.\n\nUsers can submit their own targets for review. At least one of `sequence`\nor `pdb_id` must be provided. Approved targets are added to the catalog\nand can be used in experiments.","required":["name","product_id"],"properties":{"molecular_weight":{"type":["number","null"],"format":"double","description":"Molecular weight in kilodaltons (kDa)","example":15.5},"name":{"type":"string","description":"Display name for the target","example":"My Custom Antigen"},"note":{"type":["string","null"],"description":"Additional notes or context about the target","example":"Expressed in HEK293 cells"},"pdb_file":{"type":["string","null"],"description":"Base64-encoded PDB file content for custom structures not in the PDB"},"pdb_id":{"type":["string","null"],"description":"PDB database identifier (e.g., \"1ABC\", \"6LU7\")","example":"1HHO"},"product_id":{"type":"string","description":"User-provided identifier, must be unique within your organization","example":"CUSTOM-001"},"product_url":{"type":["string","null"],"description":"URL to vendor product page or documentation","example":"https://www.acrobiosystems.com/product/PD1"},"sequence":{"type":["string","null"],"description":"Amino acid sequence using standard single-letter codes (A-Z excluding B, J, X, Z).\nSupports standard 20 amino acids plus selenocysteine (U) and pyrrolysine (O).","example":"MVLSPADKTNVKAAWGKVGAHAGEYGAEALERMFLSFPTTKTYFPHFDLSH"},"vendor":{"type":["string","null"],"description":"Source vendor name if applicable","example":"ACRO Biosystems"}}},"CreateCustomTargetResponse":{"type":"object","description":"Response after submitting a custom target request.","required":["id","status","created_at"],"properties":{"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the request was created"},"id":{"type":"string","format":"uuid","description":"Unique identifier assigned to the request"},"status":{"$ref":"#/components/schemas/CustomTargetRequestStatus","description":"Current status (always \"pending_review\" for new submissions)"}}},"CreateExpRequest":{"type":"object","description":"Request payload for creating a new experiment.\n\nCreated experiments start in Draft status. Use PATCH `/experiments/{id}`\nto modify existing experiments.","required":["name","experiment_spec"],"properties":{"auto_accept_quote":{"type":"boolean","description":"Atomic create-and-pay: accept the quote and initiate payment in the\nsame request.\n\nWhen `true`, after creating the experiment and its Stripe quote the\nserver immediately accepts the quote and finalizes the invoice. It is\n**non-settling for machine rails**: create-and-pay never settles or\nemits a payment `402`; the client settles at `/invoices/{id}/pay`.\n\n- **No header / `async_invoice`** — the quote is accepted and a draft\n  invoice is created (today's behaviour). Responds `201 Created` with\n  `stripe_invoice_id` and `stripe_hosted_invoice_url`.\n- **A machine rail (`mpp-spt` / `x402-exact`)** — the (open, unpaid)\n  invoice is finalized and the response is `200 OK` with the accepted-quote\n  body plus a HATEOAS `payment` pointer to `/invoices/{invoice_id}/pay`.\n  Follow it to answer the `402` challenge and settle.\n\nOn a failure after the invoice was created, the invoice is\ncompensated (voided) so the experiment can be retried. Implies\n`skip_draft: true`. A quote must be creatable (target_id + full pricing\navailable), otherwise the request is rejected with `400`.","default":false},"experiment_spec":{"$ref":"#/components/schemas/ExperimentSpec","description":"Structured experiment definition (type, target, sequences, parameters)"},"name":{"type":"string","description":"Human-readable name for the experiment","example":"PD-L1 affinity panel"},"payment":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PaymentSelector","description":"Optional create-time payment-method selector — the request-body\nequivalent of the `X-Adaptyv-Payment-Method` header.\n\nAbsent (the default) selects nothing here; the method is then resolved\nfrom the header, or from the caller's payment policy, exactly as before.\nWhen present it pins the experiment's payment rail (and Stripe account\ntier) for its whole lifetime. If the header is *also* present, the two\nmust agree, otherwise the request is rejected `400`\n(`payment_method_selector_conflict`)."}]},"skip_draft":{"type":"boolean","description":"Bypass Draft status and submit directly for processing.\n\nWhen `true`, the experiment is created in \"Waiting for confirmation\"\nstatus instead of Draft, skipping the manual review step. Use this for\nautomated pipelines with pre-validated payloads.\n\nWhen an experiment is submitted with `skip_draft: true` and the target\nhas existing inventory materials, those materials are automatically\nlinked to expedite processing."},"webhook_secret":{"type":["string","null"],"description":"HMAC secret this experiment's deliveries are signed with. Write-only:\nit is never returned by any endpoint, so store your copy when you set it.\n\nOmit it to inherit your organization's secret (see `/api/v1/webhooks`),\nand omit that too to inherit Adaptyv's deployment-wide secret. Minimum\n32 characters.","example":"a-32-plus-character-shared-secret","writeOnly":true,"minLength":32},"webhook_url":{"type":["string","null"],"description":"HTTPS URL for push notifications. Once set, Adaptyv POSTs an\n`experiment_update` event to this URL for every customer-facing update on\nthe experiment — the same updates that trigger an email notification.\nDeliveries are signed with `X-Adaptyv-Signature` (HMAC-SHA256).","example":"https://example.com/webhook"}}},"CreateExpResponse":{"type":"object","description":"Response confirming experiment creation.","required":["experiment_id"],"properties":{"error":{"type":["string","null"],"description":"Error message when the request fails validation or processing"},"experiment_id":{"type":"string","description":"Unique identifier assigned to the new experiment"},"stripe_hosted_invoice_url":{"type":["string","null"],"description":"Stripe hosted invoice page. When the invoice is unpaid this is the\ncheckout/payment page; after payment it becomes a receipt viewer."},"stripe_invoice_id":{"type":["string","null"],"description":"Stripe invoice ID, present when `auto_accept_quote` created an invoice."}}},"CreateOrgWebhookRequest":{"type":"object","description":"Request to register an organization-level webhook.\n\n# Example\n\n```json\n{\n  \"url\": \"https://example.com/adaptyv/webhook\",\n  \"secret\": \"a-32-plus-character-shared-secret\",\n  \"headers\": {\"X-Tenant-Id\": \"acme\"}\n}\n```","required":["url"],"properties":{"headers":{"description":"Extra HTTP headers to send with each delivery."},"secret":{"type":["string","null"],"description":"HMAC signing secret. Write-only — it is never returned again, so keep\nyour copy. Omit to inherit Adaptyv's deployment-wide secret.\n\nMinimum 32 characters.","example":"a-32-plus-character-shared-secret","writeOnly":true,"minLength":32},"url":{"type":"string","description":"HTTPS URL to deliver to. Must be publicly routable.\n\nReceives the events of every experiment in the organization that has not\nregistered a webhook of its own; an experiment that has one delivers only\nthere, not here as well.","example":"https://example.com/adaptyv/webhook"}}},"CustomTargetRequestInfo":{"type":"object","description":"Full details of a custom target request.\n\nIncludes all submitted data: sequence, PDB information, and metadata.","required":["id","name","product_id","status","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the request was created"},"id":{"type":"string","format":"uuid","description":"Unique identifier for the request"},"material_id":{"type":["string","null"],"description":"Material ID if the request was approved and linked"},"molecular_weight":{"type":["number","null"],"format":"double","description":"Molecular weight in kDa if provided"},"name":{"type":"string","description":"Display name of the target"},"note":{"type":["string","null"],"description":"User notes"},"pdb_file":{"type":["string","null"],"description":"Base64-encoded PDB file if provided"},"pdb_id":{"type":["string","null"],"description":"PDB database identifier if provided"},"product_id":{"type":"string","description":"User-provided product identifier"},"product_url":{"type":["string","null"],"description":"Vendor product URL"},"sequence":{"type":["string","null"],"description":"Amino acid sequence if provided"},"status":{"$ref":"#/components/schemas/CustomTargetRequestStatus","description":"Current review status"},"updated_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when the request was last updated"},"vendor":{"type":["string","null"],"description":"Vendor name"}}},"CustomTargetRequestStatus":{"type":"string","description":"Review status for custom target requests.","enum":["pending_review","approved","rejected"]},"ErrorResponse":{"type":"object","description":"Error response body returned by all endpoints on 4xx/5xx failures.\n\nEvery error response contains a human-readable message and the request ID\nfor support correlation. The `request_id` is also returned in the\n`x-request-id` response header.","required":["error","request_id"],"properties":{"error":{"type":"string","description":"Human-readable error description.","example":"experiment not found"},"request_id":{"type":"string","description":"Request identifier for support correlation (also in `x-request-id` header).","example":"req_019462a4-b1c2-7def-8901-23456789abcd"}}},"ExpInfo":{"type":"object","description":"Detailed information about a specific experiment.\n\nContains all data needed to understand experiment configuration\nand track progress.","required":["id","code","status","experiment_spec","created_at","results_status","experiment_url"],"properties":{"code":{"type":"string","description":"Unique experiment code (e.g., \"EXP-2024-001\") or request ID if not fulfilled"},"costs":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ExperimentCosts","description":"Dynamic cost information for this experiment.\n\nThe value depends on experiment lifecycle and creation date:\n- `null`: Pricing unavailable (experiments created before 2026-01-20)\n- `{type: \"estimate\", ...}`: Computed estimate (no Stripe quote yet)\n- `{type: \"quoted\", ...}`: Stripe quote generated\n- `{type: \"invoiced\", ...}`: Stripe invoice created\n\nResolution: invoice_id present → Invoiced, else quote_id present → Quoted,\nelse compute Estimate from experiment spec."}]},"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp of experiment creation"},"experiment_spec":{"$ref":"#/components/schemas/ExperimentSpecInfo","description":"Unified experiment specification (type, resolved target, sequences, params)"},"experiment_url":{"type":"string","description":"URL to view the experiment in the Foundry portal"},"id":{"type":"string","format":"uuid","description":"Unique identifier for the experiment"},"name":{"type":["string","null"],"description":"Human-readable name for the experiment (nullable in database)"},"results_status":{"$ref":"#/components/schemas/ResultsStatus","description":"Indicates whether results are available for this experiment"},"status":{"$ref":"#/components/schemas/ExperimentStatus","description":"Current lifecycle status of the experiment"},"stripe_invoice_url":{"type":["string","null"],"description":"URL to view/pay invoice"},"stripe_quote_id":{"type":["string","null"],"description":"Stripe quote identifier (e.g. `qt_1Abc2Def`), when a quote exists.\n\nExposed so integrations can call the `/quotes/{quote_id}` family of\nendpoints without parsing the ID out of `stripe_quote_url`. Tracks the\nupstream Stripe quote; changes if the quote is regenerated (rare)."},"stripe_quote_url":{"type":["string","null"],"description":"URL to view quote in payment provider dashboard"}}},"ExperimentConfirmationResponse":{"type":"object","description":"Confirmation response returned after a status transition via `POST /confirm`.\n\nThe confirm endpoint automatically advances the experiment through its lifecycle\nbased on the current status. This response reports both the previous and new\nstatus so callers can verify what transition occurred.","required":["experiment_id","previous_status","status","confirmed_at"],"properties":{"confirmed_at":{"type":"string","format":"date-time","description":"RFC3339 timestamp when confirmation completed","example":"2026-02-15T14:30:00Z"},"experiment_id":{"type":"string","format":"uuid","description":"Experiment identifier","example":"019462a4-b1c2-7def-8901-23456789abcd"},"previous_status":{"$ref":"#/components/schemas/ExperimentStatus","description":"Status before the transition"},"status":{"$ref":"#/components/schemas/ExperimentStatus","description":"Status after the transition"},"stripe_invoice_url":{"type":["string","null"],"description":"Hosted invoice URL from Stripe (available after WaitingForConfirmation → WaitingForMaterials)","example":"https://invoice.stripe.com/i/acct_1234/test_5678"}}},"ExperimentCosts":{"oneOf":[{"type":"object","description":"Computed estimate before Stripe quote generation.","required":["breakdown","type"],"properties":{"breakdown":{"$ref":"#/components/schemas/CostBreakdown","description":"Detailed cost breakdown"},"type":{"type":"string","enum":["estimate"]}}},{"type":"object","description":"Partial estimate when materials pricing is unavailable.\n\nReturned when the experiment references a target that lacks self-service\npricing. Assay costs are always calculable, but the total cannot be\ndetermined without material pricing.","required":["pricing_version","assay","materials_unavailable","total_cents","type"],"properties":{"assay":{"$ref":"#/components/schemas/AssayCost","description":"Assay costs (always calculable)"},"materials_unavailable":{"$ref":"#/components/schemas/MaterialsUnavailable","description":"Explanation for missing materials pricing"},"pricing_version":{"$ref":"#/components/schemas/String","description":"Pricing version applied (e.g., \"v1_2026-01-20\")"},"total_cents":{"default":null},"type":{"type":"string","enum":["incomplete_estimate"]}}},{"type":"object","description":"Stripe quote has been generated.","required":["amount_cents","currency","status","stripe_quote_url","type"],"properties":{"amount_cents":{"type":"integer","format":"int64","description":"Quote amount in smallest currency unit (cents)","example":106000},"currency":{"type":"string","description":"Currency code (ISO 4217)","example":"usd"},"expires_at":{"type":["string","null"],"format":"date-time","description":"Quote expiration timestamp (ISO 8601).\nOmitted if the quote has no expiration.","example":"2026-02-20T12:00:00Z"},"status":{"$ref":"#/components/schemas/StripeQuoteStatus","description":"Quote status from Stripe"},"stripe_quote_url":{"type":"string","description":"Direct link to the quote in the Stripe Dashboard","example":"https://dashboard.stripe.com/quotes/qt_1234567890"},"type":{"type":"string","enum":["quoted"]}}},{"type":"object","description":"Stripe invoice has been created.","required":["amount_cents","currency","status","type"],"properties":{"amount_cents":{"type":"integer","format":"int64","description":"Invoice amount in smallest currency unit (cents)","example":106000},"currency":{"type":"string","description":"Currency code (ISO 4217)","example":"usd"},"status":{"$ref":"#/components/schemas/StripeInvoiceStatus","description":"Invoice status from Stripe"},"stripe_invoice_url":{"type":["string","null"],"description":"Hosted invoice payment URL from Stripe.\nOmitted if not available (e.g., draft invoices).","example":"https://invoice.stripe.com/i/acct_abc/test_xyz"},"type":{"type":"string","enum":["invoiced"]}}}],"description":"Dynamic cost information for an experiment.\n\nRepresents the current cost state based on experiment lifecycle:\n- `Estimate`: Computed from experiment spec (no Stripe quote yet)\n- `Quoted`: Real quote from Stripe (awaiting acceptance)\n- `Invoiced`: Invoice created (payment pending or complete)\n\nResolution order: If `stripe_invoice_url` exists, returns `Invoiced`. Otherwise,\nif `stripe_quote_url` exists, returns `Quoted`. Otherwise computes `Estimate`."},"ExperimentInvoiceResponse":{"type":"object","description":"Response envelope for invoice metadata.","required":["experiment_id"],"properties":{"experiment_id":{"type":"string","description":"Experiment identifier"},"status":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/StripeInvoiceStatus","description":"Invoice status (null when the invoice has not yet been finalized)."}]},"stripe_invoice_url":{"type":["string","null"],"description":"Hosted invoice URL from Stripe"}}},"ExperimentQuoteResponse":{"type":"object","description":"Quote metadata for an experiment.","required":["experiment_id","stripe_quote_url","amount_total","amount_subtotal","currency","status"],"properties":{"amount_subtotal":{"type":"integer","format":"int64","description":"Subtotal amount (in the smallest currency unit)","example":500000},"amount_total":{"type":"integer","format":"int64","description":"Total amount (in the smallest currency unit)","example":500000},"currency":{"type":"string","description":"ISO currency code (e.g., \"usd\")","example":"usd"},"experiment_id":{"type":"string","format":"uuid","description":"Experiment identifier","example":"019462a4-b1c2-7def-8901-23456789abcd"},"expires_at":{"type":["string","null"],"format":"date-time","description":"RFC3339 timestamp for when the quote expires (if provided by Stripe)","example":"2026-03-15T23:59:59Z"},"status":{"$ref":"#/components/schemas/StripeQuoteStatus","description":"Stripe quote status"},"stripe_quote_url":{"type":"string","description":"Quote reference URL (may require provider account access).","example":"https://billing.example.com/quotes/qt_1234567890"},"updated_at":{"type":["string","null"],"format":"date-time","description":"RFC3339 timestamp for the last update time","example":"2026-02-15T14:30:00Z"}}},"ExperimentSpec":{"allOf":[{"$ref":"#/components/schemas/ExperimentSpecCommon"},{"type":"object","properties":{"target_id":{"type":["string","null"],"format":"uuid","description":"UUID of the target antigen from the catalog, required when creating\nbinding experiments (affinity, screening, epitope binning); only catalog\ntargets can be specified. On the experiment read response the resolved\ntarget is returned as `target` (see `target.target_catalog_id`).","example":"019a03da-b87f-7e15-8b02-cef171c9871d"}}}],"description":"Request body for creating an experiment (`POST /experiments`).\n\nRequired fields differ by experiment type. The matrix below is the\nauthoritative contract; submissions that violate it are rejected with a 400\nlisting every problem at once.\n\n* `required` — must be set.\n* `rejected` — must not be set for this type.\n* `optional` / `≥1` / ranges — accepted as stated; the noted default applies\n  when omitted.\n\n| Field                         | Affinity   | Screening | Thermostability | Fluorescence | Expression | EpitopeBinning            | EnzymeActivity |\n|-------------------------------|------------|-----------|-----------------|--------------|------------|---------------------------|----------------|\n| `experiment_type`             | required   | required  | required        | required     | required   | required                  | required       |\n| `method` (`bli` \\| `spr`)     | required   | required  | rejected        | rejected     | rejected   | rejected                  | rejected       |\n| `target_id` (UUID)            | required   | required  | rejected        | rejected     | rejected   | required                  | rejected       |\n| `sequences`                   | ≥1         | ≥1        | ≥1              | ≥1           | ≥1         | multiple of 4, range 4–28 | ≥1             |\n| `n_replicates`                | optional   | optional  | optional        | optional     | optional   | rejected                  | optional       |\n| `antigen_concentrations`      | default `[1000.0, 316.2, 100.0, 31.6, 0.0]` nM | — | — | — | — | — | — |\n| `parameters` (free-form JSON) | optional   | optional  | optional        | optional     | optional   | optional                  | optional       |\n\n# Example\n\n```json\n{\n  \"experiment_type\": \"screening\",\n  \"method\": \"bli\",\n  \"target_id\": \"019a03da-b87f-7e15-8b02-cef171c9871d\",\n  \"sequences\": {\n    \"mAb1\": \"EVQLVESGGGLVQPGGSLRLSCAASGFTFS...\",\n    \"control\": {\"aa_string\": \"MKTLVLLALLV...\", \"control\": true}\n  },\n  \"n_replicates\": 3\n}\n```"},"ExperimentSpecCommon":{"type":"object","description":"Fields an experiment carries on both create and read: the assay type and\nmethod, the sequences under test, the replicate plan, and optional parameters.","required":["experiment_type"],"properties":{"antigen_concentrations":{"type":["array","null"],"items":{"type":"number","format":"double"},"description":"Antigen concentrations to test (in nanoMolar)","example":[1000.0,316.2,100.0,31.6,0.0]},"experiment_type":{"$ref":"#/components/schemas/ExperimentType","description":"Assay type: `affinity`, `screening`, `thermostability`, `fluorescence`, `expression`, `epitope_binning`, or `enzyme_activity`"},"method":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Method","description":"Measurement method (BLI or SPR). Required for binding types (affinity,\nscreening); omitting it for those types is an error. Absent in\nrequests and responses for non-binding types (thermostability,\nfluorescence, expression)."}]},"n_replicates":{"type":["integer","null"],"format":"int32","description":"Number of technical replicates (minimum 1, recommended 3+)","example":3,"minimum":0},"parameters":{"type":["object","null"],"description":"Advanced experiment-specific settings. Most experiments leave this null.\nWhen provided, the object round-trips through the experiment detail with\none addition: the read path merges the `experiment_type` discriminator\ninto it, so a submitted `{\"note\":\"x\"}` reads back as\n`{\"experiment_type\":\"...\",\"note\":\"x\"}`. A user-supplied `experiment_type`\nkey is preserved as-is — the discriminator is only inserted when absent."},"sequences":{"type":"object","description":"Sequences keyed by a human readable name. Accepts either simple amino acid strings\nor rich objects with control flags and metadata.","additionalProperties":{"$ref":"#/components/schemas/SequenceValue"},"propertyNames":{"type":"string"}}}},"ExperimentSpecInfo":{"allOf":[{"$ref":"#/components/schemas/ExperimentSpecCommon"},{"type":"object","properties":{"target":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TargetReference","description":"The experiment's target. Present for binding experiments (affinity,\nscreening, epitope binning); absent for types that don't use a target\n(thermostability, fluorescence, expression, enzyme activity)."}]}}}],"description":"An experiment's specification, as returned by `GET /experiments/{id}`: the\nassay type and method, the sequences under test, the replicate plan, and the\nresolved `target`."},"ExperimentStatus":{"type":"string","description":"Current lifecycle stage of an experiment.\n\nStatus values indicate where an experiment is in the submission-to-results\npipeline. Some transitions require user action (such as accepting a quote),\nwhile others occur automatically as work progresses.","enum":["draft","waiting_for_confirmation","canceled","waiting_for_materials","in_production","quote_sent","in_queue","data_analysis","in_review","done"]},"ExperimentType":{"type":"string","description":"Experiment type determining the assay workflow.","enum":["affinity","screening","thermostability","fluorescence","expression","epitope_binning","enzyme_activity"]},"FeedbackType":{"type":"string","description":"Type of feedback submitted by the caller.","enum":["feature_request","feedback","bug_report"]},"FrameworkRegions":{"type":"object","description":"Constant region framework sequences for FAB antibody format.\n\nBoth `ch` and `cl` are required when the parent metadata has `type: fab`.\n\n# Example\n\n```json\n{\n  \"ch\": \"ASTKGPSVFPLAPSSKSTSGGTAALGCLVK...\",\n  \"cl\": \"RTVAAPSVFIFPPSDEQLKSGTASVVCLL...\"\n}\n```","properties":{"ch":{"type":["string","null"],"description":"Constant heavy chain (CH1) region sequence.\n\nTypically the human IgG1 CH1 domain (~100 amino acids).","example":"ASTKGPSVFPLAPSSKSTSGGTAALGCLVKDYFPEPVTVSWNSGALTSGVHTFPAVLQSSGLYSLSSVVTVPSSSLGTQTYICNVNHKPSNTKVDKKVEPKSC"},"cl":{"type":["string","null"],"description":"Constant light chain (CL) region sequence.\n\nEither kappa (~60% of human antibodies) or lambda (~40%).","example":"RTVAAPSVFIFPPSDEQLKSGTASVVCLLNNFYPREAKVQWKVDNALQSGNSQESVTEQDSKDSTYSLSSTLTLSKADYEKHKVYACEVTHQGLSSPVTKSFNRGEC"}}},"HealthDbResponse":{"type":"object","description":"Deep health probe response including database connectivity.","required":["status","db"],"properties":{"db":{"type":"string","description":"Database connectivity: \"connected\" or \"unreachable\"."},"status":{"type":"string","description":"Service status: \"ok\" or \"degraded\"."}}},"HealthResponse":{"type":"object","description":"Liveness probe response.","required":["status"],"properties":{"status":{"type":"string","description":"Service status: \"ok\" when the service is alive."}}},"IncompleteCostEstimate":{"type":"object","description":"Partial cost estimate when materials pricing is unavailable.\n\nUsed in the `incomplete` field of `CostEstimateResponse` when the target\nis not configured for self-service pricing.","required":["pricing_version","assay","materials_unavailable","total_cents"],"properties":{"assay":{"$ref":"#/components/schemas/AssayCost","description":"Assay costs (always calculable)"},"materials_unavailable":{"$ref":"#/components/schemas/MaterialsUnavailable","description":"Explanation for missing materials pricing"},"pricing_version":{"$ref":"#/components/schemas/String","description":"Pricing version applied (e.g., \"v1_2026-01-20\")"},"total_cents":{"default":null}}},"InvoiceDetailResponse":{"type":"object","description":"Public invoice detail DTO.\n\nRenames `amount_total_cents` to `amount_cents` for consistency with\nthe rest of the billing surface.","required":["id","stripe_invoice_id","status","currency"],"properties":{"amount_cents":{"type":["integer","null"],"format":"int64","description":"Total amount in the invoice's currency, in the smallest unit (e.g. cents).","example":25000},"currency":{"type":"string","description":"Three-letter ISO 4217 currency code.","example":"usd"},"experiment_id":{"type":["string","null"],"description":"Foundry experiment UUID linked to this invoice, if any.","example":"018f4c6b-5678-7abc-def0-0123456789cd"},"hosted_invoice_url":{"type":["string","null"],"description":"Stripe-hosted payment page URL. `None` for draft invoices.","example":"https://invoice.stripe.com/i/acct_xxx/test_YWNjdF8xxx"},"id":{"type":"string","description":"Foundry invoice UUID.","example":"018f4c6b-1234-7abc-def0-0123456789ab"},"status":{"type":"string","description":"Stripe invoice lifecycle state.\n\nOne of `\"draft\"`, `\"open\"`, `\"paid\"`, `\"void\"`, or `\"uncollectible\"`.","example":"open"},"stripe_invoice_id":{"type":"string","description":"Stripe invoice ID (`in_xxx`).","example":"in_1OqLk2LkdIwHu7ix6OboRpXl"},"stripe_quote_id":{"type":["string","null"],"description":"Stripe quote ID that generated this invoice, if any.","example":"qt_1OqLk2LkdIwHu7ix6OboRpXl"}}},"KineticInterval":{"type":"object","description":"A kinetic point estimate together with its 95% confidence interval.\n`ci_low` / `ci_high` are absent when the fit did not yield finite bounds.","required":["value"],"properties":{"ci_high":{"type":["number","null"],"format":"double","description":"Upper bound of the 95% confidence interval."},"ci_low":{"type":["number","null"],"format":"double","description":"Lower bound of the 95% confidence interval."},"value":{"type":"number","format":"double","description":"Point estimate."}}},"LotAllocation":{"type":"object","description":"A single lot within a multi-lot allocation.\n\nWhen an order requires more material than any single lot provides,\nthe greedy allocator combines lots. Each entry records the lot size,\nunit price, and how many of that lot are needed.","required":["lot_size_ug","lot_price_cents","quantity"],"properties":{"lot_price_cents":{"type":"integer","format":"int64","description":"Price of one unit of this lot in USD cents","example":80000},"lot_size_ug":{"type":"number","format":"double","description":"Size of this lot in micrograms","example":200.0},"quantity":{"type":"integer","format":"int32","description":"Number of this lot needed","example":2,"minimum":0}}},"LotPriceTier":{"type":"object","description":"A single lot-price tier within broken-lot pricing.\n\nRepresents one vendor lot size, expressed in terms of how many sequences\nit can serve (derived from `max(1, floor(lot_size_ug / consumption_per_seq))`).","required":["num_sequences","price_usdcent"],"properties":{"num_sequences":{"type":"integer","format":"int32","description":"Maximum sequences this lot can serve","example":33,"minimum":0},"price_usdcent":{"type":"integer","format":"int64","description":"Full lot price in USD cents","example":30000}}},"MaterialCost":{"oneOf":[{"type":"object","description":"Per-sequence pricing: fixed cost per sequence.","required":["target","sequence_count","price_per_sequence_cents","subtotal_cents","type"],"properties":{"price_per_sequence_cents":{"type":"integer","format":"int64","description":"Price per sequence in USD cents","example":500},"sequence_count":{"type":"integer","format":"int32","description":"Number of sequences requiring material","example":5,"minimum":0},"subtotal_cents":{"type":"integer","format":"int64","description":"Subtotal: sequence_count * price_per_sequence_cents","example":2500},"target":{"$ref":"#/components/schemas/TargetReference","description":"The target these materials are for (catalog id + display name)."},"type":{"type":"string","enum":["per_sequence"]}}},{"type":"object","description":"Broken-lot pricing: customer pays for one or more full lots.\n\nThe greedy allocator picks the cheapest combination of vendor lots\nthat covers the required material. Single-lot orders have one entry;\nlarger orders may combine multiple lot sizes.","required":["target","sequence_count","lots","subtotal_cents","type"],"properties":{"lots":{"type":"array","items":{"$ref":"#/components/schemas/LotAllocation"},"description":"Lot allocations covering the order"},"sequence_count":{"type":"integer","format":"int32","description":"Number of sequences in the experiment","example":10,"minimum":0},"subtotal_cents":{"type":"integer","format":"int64","description":"Total material cost in USD cents (sum of lot_price_cents × quantity)","example":30000},"target":{"$ref":"#/components/schemas/TargetReference","description":"The target these materials are for (catalog id + display name)."},"type":{"type":"string","enum":["per_broken_lot"]}}}],"description":"Material cost for target antigens, discriminated by pricing model."},"MaterialsUnavailable":{"type":"object","description":"Explains why materials pricing is unavailable for a target.\n\nReturned as part of an incomplete cost estimate when the target is not\nconfigured for self-service pricing.","required":["target","reason"],"properties":{"reason":{"type":"string","description":"Human-readable explanation for why pricing is unavailable","example":"Human PD-L1 is not yet onboarded to self-service pricing. You'll receive a quote with full price information."},"target":{"$ref":"#/components/schemas/TargetReference","description":"The target that lacks self-service pricing (catalog id + display name)."}}},"Method":{"type":"string","description":"The measurement method (BLI or SPR).\n\nRequired for affinity and screening; must be omitted for all other types.","enum":["bli","spr"]},"ModifyExpRequest":{"type":"object","description":"Request payload for modifying an existing experiment.\n\nAll fields are optional - only provided fields will be updated.\nNote: Some modifications may be restricted based on experiment status.\nModifications are only allowed if the experiment request is not yet fulfilled.\n\nStatus transitions are handled separately via the `/experiments/{id}/confirm`\nendpoint, which performs the appropriate transition based on current state.","properties":{"antigen_concentrations":{"type":["array","null"],"items":{"type":"number","format":"double"},"description":"Update antigen concentrations (only if not fulfilled)"},"description":{"type":["string","null"],"description":"Update experiment description"},"n_replicates":{"type":["integer","null"],"format":"int32","description":"Update replicate count (only if not fulfilled)","minimum":0},"name":{"type":["string","null"],"description":"Update experiment name"},"parameters":{"type":["object","null"],"description":"Update experiment parameters (restrictions may apply, only if not fulfilled)"},"sequences":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SequenceEntry"},"description":"Replace sequence list (only if not fulfilled)"},"target_id":{"type":["string","null"],"description":"Change the experiment's catalog target (only while the experiment is editable).\n\n- omitted: keep the current target\n- `null`: clear the target\n- a catalog product UUID (as returned by `GET /targets`): set the target to that product"},"webhook_url":{"type":["string","null"],"description":"Update the webhook URL for push notifications. The URL receives an\n`experiment_update` event for every customer-facing update on the\nexperiment — the same updates that trigger an email notification.\n\nUnlike every other field here, this one can be changed at any status, so\ndelivery can be repointed after the experiment has been submitted."}}},"ModifyExpResponse":{"type":"object","description":"Response confirming experiment modification.","required":["id","updated"],"properties":{"id":{"type":"string","description":"Experiment identifier that was modified"},"message":{"type":["string","null"],"description":"Optional message, e.g., if modification is not allowed"},"updated":{"type":"boolean","description":"Indicates if any changes were applied"}}},"OrgWebhook":{"type":"object","description":"An organization-level webhook registration as returned to the customer.","required":["id","url","secret_set","headers","enabled","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time","description":"Creation timestamp."},"enabled":{"type":"boolean","description":"When false, this registration receives no deliveries."},"headers":{"description":"Extra HTTP headers sent with each delivery."},"id":{"type":"string","format":"uuid","description":"Registration identifier.","example":"019462a4-b1c2-7def-8901-23456789abcd"},"secret_set":{"type":"boolean","description":"Whether a signing secret is configured for this registration.\n\nThe secret itself is never returned by any endpoint. `false` means\ndeliveries fall back to Adaptyv's deployment-wide secret."},"updated_at":{"type":"string","format":"date-time","description":"Last-modification timestamp."},"url":{"type":"string","description":"HTTPS URL deliveries are POSTed to.","example":"https://example.com/adaptyv/webhook"}}},"PayInvoiceResponse":{"type":"object","description":"Response returned by `POST /invoices/{invoice_id}/pay`.\n\nReports the invoice after the call, so no follow-up `GET` is needed. It is\nreturned for three outcomes, which `status` and `already_paid` tell apart:\na settlement this request performed, an invoice that was already paid, and\n— when no payment-method header was sent — the current state of an invoice\nthat is still unpaid and that this call settled nothing on.\n\nA `200` therefore does **not** by itself mean the invoice is paid. Read\n`status`. The `402` challenge is not this body.","required":["id","status","already_paid"],"properties":{"already_paid":{"type":"boolean","description":"`true` when the call was a no-op because the invoice was already paid\nbefore this request.\n\n`false` otherwise — which covers two different outcomes: this request\nsettled the invoice, or no payment method was selected and the body\nreports the invoice's current state without settling. Read `status` to\ntell those apart; `false` alone does not mean the invoice is paid."},"hosted_invoice_url":{"type":["string","null"],"description":"Stripe-hosted invoice URL, when known.","example":"https://invoice.stripe.com/i/acct_1234/test_5678"},"id":{"type":"string","description":"Stripe invoice ID (`in_xxx`) that was paid.","example":"in_1OqLk2LkdIwHu7ix6OboRpXl"},"settlement_reference":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SettlementReference","description":"Names what settled this invoice — the on-chain transaction for\n`x402-exact`, or the Stripe PaymentIntent for `mpp-spt`.\n\nOnly a response that performed the settlement can carry it, so it is\nabsent when `already_paid` is `true` and absent from the state read of\nan unpaid invoice. Treat it as optional even on a settling response:\nwhen the rail reports nothing this API can name honestly, the field is\nomitted rather than guessed at.\n\nRecord it when you receive it. Repeating the call afterwards answers\n`already_paid` without the reference, and it is not available from any\nother endpoint."}]},"status":{"$ref":"#/components/schemas/StripeInvoiceStatus","description":"Current invoice status after the call. `paid` on success."}}},"PaymentLink":{"type":"object","description":"Hypermedia pointer to where and how to pay the invoice a confirm finalized.\n\n`POST /quotes/{id}/confirm` is categorically non-settling: it finalizes the\n(open, unpaid) invoice and hands back this block instead of settling or\nemitting a payment `402`. Exactly one shape is populated per response,\ndiscriminated by `kind`:\n\n- `hosted_invoice`: async-invoice quotes. Pay via `hosted_invoice_url` (the\n  Stripe-hosted page); `pay_url`/`methods` are absent.\n- `machine`: a machine rail (x402-exact / mpp-spt). POST to `pay_url`\n  (`/api/v1/invoices/{invoice_id}/pay`) to answer the `402` challenge and\n  settle; `methods` lists the accepted rails by their\n  `X-Adaptyv-Payment-Method` spellings.","required":["kind"],"properties":{"hosted_invoice_url":{"type":["string","null"],"description":"Stripe-hosted invoice page (async-invoice only); pay here.","example":"https://invoice.stripe.com/i/acct_1234/test_5678"},"kind":{"$ref":"#/components/schemas/PaymentLinkKind","description":"Which payment surface this block points at."},"methods":{"type":"array","items":{"type":"string"},"description":"Machine-payment rails accepted at `pay_url` (machine rail only), by their\n`X-Adaptyv-Payment-Method` header spellings.","example":["x402-exact","mpp-spt"]},"pay_url":{"type":["string","null"],"description":"Machine-payment endpoint to POST to (machine rail only): answers the\n`402` challenge and settles the invoice.","example":"/api/v1/invoices/in_1OqLk2LkdIwHu7ix6OboRpXl/pay"}}},"PaymentLinkKind":{"type":"string","description":"Which payment surface a confirm [`PaymentLink`] points at.","enum":["hosted_invoice","machine"]},"PaymentSelector":{"type":"object","description":"Create-time payment-method selector, accepted in the request body as a\nbody-side equivalent of the `X-Adaptyv-Payment-Method` header.\n\nSelection only — it never carries a signature or proof. x402/MPP submit\ntheir proof in a standard retry header at `/pay`, so putting a credential\nhere would be meaningless and is not accepted.\n\n# Example\n\n```json\n{ \"method\": \"x402-exact\" }\n```","required":["method"],"properties":{"method":{"type":"string","description":"The payment rail to pin this experiment to for its whole lifetime.\n\nValues use the same vocabulary as the `X-Adaptyv-Payment-Method` header:\n`\"async_invoice\"` (the default Stripe-hosted-invoice rail), `\"mpp-spt\"`,\nor `\"x402-exact\"` (note the spelling — `x402-exact`, never `x402`). An\nunknown value is rejected with `400`. When the header is also present,\nthe two selectors must agree, otherwise the request is rejected `400`\n(`payment_method_selector_conflict`).","example":"x402-exact"}}},"PdbEntry":{"type":"object","description":"A single PDB structure entry pairing an ID with its RCSB download URL.","required":["id","url"],"properties":{"id":{"type":"string","description":"PDB ID from RCSB Protein Data Bank (e.g., \"5JDR\")","example":"5JDR"},"url":{"type":"string","description":"Direct download URL for the PDB file","example":"https://files.rcsb.org/download/5JDR.pdb"}}},"QuoteCreationErrorKind":{"type":"string","description":"Classification of a Stripe quote-creation failure during experiment creation.\n\nReturned in the `SubmitConflictError` response body on\n`POST /experiments/{id}/submit` so callers can distinguish transient failures\n(`rate_limited`, `upstream_server_error`) that are worth retrying from\npermanent ones (`customer_quote_cap`, `missing_customer`, `missing_product`)\nthat need a different action.","enum":["customer_quote_cap","rate_limited","upstream_server_error","missing_customer","missing_product","other"]},"QuoteInfo":{"type":"object","description":"Full quote details including itemized pricing and terms.\n\nReview the line items, totals, and expiration before accepting.","required":["id","quote_number","organization_id","organization_name","line_items","subtotal_cents","tax_cents","total_cents","currency","status","valid_until","created_at","notes","terms_and_conditions","stripe_quote_url"],"properties":{"created_at":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when quote was created"},"currency":{"type":"string","description":"ISO 4217 currency code"},"id":{"type":"string","description":"Unique quote identifier"},"line_items":{"type":"array","items":{"$ref":"#/components/schemas/QuoteLineItem"},"description":"Itemized list of products/services being quoted"},"notes":{"type":"string","description":"Additional notes or special pricing information"},"organization_id":{"type":"string","format":"uuid","description":"Organization ID that this quote is prepared for"},"organization_name":{"type":"string","description":"Name of the organization receiving the quote"},"quote_number":{"type":"string","description":"Human-readable quote number for reference"},"status":{"$ref":"#/components/schemas/StripeQuoteStatus","description":"Current quote status from Stripe"},"stripe_quote_url":{"type":"string","description":"Quote reference URL (may require provider account access).","example":"https://billing.example.com/quotes/qt_1234567890"},"subtotal_cents":{"type":"integer","format":"int64","description":"Sum of all line items before tax, in smallest currency unit (cents)"},"tax_cents":{"type":"integer","format":"int64","description":"Estimated tax amount in smallest currency unit (cents); may be adjusted on final invoice"},"terms_and_conditions":{"type":"string","description":"Legal terms and conditions for this quote"},"total_cents":{"type":"integer","format":"int64","description":"Total quoted amount (subtotal + tax) in smallest currency unit (cents)"},"valid_until":{"type":"string","format":"date-time","description":"ISO 8601 timestamp when quote expires"}}},"QuoteLineItem":{"type":"object","description":"Individual line item on a quote\n\nRepresents a single item or service being quoted.","required":["description","quantity","unit_price_cents","total_cents"],"properties":{"description":{"type":"string","description":"Description of the item or service being quoted"},"quantity":{"type":"integer","format":"int64","description":"Number of units"},"total_cents":{"type":"integer","format":"int64","description":"Total amount for this line item in smallest currency unit (cents)"},"unit_price_cents":{"type":"integer","format":"int64","description":"Price per unit in smallest currency unit (cents)"}}},"QuoteRejectionReason":{"type":"string","description":"Allowed reasons for rejecting a quote.","enum":["price","scope","timeline","budget","other"]},"RejectQuoteRequest":{"type":"object","description":"Request payload for rejecting a quote.\n\nProvide a reason for rejection to help improve future quotes.","required":["reason"],"properties":{"feedback":{"type":["string","null"],"description":"Additional feedback or specific concerns (optional but helpful)","example":"Budget constraints require a 15% reduction"},"reason":{"$ref":"#/components/schemas/QuoteRejectionReason","description":"Primary reason for rejection"}}},"RejectQuoteResponse":{"type":"object","description":"Response after rejecting a quote\n\nConfirms the quote rejection has been recorded.","required":["id","status"],"properties":{"id":{"type":"string","description":"Quote ID that was rejected"},"status":{"$ref":"#/components/schemas/StripeQuoteStatus","description":"New status (canceled after rejection)"}}},"ResultInfo":{"type":"object","description":"A completed experiment result: its summarized measurements and a link to the\nraw data.","required":["id","title","experiment_id","result_type","created_at","summary","metadata"],"properties":{"created_at":{"type":"string","format":"date-time","description":"When this result was generated (ISO 8601)."},"data_package_url":{"type":["string","null"],"description":"Download URL for the raw data package, the same one available in the\nFoundry portal. Omitted when no package is available."},"experiment_id":{"type":"string","format":"uuid","description":"Identifier of the experiment this result belongs to."},"id":{"type":"string","format":"uuid","description":"Identifier for this result."},"metadata":{"type":"object","description":"Additional metadata beyond the summary."},"result_type":{"type":"string","description":"Assay type for this result (e.g. \"affinity\", \"thermostability\")."},"summary":{"type":"array","items":{"$ref":"#/components/schemas/ResultSummary"},"description":"Per-readout measurements, each tagged by assay type."},"title":{"type":"string","description":"Human-readable title for this result."}}},"ResultSummary":{"oneOf":[{"allOf":[{"$ref":"#/components/schemas/AffinityResult","description":"Binding kinetics from an affinity assay."},{"type":"object","required":["result_type"],"properties":{"result_type":{"type":"string","enum":["affinity"]}}}],"description":"Binding kinetics from an affinity assay."},{"allOf":[{"$ref":"#/components/schemas/ThermostabilityResult","description":"Melting-temperature readout from a nanoDSF assay."},{"type":"object","required":["result_type"],"properties":{"result_type":{"type":"string","enum":["thermostability"]}}}],"description":"Melting-temperature readout from a nanoDSF assay."}],"description":"A result readout, tagged by assay type.\n\nThe `result_type` field selects the variant: `affinity` for binding\nkinetics, `thermostability` for melting-temperature readouts."},"ResultsStatus":{"type":"string","description":"Indicates the availability of results for an experiment.","enum":["none","partial","all"]},"RevokeTokenResponse":{"type":"object","description":"Response returned when a token family is revoked.","required":["token_id","revoked_at","children_revoked"],"properties":{"children_revoked":{"type":"integer","format":"int64","description":"Number of attenuated child tokens that were newly revoked in this call.\nWill be 0 on idempotent repeated calls (children already stamped).","example":3},"revoked_at":{"type":"string","format":"date-time","description":"Timestamp when the token was revoked (may be earlier if already revoked)."},"token_id":{"type":"string","description":"The root token_id that was revoked (all descendants are also revoked).","example":"550e8400-e29b-41d4-a716-446655440000"}}},"SequenceAddRequest":{"type":"object","description":"Request body for adding sequences to a draft experiment.\n\nIdentifies the target experiment by its human-readable code (e.g., \"PROJ-001\")\nrather than UUID. The experiment must be in Draft status; appending sequences\nto confirmed experiments returns 409 Conflict.","required":["experiment_code","sequences"],"properties":{"experiment_code":{"type":"string","description":"Human-readable experiment code (e.g., \"PROJ-001\")","example":"PROJ-001"},"sequences":{"type":"array","items":{"$ref":"#/components/schemas/SequenceEntry"},"description":"Sequences to append to the experiment"}}},"SequenceAddResponse":{"type":"object","description":"Response after successfully adding sequences to an experiment.","required":["added_count","experiment_id","experiment_code","sequence_ids"],"properties":{"added_count":{"type":"integer","format":"int32","description":"Number of sequences added","minimum":0},"experiment_code":{"type":"string","description":"Human-readable experiment code"},"experiment_id":{"type":"string","format":"uuid","description":"UUID of the experiment"},"sequence_ids":{"type":"array","items":{"type":"string"},"description":"UUIDs of the newly created sequences"}}},"SequenceEntry":{"type":"object","description":"Represents a biological sequence to be tested in an experiment.\n\nEach sequence represents an antibody or protein to be tested in the experiment.\n\n# Example\n\n```json\n{\n  \"name\": \"mAb1\",\n  \"aa_string\": \"EVQLVESGGGLVQPGGSLRLSCAASGFTFS...\",\n  \"control\": false\n}\n```","required":["aa_string"],"properties":{"aa_string":{"$ref":"#/components/schemas/AAString","description":"Protein/peptide sequence in standard single-letter amino acid format.\n\nValid characters: A,C,D,E,F,G,H,I,K,L,M,N,P,Q,R,S,T,V,W,Y and colon for multichain."},"control":{"type":"boolean","description":"Whether the sequence should be treated as a control well"},"metadata":{"description":"Arbitrary metadata supplied by the customer (tag locations, notes)"},"name":{"type":["string","null"],"description":"Optional name for the sequence (e.g., \"mAb1\", \"scFv-001\")","example":"mAb1"}}},"SequenceExperimentRef":{"type":"object","description":"Reference to the experiment containing a sequence.\n\nProvides context about which experiment the sequence belongs to and\nits current processing status.","required":["experiment_id","experiment_code"],"properties":{"experiment_code":{"type":"string","description":"Human-readable experiment code"},"experiment_id":{"type":"string","format":"uuid","description":"Unique identifier for the experiment"},"experiment_status":{"type":["string","null"],"description":"Current status of the experiment"}}},"SequenceInfo":{"type":"object","description":"Full details for a specific sequence.\n\nIncludes the complete amino acid sequence, metadata, and experiment reference.","required":["id","length","is_control","experiment","created_at"],"properties":{"aa_string":{"type":["string","null"],"description":"Complete amino acid sequence"},"created_at":{"type":"string","format":"date-time","description":"When the sequence was created"},"experiment":{"$ref":"#/components/schemas/SequenceExperimentRef","description":"Experiment containing this sequence"},"id":{"type":"string","format":"uuid","description":"Unique identifier for the sequence"},"is_control":{"type":"boolean","description":"Whether this sequence is marked as a control"},"length":{"type":"integer","format":"int32","description":"Sequence length in amino acids","minimum":0},"metadata":{"type":["object","null"],"description":"Sequence-level annotations from the analysis pipeline.\n\nWhen present, may include chain assignments and framework region\nboundaries for antibody sequences. Structure varies by experiment type."},"name":{"type":["string","null"],"description":"Optional name assigned to the sequence"}}},"SequenceInput":{"type":"object","description":"Rich sequence input with control flag and optional structural metadata.","required":["aa_string"],"properties":{"aa_string":{"$ref":"#/components/schemas/AAString","description":"Protein/peptide sequence in standard single-letter amino acid format.\n\nValid characters: A,C,D,E,F,G,H,I,K,L,M,N,P,Q,R,S,T,V,W,Y and colon for multichain."},"control":{"type":"boolean","description":"Whether the sequence should be treated as a control well"},"metadata":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SequenceMetadata","description":"Structural metadata about the sequence (antibody type, tag location, etc.)"}]}}},"SequenceMetadata":{"type":"object","description":"Structural metadata for antibody sequences.\n\nThe `type` field determines which other fields are required:\n\n| Type | Required | Optional |\n|------|----------|----------|\n| `sc_fv` | `vh`, `vl` | `linker` (defaults to `GGGGSGGGGSGGGGS`), `tag_location` |\n| `fab` | `framework_regions.ch`, `framework_regions.cl` | `tag_location` |\n| `single_chain` | (none) | `tag_location` |\n| `igg` | (none) | `tag_location` |\n\nWhen `type` is omitted, validation is skipped for backwards compatibility.\n\n# Examples\n\nScFv with minimal required fields:\n```json\n{\n  \"type\": \"sc_fv\",\n  \"tag_location\": \"C\",\n  \"vh\": \"EVQLVESGGGLVQPGGSLRLSCAASGFTFS\",\n  \"vl\": \"DIQMTQSPSSLSASVGDRVTITC\"\n}\n```\n\nFAB with explicit constant regions:\n```json\n{\n  \"type\": \"fab\",\n  \"tag_location\": \"N\",\n  \"framework_regions\": {\n    \"ch\": \"ASTKGPSVFPLAPSSKSTSGGTAALGCLVK...\",\n    \"cl\": \"RTVAAPSVFIFPPSDEQLKSGTASVVCLL...\"\n  }\n}\n```","properties":{"VH":{"type":["string","null"],"description":"Variable heavy chain sequence (required for ScFv).","example":"EVQLVESGGGLVQPGGSLRLSCAASGFTFS"},"VL":{"type":["string","null"],"description":"Variable light chain sequence (required for ScFv).","example":"DIQMTQSPSSLSASVGDRVTITC"},"chain_order":{"type":["array","null"],"items":{"type":"string"},"description":"Chain ordering for complex multi-chain formats."},"framework_regions":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/FrameworkRegions","description":"Constant region framework sequences (required for FAB)."}]},"linker":{"type":["string","null"],"description":"Flexible peptide linker connecting VH and VL domains.\n\nIf omitted for ScFv, defaults to `GGGGSGGGGSGGGGS` (Gly₄Ser)₃.","example":"GGGGSGGGGSGGGGS"},"tag_location":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TagLocation","description":"His-tag location for purification.\n\n`N` for N-terminal, `C` for C-terminal."}]},"type":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/SequenceType","description":"Antibody format type determining required fields.\n\nAccepted values: `ScFv`, `FAB`, `SingleChain`, `IgG` (Portal-style).\nAlso accepts: `sc_fv`, `fab`, `single_chain`, `igg` (API-style)."}]}},"additionalProperties":false},"SequenceType":{"type":"string","description":"Antibody format types.\n\nDetermines validation requirements and expected metadata fields.\n\nSerializes in Portal-style naming (`ScFv`, `FAB`, `SingleChain`, `IgG`) for\nconsistency with existing database data. Deserializes both Portal-style and\nAPI-style (`sc_fv`, `fab`, `single_chain`, `igg`) for backwards compatibility.","enum":["ScFv","FAB","SingleChain","IgG"]},"SequenceValue":{"oneOf":[{"$ref":"#/components/schemas/AAString","description":"Simple format: amino acid sequence string"},{"$ref":"#/components/schemas/SequenceInput","description":"Rich format: sequence with optional control flag and metadata"}],"description":"Flexible sequence input for experiment creation.\n\nAccepts a simple amino acid string or a rich object with control flag and metadata."},"SettlementReference":{"oneOf":[{"type":"object","description":"Settled on-chain: an x402 `exact` transfer broadcast by the facilitator.\n\nThe hash is the reconciliation key for this payment and is checkable\nagainst the network without any Adaptyv involvement.","required":["tx_hash","type"],"properties":{"tx_hash":{"type":"string","description":"On-chain transaction hash of the settling transfer.","example":"0x9c2f1a7e5b0d4c3a8f6e2d1b0a9c8f7e6d5c4b3a29180f7e6d5c4b3a29180f7e"},"type":{"type":"string","enum":["x402_transaction"]}}},{"type":"object","description":"Settled through Stripe: the PaymentIntent attached to the invoice.","required":["payment_intent_id","type"],"properties":{"payment_intent_id":{"type":"string","description":"Stripe PaymentIntent ID (`pi_xxx`) credited to the invoice.","example":"pi_3OqLk2LkdIwHu7ix6OboRpXl"},"type":{"type":"string","enum":["stripe_payment_intent"]}}}],"description":"Rail-specific reference to the event that settled the invoice.\n\nRead `type` to tell the two apart. `x402_transaction` carries the on-chain\ntransaction that moved the funds; `stripe_payment_intent` carries the Stripe\nPaymentIntent credited to the invoice. Either identifies this payment\nuniquely, so it can be quoted back to Adaptyv support to resolve a billing\nquestion."},"String":{"type":"string"},"StripeInvoiceStatus":{"type":"string","description":"Invoice status from Stripe's API.","enum":["draft","open","paid","void","uncollectible"]},"StripeQuoteStatus":{"type":"string","description":"Quote status from Stripe's API.","enum":["draft","open","accepted","canceled","stale"]},"SubmitConflictError":{"type":"object","description":"409 response body returned by `POST /experiments/{id}/submit` when the\nexperiment cannot transition out of its current state.\n\nShape:\n\n| Field | When present | Notes |\n|-------|-------------|-------|\n| `error` | always | human-readable message (same shape as `ErrorResponse.error`) |\n| `request_id` | always | correlation id (same shape as `ErrorResponse.request_id`) |\n| `error_kind` | only when a prior `POST /experiments` attempted to create a Stripe quote and Stripe rejected it | discriminator from `QuoteCreationErrorKind` |\n| `hint` | only when `error_kind` is populated | retry / contact guidance |\n\n`error_kind` and `hint` are `null` for 409s unrelated to quote creation\n(e.g. the experiment is already submitted or in a terminal state).","required":["error","request_id"],"properties":{"error":{"type":"string","description":"Human-readable message (matches `ErrorResponse.error`).","example":"Cannot submit draft experiment: Stripe quote creation failed."},"error_kind":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/QuoteCreationErrorKind","description":"Classifier of the quote-creation failure, or `null` when the conflict\nis unrelated to quote creation."}]},"hint":{"type":["string","null"],"description":"Operator-facing hint corresponding to `error_kind`, or `null`.","example":"Stripe rejected the quote: this organization's customer has reached the draft-quote cap. Contact admin to cancel stale test-mode quotes."},"request_id":{"type":"string","description":"Request correlation id (matches `ErrorResponse.request_id`).","example":"req_019462a4-b1c2-7def-8901-23456789abcd"}}},"SubmitFeedbackRequest":{"type":"object","description":"Feedback submission request.","required":["request_uuid","feedback_type"],"properties":{"feedback_type":{"$ref":"#/components/schemas/FeedbackType","description":"Feedback category."},"human_note":{"type":["string","null"],"description":"Human-readable text note (optional, can be combined with json_body).","example":"Got 500 error when creating experiment"},"json_body":{"type":["object","null"],"description":"Structured JSON details (optional, can be combined with human_note)."},"request_uuid":{"type":"string","format":"uuid","description":"UUID of the request that prompted this feedback.","example":"01900abc-1234-7890-1234-567890abcdef"},"title":{"type":["string","null"],"description":"Optional short title. If omitted or blank, a default is used.","example":"Timeout when creating experiment"}}},"SubmitFeedbackResponse":{"type":"object","description":"Feedback submission response.","required":["reference","message"],"properties":{"message":{"type":"string","description":"Confirmation message."},"reference":{"type":"string","description":"Feedback reference for future correspondence.","example":"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"}}},"TagLocation":{"type":"string","description":"His-tag location on the expressed protein.","enum":["N","C"]},"TargetDetails":{"type":"object","description":"Detailed target data populated on the detail endpoint and optionally\non the list endpoint when `?detailed=true` is passed.","properties":{"bioactivity":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Bioactivity","description":"Vendor bioactivity data availability. `None` means the vendor did not\nreport bioactivity (distinct from all-false)."}]},"description":{"type":["string","null"],"description":"Vendor product description."},"expression_system":{"type":["string","null"],"description":"Expression system used to produce the target (e.g., \"HEK293\").","example":"HEK293"},"family":{"type":["string","null"],"description":"Protein family classification (e.g., \"Immunoglobulin superfamily\").","example":"Immunoglobulin superfamily"},"gene_names":{"type":["array","null"],"items":{"type":"string"},"description":"Gene names/symbols. First entry is canonical (e.g., [\"PDCD1\", \"PD1\"]).","example":["PDCD1","PD1"]},"molecular_weight":{"type":["string","null"],"description":"Raw molecular weight string from the vendor catalog (e.g., \"23.1 kDa\").","example":"23.1 kDa"},"ncbi_id":{"type":["string","null"],"description":"NCBI accession ID. Complementary to `uniprot_id` — mostly viral targets.","example":"NP_005009"},"organism":{"type":["string","null"],"description":"Source organism (e.g., \"Homo sapiens\", \"SARS-CoV-2\").","example":"Human"},"purity":{"type":["number","null"],"format":"double","description":"Purity percentage (0-100). Vendor-dependent, only ~20% of targets.","example":95.0},"sequence":{"type":["string","null"],"description":"Amino acid sequence of the target protein."},"sequence_length":{"type":["integer","null"],"format":"int32","description":"Amino acid count (avoids client-side counting of `sequence`).","example":288},"structures":{"type":"array","items":{"$ref":"#/components/schemas/PdbEntry"},"description":"PDB structures with download URLs. Each entry pairs a PDB ID\nwith its RCSB download URL.","example":[{"id":"5JDR","url":"https://files.rcsb.org/download/5JDR.pdb"}]},"subcellular_locations":{"type":["array","null"],"items":{"type":"string"},"description":"Known subcellular locations (e.g., [\"Cell membrane\", \"Secreted\"]).","example":["Cell membrane"]},"synonyms":{"type":["array","null"],"items":{"type":"string"},"description":"Alternative protein names (e.g., [\"Programmed cell death protein 1\", \"CD279\"])."},"tags":{"type":["array","null"],"items":{"type":"string"},"description":"Purification/fusion tags on the product (e.g., [\"His\"], [\"Fc\"]).","example":["His"]}}},"TargetInfo":{"type":"object","description":"Target catalog entry. Used by both list and detail endpoints.\n\nThe list endpoint returns this with `details` omitted (or populated when\n`?detailed=true`). The detail endpoint always populates `details`.","required":["id","name","vendor_name","catalog_number","url"],"properties":{"catalog_number":{"type":"string","description":"Vendor's catalog/SKU number"},"details":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TargetDetails","description":"Detailed target data. Populated on the detail endpoint and on the\nlist endpoint when `?detailed=true` is passed."}]},"id":{"type":"string","format":"uuid","description":"Unique identifier for the target from the catalog"},"name":{"type":"string","description":"Product name of the target antigen"},"pricing":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TargetPricing","description":"Material pricing for this target, if self-service pricing is available.\nWhen absent, this target requires a custom quote.\n\nThe `type` field discriminates the pricing model:\n- `per_sequence`: fixed cost per sequence\n- `per_broken_lot`: customer pays for the full vendor lot"}]},"uniprot_id":{"type":["string","null"],"description":"UniProt accession for the primary protein component.\nPresent on ~82% of targets; viral/non-standard proteins lack this\n(they have `details.ncbi_id` instead).","example":"Q15116"},"url":{"type":"string","description":"URL to view this target in the catalog web UI","example":"https://targets.adaptyvbio.com/protein/f3b2afd0-f70b-5191-a90a-ae1e0545c744"},"vendor_name":{"type":"string","description":"Vendor/supplier name (e.g., \"ACRO Biosystems\")"}}},"TargetPricing":{"oneOf":[{"type":"object","description":"Per-sequence pricing: fixed cost per sequence.","required":["price_per_sequence_cents","type"],"properties":{"price_per_sequence_cents":{"type":"integer","format":"int64","description":"Material cost per sequence in USD cents","example":500},"type":{"type":"string","enum":["per_sequence"]}}},{"type":"object","description":"Broken-lot pricing: customer pays for the full lot.\n\nEach tier represents a lot size and the number of sequences it can serve.\nThe API consumer should select the smallest tier whose `num_sequences`\nis >= the experiment's sequence count.","required":["lot_prices","type"],"properties":{"lot_prices":{"type":"array","items":{"$ref":"#/components/schemas/LotPriceTier"},"description":"Lot price tiers, sorted by num_sequences ascending"},"type":{"type":"string","enum":["per_broken_lot"]}}}],"description":"Pricing information for a target's material costs.\n\nDetermines how material costs are calculated for an experiment using this target.\nThe variant reflects the pricing model configured in `foundry_api.product_prices`."},"TargetReference":{"type":"object","description":"A resolved reference to an experiment's target, as shown in the Foundry Portal.\n\nThis is the single shape every API response uses to name a target: the\nexperiment spec, result readouts, cost-estimate line items, and the internal\nminimum-sequence admin config all embed it instead of a bare id string.\n\n`name` is always present. `target_catalog_id` is present when the target is a\ncatalog product — fetch its full details from `GET /targets/{target_catalog_id}`;\nits absence indicates a custom target. `sequence` and `supplier_url` are\nincluded when available.","required":["name"],"properties":{"name":{"type":"string","description":"The target's display name, as shown in the Foundry Portal.","example":"Human PD-L1"},"sequence":{"type":["string","null"],"description":"The target's amino-acid sequence, when available."},"supplier_url":{"type":["string","null"],"description":"Link to the target's vendor product page, when available."},"target_catalog_id":{"type":["string","null"],"format":"uuid","description":"Identifier of the matching catalog product, when the target is in the\ncatalog; absent for custom targets. Resolve full details via\n`GET /targets/{target_catalog_id}`.","example":"019a03da-b87f-7e15-8b02-cef171c9871d"}}},"ThermostabilityResult":{"type":"object","description":"A thermostability readout for a single sample, measured by nanoDSF.\n\n`tm` is the melting temperature derived from the 350nm/330nm fluorescence\nratio; `onset_pts_for_ratio` and `inflection_pts_for_ratio` give the onset\nand inflection temperatures of each unfolding transition.","required":["sequence_id","onset_pts_for_ratio","inflection_pts_for_ratio"],"properties":{"bli_result_id":{"type":["string","null"],"format":"uuid","description":"Identifier of the binding result for the same sample, when the experiment\nalso ran a binding assay."},"inflection_pts_for_ratio":{"type":"array","items":{"type":"number","format":"double"},"description":"Inflection temperature (°C) of each unfolding transition, from the ratio\ncurve."},"initial_330nm":{"type":["number","null"],"format":"double","description":"Initial raw fluorescence at 330nm."},"onset_pts_for_ratio":{"type":"array","items":{"type":"number","format":"double"},"description":"Onset temperature (°C) of each unfolding transition, from the ratio curve."},"sequence":{"type":["string","null"],"description":"Protein sequence for this readout."},"sequence_id":{"type":"string","format":"uuid","description":"Identifier for this readout."},"sequence_name":{"type":["string","null"],"description":"Sample or construct name for this readout."},"tm":{"type":["number","null"],"format":"double","description":"Melting temperature (°C), from the 350nm/330nm fluorescence ratio."}}},"UpdateOrgWebhookRequest":{"type":"object","description":"Request to modify an existing organization-level webhook.\n\nEvery field is optional; omitted fields are left unchanged. `secret` uses a\nnested option so the three cases stay distinguishable in one payload:\nabsent = leave as-is, `null` = clear it (fall back to the deployment-wide\nsecret), a string = replace it. Rotation is therefore a single PATCH.\n\n# Example\n\n```json\n{\"secret\": \"a-new-32-plus-character-shared-secret\", \"enabled\": true}\n```","properties":{"enabled":{"type":["boolean","null"],"description":"Pause or resume deliveries without deleting the registration."},"headers":{"description":"Replacement header map."},"secret":{"type":["string","null"],"description":"Replacement secret, or an explicit `null` to clear it.","writeOnly":true,"minLength":32},"url":{"type":["string","null"],"description":"Replacement HTTPS URL.","example":"https://example.com/adaptyv/webhook-v2"}}},"WhoAmIOrganization":{"type":"object","description":"One organization the caller's token can access.","required":["id","name","active"],"properties":{"active":{"type":"boolean","description":"`true` for the organization this token is currently acting as (the\nactive context); `false` for the other organizations it can also access.","example":true},"id":{"type":"string","format":"uuid","description":"Unique identifier for the organization. Use this value as the\n`organization_id` in other API calls.","example":"019b8da3-4a91-16c6-fa94-619212bee6a6"},"name":{"type":"string","description":"Organization display name.","example":"Acme Biotech"},"role":{"type":["string","null"],"description":"The caller's role in this organization (e.g. `member`, `viewer`,\n`billing`).","example":"member"}}},"WhoAmIResponse":{"type":"object","description":"Identity and access summary for the calling token.","required":["organizations","permissions"],"properties":{"active_organization_id":{"type":["string","null"],"format":"uuid","description":"Identifier of the organization the token is currently acting as. This is\nthe organization flagged `active: true` in `organizations`.","example":"019b8da3-4a91-16c6-fa94-619212bee6a6"},"organizations":{"type":"array","items":{"$ref":"#/components/schemas/WhoAmIOrganization"},"description":"Every organization the token can access. For most tokens this is a\nsingle organization; exactly one entry is flagged `active`."},"permissions":{"type":"array","items":{"type":"string"},"description":"The permissions this token grants, as `resource:action` strings (for\nexample `experiment:create`). Use these to learn what the token can do\nbefore attempting an operation.","example":["organization:read","experiment:read","experiment:create"]},"token_expires_at":{"type":["string","null"],"format":"date-time","description":"When the token expires (ISO 8601 / RFC 3339, UTC). `null` for tokens\nwithout a recorded expiry.","example":"2026-12-31T23:59:59Z"},"user_id":{"type":["string","null"],"format":"uuid","description":"The authenticated user's unique identifier.","example":"019b8da3-1111-2222-3333-444455556666"}}}},"securitySchemes":{"adaptyv_biscuits_v0":{"type":"http","scheme":"bearer","bearerFormat":"Adaptyv Biscuits v0","description":"Biscuit-based bearer token. Obtain tokens from the Adaptyv Portal or via the `/tokens` endpoint. Tokens encode organization membership and role-based capabilities; the API verifies the token's cryptographic signature and authorization claims before processing requests. Use `/tokens/attenuate` to create restricted tokens for delegation."}}},"security":[{"adaptyv_biscuits_v0":[]}],"tags":[{"name":"experiments","description":"\nExperiments are the core resource for submitting protein sequences to Adaptyv's characterization services.\n\n## Lifecycle\n\nExperiments progress through these stages:\n\n1. **Draft** — Initial state for designing your experiment\n2. **In Review** — Submitted for validation and quote generation\n3. **Quote Sent** — Adaptyv has sent a pricing quote\n4. **Waiting for Confirmation** — Quote ready, awaiting acceptance\n5. **In Queue** — Confirmed and queued for lab scheduling\n6. **Waiting for Materials** — Confirmed, waiting for samples\n7. **In Production** — Lab work in progress\n8. **Data Analysis** — Characterization complete, processing results\n9. **Done** — Results available for download\n\n**Canceled** is a terminal state reachable from Draft, In Review, Waiting for Confirmation, or In Queue.\n\n## Experiment Types\n\n| Type | Description | Target Required |\n|------|-------------|-----------------|\n| `screening` | High-throughput binding assessment | Yes |\n| `affinity` | Kinetic characterization (KD, kon, koff) | Yes |\n| `thermostability` | Thermal stability measurement (Tm) | No |\n| `expression` | Expression yield assessment | No |\n| `fluorescence` | Fluorescence-based characterization | No |\n\n## Typical Workflow\n\n```\nPOST /experiments                    # Create experiment\nPOST /experiments/cost-estimate      # Preview pricing\nPOST /experiments/{id}/submit        # Submit for review\nGET /experiments/{id}/quote          # Retrieve quote when ready\nPOST /experiments/{id}/quote/confirm # Accept quote\nGET /experiments/{id}                # Monitor progress\nGET /results?experiment_id={id}      # Retrieve results when done\n```\n\n## Related Resources\n\n- [Results](#tag/results) — Characterization data from completed experiments\n- [Sequences](#tag/sequences) — Individual sequences within experiments\n- [Targets](#tag/targets) — Target proteins for binding experiments\n"},{"name":"feedback","description":"Use this endpoint to give us feedback, report bugs that are not already caught by our observability or request features (or have your agents do it)."},{"name":"info","description":"Programmatic discovery and health probes. Anonymous-eligible liveness and database connectivity probes; admin-only assay-type catalog (internal builds)."},{"name":"invoices","description":"Invoice listing for organization billing history. Read-only access to invoices generated from accepted Stripe quotes, mirrored locally with org context, currency, and status."},{"name":"quotes","description":"Stripe quote lifecycle: list, retrieve, confirm, and reject quotes for experiment pricing."},{"name":"results","description":"\nResults contain the characterization data from completed experiments.\n\n## Result Availability\n\nResults appear when an experiment reaches the **Done** status. Some experiment types provide partial results during **Data Analysis**.\n\n## Result Types by Experiment\n\n| Experiment Type | Result Data |\n|-----------------|-------------|\n| Affinity | KD, kon, koff, sensorgrams |\n| Screening | Binding yes/no, response units |\n| Thermostability | Tm values, melting curves |\n| Expression | Yield measurements |\n\n## Downloading Results\n\nUse the result `id` to fetch detailed data. Result downloads may include:\n\n- Raw sensorgram data\n- Fitted kinetic parameters\n- Quality metrics\n- Summary reports\n\n## Related Resources\n\n- [Experiments](#tag/experiments) — Parent resource that produces results\n"},{"name":"sequences","description":"\nSequences provide read-only access to protein sequences submitted across experiments.\n\n## Sequence Format\n\nSequences are amino acid strings in standard single-letter IUPAC format (e.g., `MKTLLLTLLV...`).\n\n## Sequence Properties\n\n| Field | Description |\n|-------|-------------|\n| `aa_string` | The amino acid sequence |\n| `name` | Optional human-readable identifier |\n| `control` | Whether this is a control sequence |\n| `metadata` | Structural annotations (tag location, antibody type) |\n\n## Querying Sequences\n\nFilter sequences by experiment or other criteria:\n\n```\nGET /sequences?experiment_id={id}\nGET /sequences?name=mAb-001\n```\n\n## Related Resources\n\n- [Experiments](#tag/experiments) — Parent resource containing sequences\n"},{"name":"targets","description":"\nTargets represent the molecules your samples will be tested against in binding experiments (affinity and screening).\n\n## Target Catalog\n\nAdaptyv maintains a catalog of pre-validated target proteins with established pricing. Use `selfservice_only=true` to filter for targets with immediate pricing availability.\n\n## Self-Service vs Custom Targets\n\n| Category | Description | Pricing |\n|----------|-------------|---------|\n| Self-service | Pre-validated catalog targets | Instant quote via cost estimate |\n| Custom | User-supplied or special request | Requires manual quote |\n\n## Usage\n\n1. Browse targets with `GET /targets?selfservice_only=true`\n2. Use the target's `id` when creating an experiment\n3. For custom targets, provide a `requested_target` object in the experiment request\n\n## Related Resources\n\n- [Experiments](#tag/experiments) — Create experiments using targets\n- [Cost Estimate](#operation/cost_estimate) — Preview pricing for self-service targets\n"},{"name":"tokens","description":"\nCreate restricted versions of your API token for delegation to team members or automated systems.\n\n## Token Attenuation\n\nAttenuation **reduces** the permissions of your token—it cannot grant additional access. Attenuated tokens inherit a subset of the parent token's capabilities.\n\n## Restriction Types\n\n| Restriction | Effect |\n|-------------|--------|\n| Organization | Limit access to specific organizations |\n| Resource | Limit to specific resource types (experiments, results) |\n| Action | Limit to specific actions (read, create, update) |\n| Expiry | Set a shorter expiration time |\n\n## Security Best Practices\n\n- Create narrowly-scoped tokens for automated systems\n- Use short expiration times for temporary access\n- Revoke tokens promptly when no longer needed\n"},{"name":"updates","description":"Real-time feed of experiment status changes, progress notifications, and operational alerts. Supports cursor-based pagination for efficient polling."},{"name":"webhooks","description":"Register an HTTPS endpoint that receives signed push notifications for the experiments in your organization that have not registered a webhook of their own. Each delivery carries an X-Adaptyv-Signature header (HMAC-SHA256, hex) computed over the raw request body. Set a secret here to verify deliveries with a key only your organization holds; a per-experiment webhook_secret overrides it."},{"name":"whoami","description":"Identify yourself: the organizations your token can access (with the active one marked), your user id, the permissions your token grants, and its expiry."}],"x-tagGroups":[{"name":"Experiments Workflow","tags":["experiments","results","sequences","updates"]},{"name":"Support","tags":["feedback"]},{"name":"Billing & Usage","tags":["invoices","quotes"]},{"name":"Resources","tags":["targets"]},{"name":"Discovery","tags":["info"]},{"name":"Authentication","tags":["tokens"]},{"name":"Account","tags":["whoami"]},{"name":"Notifications","tags":["webhooks"]}]}