This tutorial demonstrates how to use the 3decision API to perform an advanced search, post-filter the hits, and download the results as PDB or mmCIF files.
In this example, we will:
The tutorial covers authentication, searching by project label, filtering out apo structures, and exporting the results.
To access the 3decision API endpoints, follow these steps to set up authentication:
Get an API Secret Key
Get an Access Token
GET /auth/api/loginDng-Api-Key headercurl -X 'GET' \
'https://3decision-[customer]-api.discngine.cloud/auth/api/login' \
-H 'accept: application/json' \
-H 'X-API-VERSION: 1' \
-H 'Dng-Api-Key: your-api-secret-key'
{
"access_token": "eyJhb********htE"
}
Search for all structures belonging to a project by its label using the exact-match search with mode=PROJECT_LABEL:
GET /search/{query}/exact-match/sync?mode=PROJECT_LABEL
This returns the structure IDs matching the project.
curl -X 'GET' \
'https://3decision-[customer]-api.discngine.cloud/search/ABL1/exact-match/sync?mode=PROJECT_LABEL' \
-H 'accept: application/json' \
-H 'X-API-VERSION: 1' \
-H 'Authorization: Bearer ey*********W8'
{
"STRUCTURE_ID": [
533583,
540918,
749561
],
"number_of_hits": 3
}
ℹ️ If you don't know the project label, you can use
GET /projectsto retrieve the list of all projects you have access to, along with their labels.
In our example, we want to exclude apo structures (those without drug-like ligands). Use the batch structure information endpoint to retrieve ligand metadata for the structures found in Step 2.
POST /structures/info/batch?metadataType=ligands
Pass the structure IDs in the request body. The metadataType=ligands parameter tells the API to return ligand information, including the isDruglike flag.
ℹ️ You can also use other metadata types to post-filter on different criteria. Add one or several of the following values to the
metadataTypequery parameter:general,filter,ligands,chain,parameter,annotations,datatable. For example,metadataType=ligands,generalwould return both ligand and general structure information.
curl -X 'POST' \
'https://3decision-[customer]-api.discngine.cloud/structures/info/batch?metadataType=ligands' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'X-API-VERSION: 2' \
-H 'Authorization: Bearer ey*********W8' \
-d '[
533583, 540918, 749561
]'
[
{
"structure_id": 533583,
"ligands_info": [
{
"isDruglike": false,
"small_mol_id": 86804,
"Data": {
"RESIDUE_CODE": "PO4",
"MW": 94.971,
"SMILES": "[O-]P(=O)([O-])[O-]"
}
}
]
},
{
"structure_id": 540918,
"ligands_info": [
{
"isDruglike": true,
"small_mol_id": 88078,
"Data": {
"RESIDUE_CODE": "STI",
"MW": 493.615,
"SMILES": "CN1CCN(Cc2ccc(cc2)C(=O)Nc3ccc(C)c(Nc4nccc(n4)c5cccnc5)c3)CC1"
}
}
]
},
{
"structure_id": 749561,
"ligands_info": [
{
"isDruglike": true,
"small_mol_id": 44992,
"Data": {
"RESIDUE_CODE": "VX6",
"MW": 464.59,
"SMILES": "CN1CCN(CC1)c2cc(Nc3cc(C)n[nH]3)nc(Sc4ccc(NC(=O)C5CC5)cc4)n2"
}
}
]
}
]
Structures where ligands_info only contains ligands with isDruglike=false are apo structures (no drug-like ligands). In the example above, structure 533583 only has PO4 (isDruglike=false) and should be excluded.
Filter your structure ID list to keep only those with at least one drug-like ligand in ligands_info. Use this filtered list in the next step.
Finally, export the filtered structures as zipped PDB files. This is a two-step asynchronous process: first submit an export job, then retrieve the result.
POST /exports/structure?output_format=structures-pdb-zip
The request body accepts either structures_id (list of 3decision structure IDs) or external_codes (list of structure codes).
curl -X 'POST' \
'https://3decision-[customer]-api.discngine.cloud/exports/structure?output_format=structures-pdb-zip' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'X-API-VERSION: 1' \
-H 'Authorization: Bearer ey*********W8' \
-d '{
"structures_id": [540918, 749561]
}'
"21c6ca38-13b9-44b7-8956-78545345d4ce"
The response is a job ID (UUID string). Use it to retrieve the export result.
ℹ️ Available output formats:
structures-pdb-zip— Multiple PDB files in a ZIP archivestructures-pdb-txt— Single PDB file as text (one structure only)structures-mmcif-zip— Multiple mmCIF files in a ZIP archivestructures-mmcif-txt— Single mmCIF file as text (one structure only)structures-original-zip— Original submitted files in a ZIP archivestructures-original-txt— Single original file as text (one structure only)
Poll the job result using:
GET /exports/structure/{jobId}?filename=my_export&download=true
curl -X 'GET' \
'https://3decision-[customer]-api.discngine.cloud/exports/structure/21c6ca38-13b9-44b7-8956-78545345d4ce?filename=project_structures&download=true' \
-H 'accept: application/json' \
-H 'X-API-VERSION: 1' \
-H 'Authorization: Bearer ey*********W8' \
--output project_structures.zip
If the job is still processing, the response will contain a status message. Retry after a few seconds until the file is ready.
| Step | Endpoint | Purpose |
|---|---|---|
| 1 | GET /auth/api/login |
Authenticate and get an access token |
| 2 | GET /search/{query}/exact-match/sync?mode=PROJECT_LABEL |
Search for project structures |
| 3 | POST /structures/info/batch?metadataType=ligands |
(Optional) Post-filter — remove apo structures |
| 4a | POST /exports/structure?output_format=structures-pdb-zip |
Submit an export job |
| 4b | GET /exports/structure/{jobId} |
Download the exported PDB ZIP file |
"""
Demo script: Export Project Structures via the 3decision API
This script follows the tutorial steps:
1. Authenticate with the API
2. Search for project structures by label
3. Post-filter to remove apo structures (no drug-like ligands)
4. Export the remaining structures as a zipped PDB file
"""
import time
import requests
# ============================================================
# Configuration — update these values before running
# ============================================================
BASE_URL = "https://3decision-[customer]-api.discngine.cloud" # replace [customer]
API_SECRET_KEY = "your-api-key-here" # replace with your API key
PROJECT_LABEL = "ABL1"
OUTPUT_FILENAME = "project_structures"
# ============================================================
def print_step(description: str):
print(f" {description}")
def parse_json(response: requests.Response):
try:
return response.json()
except Exception:
return None
# ------------------------------------------------------------------
# Step 1: Authenticate
# ------------------------------------------------------------------
print_step("Authenticating...")
resp = requests.get(
f"{BASE_URL}/auth/api/login",
headers={
"accept": "application/json",
"X-API-VERSION": "1",
"Dng-Api-Key": API_SECRET_KEY,
},
)
if not resp.ok:
print(f" ERROR: Authentication failed ({resp.status_code}): {resp.text[:200]}")
exit(1)
auth_body = parse_json(resp)
access_token = auth_body["access_token"]
auth_headers = {
"accept": "application/json",
"X-API-VERSION": "1",
"Authorization": f"Bearer {access_token}",
}
# ------------------------------------------------------------------
# Step 2: Search for project structures
# ------------------------------------------------------------------
print_step(f"Searching for project '{PROJECT_LABEL}'...")
resp = requests.get(
f"{BASE_URL}/search/{PROJECT_LABEL}/exact-match/sync",
params={"mode": "PROJECT_LABEL"},
headers=auth_headers,
)
if not resp.ok:
print(f" ERROR: Search failed ({resp.status_code}): {resp.text[:200]}")
exit(1)
search_body = parse_json(resp)
structure_ids = search_body["STRUCTURE_ID"]
print(f" Found {search_body['number_of_hits']} structures.")
# ------------------------------------------------------------------
# Step 3: Post-filter — remove apo structures
# ------------------------------------------------------------------
print_step("Filtering out apo structures...")
resp = requests.post(
f"{BASE_URL}/structures/info/batch",
params={"metadataType": "ligands"},
headers={
**auth_headers,
"Content-Type": "application/json",
"X-API-VERSION": "2",
},
json=structure_ids,
)
if not resp.ok:
print(f" ERROR: Batch info failed ({resp.status_code}): {resp.text[:200]}")
exit(1)
batch_body = parse_json(resp)
# Keep only structures that have at least one drug-like ligand
filtered_ids = []
for entry in batch_body:
sid = entry.get("STRUCTURE_ID") or entry.get("structure_id")
ligands = entry.get("LIGANDS_INFO") or entry.get("ligands_info") or []
if any(lig.get("isDruglike", False) for lig in ligands):
filtered_ids.append(sid)
excluded = len(structure_ids) - len(filtered_ids)
print(f" Excluded {excluded} apo structure(s), {len(filtered_ids)} remaining.")
if not filtered_ids:
print(" No structures remaining after filtering. Exiting.")
exit(0)
# ------------------------------------------------------------------
# Step 4: Export as PDB ZIP
# ------------------------------------------------------------------
print_step("Exporting structures as PDB ZIP...")
resp = requests.post(
f"{BASE_URL}/exports/structure",
params={"output_format": "structures-pdb-zip"},
headers={
**auth_headers,
"Content-Type": "application/json",
},
json={"structures_id": filtered_ids},
)
if not resp.ok:
print(f" ERROR: Export request failed ({resp.status_code}): {resp.text[:200]}")
exit(1)
# The job ID may be returned as plain text or as a JSON string
export_body = parse_json(resp)
if export_body is not None:
job_id = export_body if isinstance(export_body, str) else str(export_body)
else:
job_id = resp.text.strip().strip('"')
output_file = f"{OUTPUT_FILENAME}.zip"
max_retries = 30
for attempt in range(1, max_retries + 1):
resp = requests.get(
f"{BASE_URL}/exports/structure/{job_id}",
params={"filename": OUTPUT_FILENAME, "download": "true"},
headers=auth_headers,
)
content_type = resp.headers.get("Content-Type", "")
if "application/zip" in content_type or "application/octet-stream" in content_type:
with open(output_file, "wb") as f:
f.write(resp.content)
print(f" Saved {output_file} ({len(resp.content)} bytes)")
break
try:
status_body = resp.json()
if isinstance(status_body, dict) and "url" in status_body:
dl_resp = requests.get(status_body["url"])
with open(output_file, "wb") as f:
f.write(dl_resp.content)
print(f" Saved {output_file} ({len(dl_resp.content)} bytes)")
break
except Exception:
pass
if attempt < max_retries:
time.sleep(5)
else:
print(f" ERROR: Export timed out. Job ID: {job_id}")
exit(1)
print(" Done!")