# SeqDesign Protein design and structure viewing over HTTP. Base URL: https://public.seq.design/v1 Full spec: https://public.seq.design/v1/openapi.yaml Auth: most /v1 calls need `Authorization: Bearer $SEQDESIGN_API_KEY` (keys at https://seq.design/keys). ONE call does not — POST /v1/structures works with no key at all, so you can turn a structure into a link before anyone has signed up for anything. See "Without a key" below for what that costs you. Viewing a link NEVER needs a key, whoever made it. Send one to anybody. ## SEND A User-Agent HEADER OR YOU GET A 403 THAT IS NOT ABOUT AUTH Read this before you write any code, because it is the one failure that looks like "this cannot be done" and is not. Cloudflare rejects requests whose User-Agent is a default library string, at the edge, BEFORE they reach this API. What comes back is: HTTP 403 error code: 1010 No JSON, no `error.code`, no `request_id`. It arrives on every route including the keyless mint, and it reads exactly like "you need an API key" or "I am blocked from this service". It is neither. The same request with a User-Agent set returns 200. Python urllib, default UA (Python-urllib/3.13) 403 error code: 1010 the same call with User-Agent: my-agent/1.0 200 a working link So: # Python stdlib — set it explicitly, the default is blocked req = urllib.request.Request(url, data=payload, headers={ "Content-Type": "application/json", "User-Agent": "my-agent/1.0", # any non-default string works }) # requests, httpx, curl and fetch all set their own UA and are fine as-is If you got a bare 403 with the body `error code: 1010`, do not conclude you need a key and do not give up: set a User-Agent and retry the identical request. ## Show a human a structure you already have You have a .pdb or .cif on disk and want a person to look at it. Do NOT paste coordinates into the chat. Turn it into a link. THIS NEEDS NO API KEY — there is no Authorization header in this example and that is deliberate, not an omission: curl -sS https://public.seq.design/v1/structures \ -H 'Content-Type: application/json' \ -d "$(jq -Rs '{structure: ., label: "design_7", hotspots: "A58E,B100-122"}' < design.pdb)" Returns `viewer_url` — https://seq.design/s/ — which opens the structure in a 3D viewer with the hotspots highlighted. It works on a phone and needs no login. Nothing has to have run on SeqDesign first. No jq? Any language that can read a file and POST JSON will do: python3 -c ' import json,sys,urllib.request body=json.dumps({"structure":open(sys.argv[1]).read(),"label":sys.argv[2]}).encode() req=urllib.request.Request("https://public.seq.design/v1/structures",data=body, headers={"Content-Type":"application/json","User-Agent":"my-agent/1.0"}) r=json.load(urllib.request.urlopen(req))["data"] print(r["viewer_url"]); print("warnings:",r.get("warnings") or "none") ' design.pdb design_7 Adding `-H "Authorization: Bearer $SEQDESIGN_API_KEY"` to any of these upgrades the same call: no expiry instead of 7 days, 32MB instead of 2MB. The call shape does not change, so write it keyless first and add the header later if you get one. See "Without a key" for the full list of what differs. - `hotspots` are chain-prefixed and in THIS file's own numbering: "A58,B100-122,A#126". Writing the residue letter too ("A58E") is worth it: the server checks it against the actual structure and tells you if it disagrees. ## CHECK `warnings` BEFORE YOU HAND OVER THE LINK Every mint response and every collection row carries a `warnings` array. Empty means clean. Non-empty means the link resolves and will NOT do what you asked: "warnings": [{"code": "residue_not_found", "token": "A1", "message": "Chain A has no residue 1. Its residues run 50-54. This usually means the hotspots are in a different numbering than the file."}] The codes are `unknown_chain`, `residue_not_found`, `identity_mismatch`, `ordinal_out_of_range`, `unparsed_token`, `no_chains`, `unknown_residues`. Branch on the code, not the prose. This matters because the failure is otherwise invisible. Name a chain that is not in the file, or a residue past the end of one, or the right position in the wrong numbering, and the link mints, resolves, and highlights NOTHING. It looks exactly like a link that works, and you will have moved on by the time anyone opens it. The warnings are not fatal — numbering conventions vary and the server will not refuse your link — so it is on you to read them. - `metrics` is a free-form flat map, e.g. {"ipsae":0.81,"iptm":0.9}. It shows on the link preview and comes back when the link is read as JSON. - Links are content-addressed: the same structure with the same hotspots always returns the same url, so re-running a loop is safe and reproducible. ## Without a key Drop the Authorization header and POST /v1/structures still works. Use this when you do not have a key and the alternative is pasting coordinates into a chat. curl -sS https://public.seq.design/v1/structures \ -H 'Content-Type: application/json' \ -d "$(jq -Rs '{structure: ., label: "design_7"}' < design.pdb)" What you give up, all of it visible in the response: expires_at ALWAYS set, 7 days out. Ask for longer and it is clamped, not refused — read expires_at, do not assume what you asked for. structure 2MB max, vs 32MB with a key. A 800-residue complex is ~600KB, so this is a real limit only for very large assemblies. metrics numbers only. Text metrics need a key. batch POST /v1/structures/batch needs a key. collections POST /v1/collections works with `links` (codes you minted without a key), expires in 7 days. `structures` needs a key. A WRONG key is an error, never a silent downgrade: if you send a key and it is revoked or mistyped you get 401, not a quiet 7-day link. Omit the header entirely to mean anonymous. Use a key when the link has to outlive the week, when you are linking a run rather than a structure, or when you want the links to belong to an account you can revoke. Everything else works without one. ## Hand over a whole run One url beats three hundred. POST /v1/collections and you get `collection_url` — https://seq.design/c/ — a sortable table, one row per design, one column per metric, each row opening the viewer. Send `structures` to mint and collect in one call, up to 100: curl -sS https://public.seq.design/v1/collections \ -H "Authorization: Bearer $SEQDESIGN_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"label":"PD-L1 round 3","structures":[{"structure":"ATOM...","label":"d1","metrics":{"ipsae":0.81}}]}' Send `links` to build the table out of codes you already minted, up to 500: curl -sS https://public.seq.design/v1/collections \ -H "Authorization: Bearer $SEQDESIGN_API_KEY" \ -H 'Content-Type: application/json' \ -d '{"label":"PD-L1 round 3","links":[{"short_code":"aB3xY9zQ","metrics":{"rank":1}}]}' The cap differs because the shapes cost different things: `structures` carries coordinates and parses every one of them, `links` carries codes for structures already stored. A `short_code` may be the bare code or the whole /s/ url, and metrics you send are MERGED over the ones stored at mint time — send the rank you just worked out, not all the scores again. A code that is unknown, revoked, expired or someone else's keeps its row and carries the reason. POST /v1/structures/batch mints up to 100 and returns one url per structure, for when you are building a spreadsheet rather than sending a message. Batch, then collect, is the route to a table of several hundred designs. ## Read a link back Any link is also its own API. Fetch it with `Accept: application/json`, or just append `.json`, and you get the chains, their sequences, the hotspots and the metrics — no need to download the coordinates: curl -sS https://seq.design/s/.json curl -sS https://seq.design/c/.json A collection also serves the formats you were going to convert it to anyway. Same URL, no auth, no jq: .csv one row per design; a viewer_url column, so every row in the spreadsheet opens its own structure. read_csv(url) works directly. .tsv the same, tab-separated .txt an aligned table to read in a terminal .fasta EVERY sequence in the run, in one fetch. Headers are ">label|chain code". This is the only bulk sequence endpoint — per-structure sequences are on /s/.json. curl -sS https://seq.design/c/.txt # look at the scores curl -sS https://seq.design/c/.fasta # get every sequence To verify a link is actually right, follow `structure_fetch_url` and check the RESIDUE IDENTITY at each hotspot position. Checking that the index is in range proves nothing: measured on PD-L1 across offsets -40..+40, "indices in range" held at almost every offset while identity held at exactly one. ## What it costs, before you run anything Ask first. You do NOT need a key for this — pricing is the one thing you can read before signing up for anything. GET /v1/pricing rate card, every enabled tool, sampled at several complex sizes GET /v1/pricing/estimate?tool=bindcraft&total_residues=260&num_designs=50 a quote for one specific run GET /v1/pricing/estimate?tool=bindcraft&total_residues=260&budget_usd=5 the design count $5 buys Price scales with the TOTAL residue count of the complex (target + binder), not with wall-clock time. These endpoints call the same function that prices a real submission, so the number is authoritative, not indicative. Paying: prepaid credits. `GET /v1/credits` for balance and spend caps. Top up with USDC on Base via `POST /v1/credits/topup {amount_usd}`, which answers 402 with x402 payment details — see https://public.seq.design/v1/docs/x402. ## Run a design job GET /v1/tools list tools and their slugs GET /v1/tools/{slug} FULL parameter schema — read before submitting; in particular which residue-numbering dialect the tool's hotspots argument consumes POST /v1/jobs submit; exactly one target form, and num_designs OR budget_usd, not both GET /v1/jobs/{id} poll until terminal:true, then read OUTCOME, not status — "completed" with outcome:"succeeded_no_designs" means it ran and produced nothing POST /v1/jobs/{id}/cancel stop a run you own. Returns cancelled:N. already_terminal:true means it had already finished — not an error. A non-zero "failed" count means those GPUs are STILL RUNNING and still billing; read "failures" for which ones. GET /v1/jobs/{id}/dataframe one row per design, every QC column. Every metric cell is {value,status,reason}: a blank is never ambiguous. Beside "rows": metric_contract which fields are cross-tool comparable (class A) vs within-tool only (B) vs annotation (C), and what provenance each one needs consensus within-target, within-length-class z ordering over THIS page sort whether the field you sorted on is comparable across tools Rank on class A only. GET /v1/jobs/{id}/dataframe?format=csv the same rows as text/csv GET /v1/jobs/{id}/structures per-design viewer links GET /v1/credits balance and spend caps — check before a campaign ## Refolding a CYCLIC design A macrocycle folded without declaring its ring comes back OPEN and still reports ipTM, ipSAE and pLDDT — describing a linear peptide. So a refold whose parent was cyclic (rfpeptides by tool identity, ProteinHunter by its recorded mode) and whose input declares no ring is refused 400 `cyclisation_mismatch`, before dispatch. Declare the ring on the record. It is read in any field after the chain id, and it reaches the GPU for EVERY fold tool, including protenix and opendde, whose input is a generated sequences.json rather than your FASTA: >A|protein target, linear >B|protein|cyclic head-to-tail amide >B|protein|cyclic:disulfide terminal cysteines, the (1, L) pair >B|protein|ss:3-11,5-20 explicit internal disulfides, 1-based >B|protein|empty|cyclic both: no MSA for this chain, and a ring A malformed declaration is a 400 at submit, not a surprise on the GPU. To fold a cyclic design's refold LINEAR on purpose, send `allow_linear_refold: true`; that is a deliberate choice recorded in the request, not a way to quiet the check. ## Other surfaces Both are conveniences over the HTTP above. If either is not installed where you are running, use curl — it always works and needs nothing. - Python CLI. NOT on PyPI: install from the repo (`pip install -e /clients/python`, or put `/clients/python` on PYTHONPATH). Then `seqdesign view out/*.pdb --urls`, or `seqdesign view --manifest designs.csv --collection` to turn a metrics table into one link. - MCP server (stdio): `SEQDESIGN_API_KEY=... npx @seqdesign/mcp`. Tools include create_structure_link, create_structure_links, create_structure_collection, submit_design. Check the version you get: the structure-link tools landed in 0.3.0, and an older published build will simply not list them. ## Errors Every error is `{"error":{"code","message"},"request_id"}`. `code` is stable; branch on it, not on the message. A 403 saying "missing required scope" means the key is older than the endpoint — mint a new one.