---
title: Design proteins | Boltz API Docs
description: Generate binder, generic, or composite protein designs, migrate legacy callers safely, and understand the discriminated result shapes.
---

POST `/compute/v1/protein/design` is a legacy-compatible union. New integrations should send the top-level `type` discriminator: `binder` for target-plus-binder design or `generic` for free-form protein design without target semantics. The legacy `{ target, binder_specification, num_proteins }` body is still accepted for migration.

Use [`POST /compute/v1/protein/sequence-redesign`](/docs/api/guides/protein-sequence-redesign/index.md) when you already have a single CIF complex or scaffold and want to redesign selected residues in that fixed input structure.

## Type-discriminated request bodies

New callers should start with one of the type-discriminated request branches below:

request

Binder / shared CIFBinder / uniformGenericFusion protein

Copy

```
{
  "type": "binder",
  "num_proteins": 25,
  "templates": [
    { "id": "complex", "type": "url", "url": "https://example.com/target-and-binder.cif" }
  ],
  "target": {
    "entities": [
      {
        "type": "from_template",
        "template_id": "complex",
        "chain_id": "A",
        "crop_residues": "all",
        "epitope_residues": [42, 43, 44],
        "non_binding_residues": [0, 1, 2]
      }
    ]
    # optional: "bonds" (see Core Concepts)
  },
  "binder": {
    "modality": "nanobody",
    "entities": [
      {
        "type": "from_template",
        "template_id": "complex",
        "chain_id": "B",
        "crop_residues": "all",
        "design_motifs": [
          {
            "type": "replacement",
            "start_index": 26,
            "end_index": 33,
            "design_length_range": { "min": 7, "max": 10 },
            "filters": [
              { "type": "max_hydrophobic_fraction", "max_fraction": 0.35 }
            ]
          }
        ]
      }
    ]
  }
  # omit "global_design_filters" to keep the default excluded_amino_acids:["C"]
}
```

- `templates` is a request-local CIF catalog. Reuse the same `template_id` across `target` and `binder` when one uploaded CIF contains both.
- `global_design_filters` applies to every designed region. Omit it to keep the default `excluded_amino_acids: ["C"]`; pass `[]` to opt out completely.
- `design_motifs[].filters` stacks with `global_design_filters`, so motif-local filters can tighten only the regions that need extra constraints.
- `from_template` selects a chain from `templates`; `no_template` retains the compact legacy entity contract, including `designed_protein.value` strings such as `"MKT5..8G"`.
- Motif indices are 0-based positions within the selected `crop_residues`, while target annotations and bond residue indices refer to positions in the original CIF chain.

### Binder mode (`type: "binder"`)

`binder` mode keeps target context separate from the designed binder:

- `target.entities` is fixed context. Each target entity can come from a template or from the template-free entity union.
- A binder with `modality` and `entities` designs one binder specification.
- `binder.type: "uniformly_sampled"` samples uniformly across multiple custom or `type: "boltz_curated"` specifications. This replaces legacy `uniformly_sampled_specifications` for new requests.
- `binder.type: "boltz_curated"` lets Boltz select maintained nanobody or antibody scaffold families.

For `from_template` binder entities, `design_motifs` is where redesign work happens. Use `replacement` or `insertion` motifs, and omit `design_motifs` on a selected chain to keep it as fixed scaffold context.

When you do not have your own scaffold, `binder.type: "boltz_curated"` is the recommended way to explore de novo antibody and nanobody families.

### Generic mode (`type: "generic"`)

Use `generic` when you want protein design without target/binder semantics:

- Provide top-level `entities`, plus optional covalent `bonds`.
- Mix `from_template` and `no_template` entities in the same run.
- At least one entity or motif must create actual design work; fixed-only generic requests are rejected.

Generic results still use the same run lifecycle and artifact model, but they omit binding-specific metrics.

#### Fusion proteins (`type: "fusion_protein"`)

Within a generic request, use `fusion_protein` to concatenate two or more ordered protein segments into one output chain:

- `output_chain_id` names the single chain that survives in generated results.
- `segments` may mix `from_template`, fixed `no_template` proteins, and designed `no_template` proteins. Template-free segments omit `chain_ids` because only the parent owns an output chain ID.
- Any ordered combination of at least two supported protein segments is valid; templates are optional.
- Segments are appended in array order. The model receives the first segment as the surviving chain and fuses each later segment into it.
- Every segment must be a non-cyclic protein. Cyclic sources and targets are rejected.
- A fusion protein may be fixed when another generic entity supplies design work; the request as a whole must still contain at least one designed entity or motif.
- Multiple fusion proteins may coexist with ordinary generic entities. Top-level `bonds` cannot reference a fusion output chain.

Fusion results remain `type: "generic"`. Only the parent `output_chain_id` appears in the fused result entity.

## Run

`run()` submits a design, waits while results are generated, and downloads them to a local directory. The Python workflow helper keeps the existing `target` + `binder_specification` convenience signature; the CLI and TypeScript examples below send the type-discriminated union body directly. Use `start()` + `client.experiments.download_results()` when you want to submit now and download later.

- [Python](#tab-panel-0-0)
- [CLI](#tab-panel-0-1)
- [TypeScript](#tab-panel-0-2)

```
import os
from boltz_api import Boltz


client = Boltz(
    base_url="https://api.boltz.bio",
    api_key=os.environ["BOLTZ_API_KEY"])


target = {
"type": "no_template",
"entities": [{"type": "protein", "value": "MKTIIALSYIFCLVFA", "chain_ids": ["A"]}],
"epitope_residues": {"A": [10, 11, 12]},
}


binder_specification = {
"type": "no_template",
"modality": "custom_protein",
"entities": [{"type": "designed_protein", "chain_ids": ["B"], "value": "10..20"}],
}


# The convenience helper keeps the legacy ergonomic signature while the endpoint


# remains migration-compatible.


run_dir = client.protein.design.run(
target=target,
binder_specification=binder_specification,
num_proteins=10,
name="my-design",
)


design = client.protein.design.start(
target=target,
binder_specification=binder_specification,
num_proteins=10,
)
run_dir = client.experiments.download_results(id=design.id, name="my-design")
```

Write your type-discriminated request body to `protein-design.json` (see [Type-discriminated request bodies](#type-discriminated-request-bodies)), then:

Terminal window

```
export BOLTZ_BASE_URL="https://api.boltz.bio"
RUN_ID=$(
  boltz-api --format raw protein:design start \
    --input @json://./protein-design.json | jq -r '.id'
)


boltz-api download-results --id "$RUN_ID" --name my-design
```

The TypeScript client drives the REST API directly. Submit with `start()`, then poll and read results yourself.

```
import Boltz from 'boltz-api'


const client = new Boltz({
  baseURL: "https://api.boltz.bio",
  apiKey: process.env['BOLTZ_API_KEY'] })


const design = await client.protein.design.start({
  type: 'binder',
  num_proteins: 25,
  templates: [
    {
      id: 'complex',
      type: 'url',
      url: 'https://example.com/target-and-binder.cif',
    },
  ],
  target: {
    entities: [
      {
        type: 'from_template',
        template_id: 'complex',
        chain_id: 'A',
        crop_residues: 'all',
        epitope_residues: [42, 43, 44],
      },
    ],
  },
  binder: {
    modality: 'nanobody',
    entities: [
      {
        type: 'from_template',
        template_id: 'complex',
        chain_id: 'B',
        crop_residues: 'all',
        design_motifs: [
          {
            type: 'replacement',
            start_index: 26,
            end_index: 33,
            design_length_range: { min: 7, max: 10 },
            filters: [{ type: 'max_hydrophobic_fraction', max_fraction: 0.35 }],
          },
        ],
      },
    ],
  },
})
```

## Legacy migration

Use `BinderProteinDesignRunInput` for binder design or `GenericProteinDesignRunInput` for generic protein design. `POST /compute/v1/protein/design` still accepts the legacy `ProteinDesignRunInput` body below while you move callers over to top-level `type`:

Copy

```
{
  "num_proteins": 10,
  "target": {
    "type": "no_template",
    "entities": [
      { "type": "protein", "chain_ids": ["A"], "value": "MKTAYIAKQRQISFVKSHFSRQ" }
    ],
    "epitope_residues": { "A": [10, 11, 12] }
  },
  "binder_specification": {
    "type": "no_template",
    "modality": "peptide",
    "entities": [
      { "type": "designed_protein", "chain_ids": ["B"], "value": "12..18" }
    ]
  }
}
```

- Legacy requests keep `target`, `binder_specification`, `rules`, and `uniformly_sampled_specifications`.
- Legacy and type-discriminated `binder` and `generic` requests accept `10..1_000_000` for `num_proteins`.
- New integrations should prefer the type-discriminated request shapes above.

For fixed single-CIF sequence redesign, use [`POST /compute/v1/protein/sequence-redesign`](/docs/api/guides/protein-sequence-redesign/index.md).

## Output format

When you download with `run()` / `start()` + `client.experiments.download_results()` (or the CLI’s `download-results`), results land in a self-contained run directory:

- [Python](#tab-panel-1-0)
- [CLI](#tab-panel-1-1)
- [TypeScript](#tab-panel-1-2)

`run()` and `start()` + `client.experiments.download_results()` poll on your behalf, append each result as it's generated, and download its files into a self-contained **run directory**. Rerun with the same `name` to resume.

boltz-experiments/\<name>/

```
boltz-experiments/
└── my-run/                       # the name you chose (or an auto-generated one)
    ├── .boltz-run.json           # run + resume state, managed for you (don't edit)
    ├── run.json                  # the run object: status, progress, engine (download URLs stripped)
    └── results/
        ├── index.jsonl           # the manifest: one JSON record per result
        └── <result-id>/
            ├── archive.tar.gz             # the downloaded result archive
            ├── metadata.json              # this result's fields (metrics, sequence/SMILES, …)
            └── files/                     # extracted from the archive
                ├── metrics.json
                ├── <result-id>_predicted.cif   # predicted structure
                └── pae.npz
```

**`results/index.jsonl`** is what you read to triage a run: one compact JSON record per result, appended as results arrive. Each record mirrors the API result **minus its `artifacts`** (those are short-lived download URLs), and adds a `paths` map pointing at the files downloaded for that result. Each record also carries the designed \`entities\` and discriminated \`metrics\` for each generated result.

one results/index.jsonl record (pretty-printed; the file stores one per line)

```
{
  "id": "<result-id>",
  "created_at": "2026-02-25T13:03:40Z",
  "metrics": { "binding_confidence": 0.94, "structure_confidence": 0.95 },
  "paths": {
    "archive": "results/<result-id>/archive.tar.gz",
    "files": "results/<result-id>/files",
    "metrics": "results/<result-id>/files/metrics.json",
    "structure": "results/<result-id>/files/<result-id>_predicted.cif",
    "pae": "results/<result-id>/files/pae.npz"
  }
}
```

Everything is downloaded by default. To keep just the manifest and skip the archives, pass `download_mode="metadata_only"`.

`download-results` polls on your behalf, appends each result as it's generated, and downloads its files into a self-contained **run directory**. Rerun with the same `--name` to resume.

boltz-experiments/\<name>/

```
boltz-experiments/
└── my-run/                       # the name you chose (or an auto-generated one)
    ├── .boltz-run.json           # run + resume state, managed for you (don't edit)
    ├── run.json                  # the run object: status, progress, engine (download URLs stripped)
    └── results/
        ├── index.jsonl           # the manifest: one JSON record per result
        └── <result-id>/
            ├── archive.tar.gz             # the downloaded result archive
            ├── metadata.json              # this result's fields (metrics, sequence/SMILES, …)
            └── files/                     # extracted from the archive
                ├── metrics.json
                ├── <result-id>_predicted.cif   # predicted structure
                └── pae.npz
```

**`results/index.jsonl`** is what you read to triage a run: one compact JSON record per result, appended as results arrive. Each record mirrors the API result **minus its `artifacts`** (those are short-lived download URLs), and adds a `paths` map pointing at the files downloaded for that result. Each record also carries the designed \`entities\` and discriminated \`metrics\` for each generated result.

one results/index.jsonl record (pretty-printed; the file stores one per line)

```
{
  "id": "<result-id>",
  "created_at": "2026-02-25T13:03:40Z",
  "metrics": { "binding_confidence": 0.94, "structure_confidence": 0.95 },
  "paths": {
    "archive": "results/<result-id>/archive.tar.gz",
    "files": "results/<result-id>/files",
    "metrics": "results/<result-id>/files/metrics.json",
    "structure": "results/<result-id>/files/<result-id>_predicted.cif",
    "pae": "results/<result-id>/files/pae.npz"
  }
}
```

Everything is downloaded by default. To keep just the manifest and skip the archives, pass `--download-mode metadata_only`.

The TypeScript client does not download for you; you read the API objects directly. The result and run-status shapes are identical to what `index.jsonl` and `run.json` mirror on disk.

Type-discriminated `list_results()` responses use top-level `type`. Binder runs include binding metrics; generic runs include only structure and secondary-structure metrics. In the composite example, all child components are fused into parent chain A, so only A appears in `entities`. Legacy migration runs keep the historical result shape until you switch request branches.

result type

BinderGenericComposite generic

Copy

```
{
  "data": [
    {
      "id": "prot_des_result_8f3a2b",
      "type": "binder",
      "created_at": "2026-02-25T13:03:40Z",
      "entities": [
        { "type": "protein", "chain_ids": ["B"], "value": "GSAEELKKLAEELAKQGNSEEVKKLAEKLAQ" }
      ],
      "metrics": {
        "binding_confidence": 0.88, # 0-1; confidence that binding occurs
        "structure_confidence": 0.91, # 0-1; confidence in the predicted structure
        "iptm": 0.86, # 0-1; interface predicted TM-score
        "min_interaction_pae": 4.9, # Angstrom; lower is better
        "helix_fraction": 0.74,
        "sheet_fraction": 0.0,
        "loop_fraction": 0.26
      },
      "artifacts": {
        "structure": {
          "url": "https://.../structure.cif",
          "url_expires_at": "2026-02-25T14:03:40Z"
        },
        "archive": {
          "url": "https://.../archive.tar.gz",
          "url_expires_at": "2026-02-25T14:03:40Z"
        }
      },
      "warnings": []
    }
  ],
  "has_more": True,
  "first_id": "prot_des_result_8f3a2b",
  "last_id": "prot_des_result_4ab7e0"
}
```

The run object tracks status and progress. It is what `retrieve()` returns, and what `run.json` mirrors:

Copy

```
{
  "id": "prot_des_run_8f3a2b",
  "status": "running", # pending | running | succeeded | failed | stopped
  "progress": {
    "total_proteins_to_generate": 100,
    "num_proteins_generated": 37,
    "latest_result_id": "prot_des_result_8f3a2b"
  },
  "error": None, # { code, message } once status is "failed"
  "pipeline": "boltzprot",
  "pipeline_version": "1.0",
  "livemode": True, # false for runs created with a test key
  "workspace_id": "ws_3a2b",
  "created_at": "2026-02-25T12:00:00Z",
  "started_at": "2026-02-25T12:00:05Z",
  "completed_at": None,
  "stopped_at": None,
  "data_deleted_at": None
  # "input" echoes the submitted request while data is retained; type-discriminated runs include top-level "type"
}
```

Download URLs expire. Check `url_expires_at` and download promptly. Once a URL expires, a new one can be generated until the data is deleted. By default data is retained for 7 days; see [Data Retention](/docs/api/guides/data-retention/index.md).

Metric values and sequences shown in this guide are illustrative.
