---
title: "Create experiment"
method: POST
path: "/api/v1/experiments"
tags: ["experiments"]
---

# Create experiment

`POST /api/v1/experiments`

## Request

- Base URL: `https://devs.adaptyvbio.com`
- URL: `https://devs.adaptyvbio.com/api/v1/experiments`
- Auth: HTTP bearer

Creates a new experiment request for the Adaptyv Foundry platform.

The payload captures the target (from the catalog), antibody sequences,
replicate plan, assay parameters, optional metadata, and an optional webhook
for notifications. By default, experiments are created
in Draft status so clients can review inputs before submission.

Set `skip_draft: true` to bypass Draft status and submit directly for processing.
This is useful for automated pipelines with pre-validated payloads that
don't require manual review. The experiment will be created in
"Waiting for confirmation" status instead of Draft.

API-originated experiments are automatically assigned to the organization's
API submissions project for traceability.

# ExperimentSpec fields

Validation is strict: fields that are not applicable to a given experiment
type (e.g., `method` on a non-binding type) cause a 400 rejection. Multiple
validation errors are accumulated and returned together so callers can fix
everything in one round-trip.

| Field | Type | Description |
|-------|------|-------------|
| `experiment_type` | string | **Required.** One of `affinity`, `screening`, `thermostability`, `fluorescence`, `expression`, `epitope_binning`, `enzyme_activity`. |
| `method` | string | Required for `affinity`/`screening` (`bli` or `spr`). **Rejected** for all other types. |
| `target_id` | string | Target UUID from the catalog. Required for `affinity`, `screening`, and `epitope_binning`. **Rejected** for non-binding types. |
| `sequences` | object | **Required.** Map of sequence name to amino acid string. `epitope_binning` requires exactly 4-28 sequences in multiples of 4. |
| `n_replicates` | integer | Optional for most types (default 3, range 1-5). **Rejected** for `epitope_binning`. |
| `antigen_concentrations` | array | `affinity` only; optional, defaults to `[1000.0, 316.2, 100.0, 31.6, 0.0]` nM. |
| `parameters` | object | Optional. Experiment-specific settings |

# Sequence Formats

The `sequences` field accepts two formats:

**Simple format** — amino acid string only:
```json
"sequences": {
  "seq1": "EVQLVESGGGLVQPGGSLRLSCAAS"
}
```

**Object format** — with metadata:
```json
"sequences": {
  "seq1": {
    "aa_string": "EVQLVESGGGLVQPGGSLRLSCAAS",
    "control": false,
    "metadata": { "type": "scfv", "tag_location": "C" }
  }
}
```

# Example: Affinity (BLI)

Full kinetic characterization measuring on/off rates and KD.
Target ID `019a03da-b87f-7e15-8b02-cef171c9871d` is Human PD-L1 from the catalog.

```json
{
  "name": "PD-L1 affinity panel",
  "experiment_spec": {
    "experiment_type": "affinity",
    "method": "bli",
    "target_id": "019a03da-b87f-7e15-8b02-cef171c9871d",
    "sequences": {
      "pembrolizumab_vh": "QVQLVQSGVEVKKPGASVKVSCKASGYTFTNYYMYWVRQAPGQGLEWMGGINPSNGGTNFNEKFKNRVTLTTDSSTTTAYMELKSLQFDDTAVYYCARRDYRFDMGFDYWGQGTTVTVSS",
      "pembrolizumab_vl": "EIVLTQSPATLSLSPGERATLSCRASKGVSTSGYSYLHWYQQKPGQAPRLLIYLASYLESGVPARFSGSGSGTDFTLTISSLEPEDFAVYYCQHSRDLPLTFGGGTKVEIK"
    },
    "n_replicates": 3,
    "antigen_concentrations": [1000.0, 316.2, 100.0, 31.6, 0.0]
  },
  "webhook_url": "https://example.com/webhook"
}
```

# Example: Screening (SPR)

High-throughput yes/no binding assessment using SPR.

```json
{
  "name": "Library screening round 1",
  "experiment_spec": {
    "experiment_type": "screening",
    "method": "spr",
    "target_id": "019a03da-b87f-7e15-8b02-cef171c9871d",
    "sequences": {
      "clone_A1": "EVQLVESGGGLVQPGGSLRLSCAASGFTFSSYAMSWVRQAPGKGLEWVSAISGSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCAKDRLSITIRPRYYGLDVWGQGTLVTVSS",
      "clone_A2": "QVQLVQSGAEVKKPGASVKVSCKASGYTFTSYGISWVRQAPGQGLEWMGWISAYNGNTNYAQKLQGRVTMTTDTSTSTAYMELRSLRSDDTAVYYCARDVGYCTDYSCYFDYWGQGTLVTVSS",
      "clone_A3": "EVQLLESGGGLVQPGGSLRLSCAASGFTFSTYAMSWVRQAPGKGLEWVSSISSGGSYIYYADSVKGRFTISRDNAKNSLYLQMNSLRAEDTAVYYCARRPWGYYALDIWGQGTTVTVSS"
    },
    "n_replicates": 2
  }
}
```

# Example: Thermostability

Measures melting temperature (Tm) via differential scanning fluorimetry.
No target required.

```json
{
  "name": "Lead candidates stability",
  "experiment_spec": {
    "experiment_type": "thermostability",
    "sequences": {
      "candidate_1": "QVQLVQSGAEVKKPGASVKVSCKASGYTFTSYAMHWVRQAPGQRLEWMGWINAGNGNTKYSQKFQGRVTITRDTSASTAYMELSSLRSEDTAVYYCARAKFGATGAFDIWGQGTMVTVSS",
      "candidate_2": "EVQLVESGGGLVQPGGSLRLSCAASGFNIKDTYIHWVRQAPGKGLEWVARIYPTNGYTRYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCSRWGGDGFYAMDYWGQGTLVTVSS"
    },
    "n_replicates": 3,
    "parameters": {
      "buffer": "PBS",
      "ph": 7.4
    }
  }
}
```

# Example: Fluorescence

Fluorescence-based protein characterization measuring intrinsic properties.
No target required.

```json
{
  "name": "Fluorescence characterization",
  "experiment_spec": {
    "experiment_type": "fluorescence",
    "sequences": {
      "variant_1": "QVQLVQSGAEVKKPGASVKVSCKASGYTFTSYDINWVRQATGQGLEWMGWMNPNSGNTGYAQKFQGRVTMTRDTSISTAYMELRSLRSDDTAVYYCARGGFYGSTIWFDYWGQGTLVTVSS",
      "variant_2": "EVQLVESGGGLVQPGGSLRLSCAASGFTFSSYWMSWVRQAPGKGLEWVANIKQDGSEKYYVDSVKGRFTISRDNAKNSLYLQMNSLRAEDTAVYYCARDRYGNYVDYWGQGTLVTVSS"
    },
    "n_replicates": 3
  }
}
```

# Example: Expression

Protein expression screening measuring yield and quality.
No target required.

```json
{
  "name": "Expression screening",
  "experiment_spec": {
    "experiment_type": "expression",
    "sequences": {
      "construct_A": "QVQLVQSGAEVKKPGASVKVSCKASGYTFTSYGISWVRQAPGQGLEWMGWISAYNGNTNYAQKLQGRVTMTTDTSTSTAYMELRSLRSDDTAVYYCARDVGYCTDYSCYFDYWGQGTLVTVSS",
      "construct_B": "EVQLVESGGGLVQPGGSLRLSCAASGFNIKDTYIHWVRQAPGKGLEWVARIYPTNGYTRYADSVKGRFTISADTSKNTAYLQMNSLRAEDTAVYYCSRWGGDGFYAMDYWGQGTLVTVSS"
    },
    "n_replicates": 2
  }
}
```

# Example: With skip_draft=true (Auto-Submit)

Bypass Draft status and submit directly for processing:

```json
{
  "name": "Pre-validated batch",
  "skip_draft": true,
  "experiment_spec": {
    "experiment_type": "thermostability",
    "sequences": {
      "seq1": "EVQLVESGGGLVQPGGSLRLSCAASGFTFSSYAMSWVRQAPGKGLEWVSAISGSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCAKDRLSITIRPRYYGLDVWGQGTLVTVSS"
    },
    "n_replicates": 2
  }
}
```

## Request body

- CreateExpRequest: Request payload for creating a new experiment. Created experiments start in Draft status. Use PATCH `/experiments/{id}` to modify existing experiments.
  - `auto_accept_quote` boolean: Atomic create-and-pay: accept the quote and initiate payment in the same request. When `true`, after creating the experiment and its Stripe quote the server immediately accepts the quote and finalizes the invoice. It is **non-settling for machine rails**: create-and-pay never settles or emits a payment `402`; the client settles at `/invoices/{id}/pay`. - **No header / `async_invoice`** — the quote is accepted and a draft invoice is created (today's behaviour). Responds `201 Created` with `stripe_invoice_id` and `stripe_hosted_invoice_url`. - **A machine rail (`mpp-spt` / `x402-exact`)** — the (open, unpaid) invoice is finalized and the response is `200 OK` with the accepted-quote body plus a HATEOAS `payment` pointer to `/invoices/{invoice_id}/pay`. Follow it to answer the `402` challenge and settle. On a failure after the invoice was created, the invoice is compensated (voided) so the experiment can be retried. Implies `skip_draft: true`. A quote must be creatable (target_id + full pricing available), otherwise the request is rejected with `400`.
  - `experiment_spec` ExperimentSpec, required: Request body for creating an experiment (`POST /experiments`). Required fields differ by experiment type. The matrix below is the authoritative contract; submissions that violate it are rejected with a 400 listing every problem at once. * `required` — must be set. * `rejected` — must not be set for this type. * `optional` / `≥1` / ranges — accepted as stated; the noted default applies when omitted. | Field | Affinity | Screening | Thermostability | Fluorescence | Expression | EpitopeBinning | EnzymeActivity | |-------------------------------|------------|-----------|-----------------|--------------|------------|---------------------------|----------------| | `experiment_type` | required | required | required | required | required | required | required | | `method` (`bli` \| `spr`) | required | required | rejected | rejected | rejected | rejected | rejected | | `target_id` (UUID) | required | required | rejected | rejected | rejected | required | rejected | | `sequences` | ≥1 | ≥1 | ≥1 | ≥1 | ≥1 | multiple of 4, range 4–28 | ≥1 | | `n_replicates` | optional | optional | optional | optional | optional | rejected | optional | | `antigen_concentrations` | default `[1000.0, 316.2, 100.0, 31.6, 0.0]` nM | — | — | — | — | — | — | | `parameters` (free-form JSON) | optional | optional | optional | optional | optional | optional | optional | # Example ```json { "experiment_type": "screening", "method": "bli", "target_id": "019a03da-b87f-7e15-8b02-cef171c9871d", "sequences": { "mAb1": "EVQLVESGGGLVQPGGSLRLSCAASGFTFS...", "control": {"aa_string": "MKTLVLLALLV...", "control": true} }, "n_replicates": 3 } ```
    - `antigen_concentrations` number[], nullable: Antigen concentrations to test (in nanoMolar)
    - `experiment_type` 'affinity' | 'screening' | 'thermostability' | 'fluorescence' | 'expression' | 'epitope_binning' | 'enzyme_activity', required: Experiment type determining the assay workflow.
    - `method` 'bli' | 'spr': The measurement method (BLI or SPR). Required for affinity and screening; must be omitted for all other types.
    - `n_replicates` integer, nullable: Number of technical replicates (minimum 1, recommended 3+)
    - `parameters` object, nullable: Advanced experiment-specific settings. Most experiments leave this null. When provided, the object round-trips through the experiment detail with one addition: the read path merges the `experiment_type` discriminator into it, so a submitted `{"note":"x"}` reads back as `{"experiment_type":"...","note":"x"}`. A user-supplied `experiment_type` key is preserved as-is — the discriminator is only inserted when absent.
    - `sequences` object: Sequences keyed by a human readable name. Accepts either simple amino acid strings or rich objects with control flags and metadata.
    - `target_id` string, uuid, nullable: UUID of the target antigen from the catalog, required when creating binding experiments (affinity, screening, epitope binning); only catalog targets can be specified. On the experiment read response the resolved target is returned as `target` (see `target.target_catalog_id`).
  - `name` string, required: Human-readable name for the experiment
  - `payment` PaymentSelector: Create-time payment-method selector, accepted in the request body as a body-side equivalent of the `X-Adaptyv-Payment-Method` header. Selection only — it never carries a signature or proof. x402/MPP submit their proof in a standard retry header at `/pay`, so putting a credential here would be meaningless and is not accepted. # Example ```json { "method": "x402-exact" } ```
    - `method` string, required: The payment rail to pin this experiment to for its whole lifetime. Values use the same vocabulary as the `X-Adaptyv-Payment-Method` header: `"async_invoice"` (the default Stripe-hosted-invoice rail), `"mpp-spt"`, or `"x402-exact"` (note the spelling — `x402-exact`, never `x402`). An unknown value is rejected with `400`. When the header is also present, the two selectors must agree, otherwise the request is rejected `400` (`payment_method_selector_conflict`).
  - `skip_draft` boolean: Bypass Draft status and submit directly for processing. When `true`, the experiment is created in "Waiting for confirmation" status instead of Draft, skipping the manual review step. Use this for automated pipelines with pre-validated payloads. When an experiment is submitted with `skip_draft: true` and the target has existing inventory materials, those materials are automatically linked to expedite processing.
  - `webhook_secret` string, nullable: HMAC secret this experiment's deliveries are signed with. Write-only: it is never returned by any endpoint, so store your copy when you set it. Omit it to inherit your organization's secret (see `/api/v1/webhooks`), and omit that too to inherit Adaptyv's deployment-wide secret. Minimum 32 characters.
  - `webhook_url` string, nullable: HTTPS URL for push notifications. Once set, Adaptyv POSTs an `experiment_update` event to this URL for every customer-facing update on the experiment — the same updates that trigger an email notification. Deliveries are signed with `X-Adaptyv-Signature` (HMAC-SHA256).

## Response `200`

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.

- ConfirmQuoteResponse: Response after accepting a quote Confirms the quote acceptance and provides invoice information if applicable.
  - `hosted_invoice_url` string, nullable: Stripe-hosted URL where the customer can view and pay the generated invoice
  - `id` string, required: Quote ID that was accepted
  - `invoice_id` string, nullable: ID of the invoice generated from this quote (if applicable)
  - `payment` PaymentLink: Hypermedia pointer to where and how to pay the invoice a confirm finalized. `POST /quotes/{id}/confirm` is categorically non-settling: it finalizes the (open, unpaid) invoice and hands back this block instead of settling or emitting a payment `402`. Exactly one shape is populated per response, discriminated by `kind`: - `hosted_invoice`: async-invoice quotes. Pay via `hosted_invoice_url` (the Stripe-hosted page); `pay_url`/`methods` are absent. - `machine`: a machine rail (x402-exact / mpp-spt). POST to `pay_url` (`/api/v1/invoices/{invoice_id}/pay`) to answer the `402` challenge and settle; `methods` lists the accepted rails by their `X-Adaptyv-Payment-Method` spellings.
    - `hosted_invoice_url` string, nullable: Stripe-hosted invoice page (async-invoice only); pay here.
    - `kind` 'hosted_invoice' | 'machine', required: Which payment surface a confirm [`PaymentLink`] points at.
    - `methods` string[]: Machine-payment rails accepted at `pay_url` (machine rail only), by their `X-Adaptyv-Payment-Method` header spellings.
    - `pay_url` string, nullable: Machine-payment endpoint to POST to (machine rail only): answers the `402` challenge and settles the invoice.
  - `status` 'draft' | 'open' | 'accepted' | 'canceled' | 'stale', required: Quote status from Stripe's API.

## Other responses

- `201`: Experiment created (no payment, or async-invoice auto-accept)
- `400`: Invalid request parameters (e.g. malformed target_id), or a duplicate sequence — two sequences sharing normalized residues and tag location (Fabs exempt)
- `401`: Invalid or missing authentication token
- `403`: Insufficient permissions
- `404`: target_id references a target that is not in the catalog
- `415`: Unsupported media type — the request body must be sent as application/json
- `422`: Unprocessable request body (well-formed JSON that fails semantic validation, e.g. an invalid amino-acid sequence)
- `429`: With `auto_accept_quote`: Stripe rate-limited the quote call; retry after `Retry-After` seconds
- `502`: With `auto_accept_quote`: Stripe refused or failed a call on the quote. The cause is on Adaptyv's side and Adaptyv has been alerted; the message says whether retrying can help

---

[API](https://skmtc.dev/adaptyvbio/apis/adaptyv-foundry-api.md) - [All operations](https://skmtc.dev/adaptyvbio/apis/adaptyv-foundry-api/llms.txt) - [OpenAPI document](https://skmtc.dev/adaptyvbio/apis/adaptyv-foundry-api/revisions/5a2228f4bd59?raw)
