Skip to content
Go to Boltz API

Design proteins

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.

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

request
{
  "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 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.

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.

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() 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.

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")

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:

{
  "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.

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

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".

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
{
  "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:

{
  "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"
}