Logo
Insilicom Open Services
?
Main
  • Home
  • Documentation
      Get Started
      • Quick Start
      IKraph APIs
      • Introduction
      • v3 APIs
      ISearch API
      • v1 Search
      Accounts
      • API Keys
  • API Keys
Insilicom Open Servicesv1.0

IKraph API Documentations#

Base URL: https://service.insilicom.com

Authentication#

  • Header: X-API-KEY — Your API key provided by the service
  • All requests must include this header. The proxy service handles signature generation and JWT authentication internally.

Endpoints#

EndpointPurpose
POST /open_api/iexplore/v3/search_direct_relationsSubmit direct relation search (returns task_id)
POST /open_api/iexplore/v3/search_direct_relations_detailsRetrieve saved results (evidence counts only)
POST /open_api/iexplore/v3/search_direct_relations_evidencesRetrieve evidence PMIDs for a specific direct relation (synchronous, paginated)
POST /open_api/iexplore/v3/search_indirect_relationsSubmit indirect relation search (returns task_id)
POST /open_api/iexplore/v3/search_indirect_relations_detailsRetrieve saved indirect results (evidence counts only)
POST /open_api/iexplore/v3/search_indirect_relations_evidencesRetrieve evidence PMIDs for a specific indirect relation (synchronous, paginated)
POST /open_api/iexplore/v3/search_pathSubmit shortest / k-shortest path search between two entities (returns task_id)
POST /open_api/iexplore/v3/search_specific_pathSubmit an explicit user-specified hop-sequence path search (returns task_id)
POST /open_api/iexplore/v3/task_statusPoll the status of an async search task
POST /open_api/iexplore/v3/get_biokde_idResolve entity name to BioKDE ID (or synonym candidate list)
GET /open_api/iexplore/v3/readmeAgent-readable API documentation (raw markdown)
GET /open_api/iexplore/v3/entity-typeList all valid entity type names
POST /open_api/iexplore/v3/entity-type-detailDefinitions of the requested entity types
GET /open_api/iexplore/v3/relation-typeList all valid relation type names
POST /open_api/iexplore/v3/relation-type-detailDefinitions of the requested relation types

Note: search_path, search_specific_path, and the path results are stored in the indirect result store — fetch them with search_indirect_relations_details and search_indirect_relations_evidences using the returned result_id.

Async Flows#

v3 search endpoints use an async task queue. Evidence endpoints are synchronous.

Search Flow#

Every search request is enqueued and returns a task_id. Cached results are resolved immediately by the worker (fast path) but use the same response shape and polling flow as new searches — callers do not need to branch on cache state.

   ┌─────────────────────┐
   │  search_direct_     │
   │  relations  (POST)  │
   └──────┬──────────────┘
          │
          │ 202
       {task_id}
          │
          ▼
   ┌──────────────┐
   │ task_status  │◄── poll every 3s
   └──┬────────┬──┘
      │        │
   pending  completed
               │
           {result_id}
               │
               ▼
   ┌──────────────────────┐
   │  _details /          │
   │  _evidences          │
   └──────────────────────┘

Search → HTTP 202 with task_id → poll task_status → get result_id when completed → get details/evidences. Previously-cached searches complete on the first poll; new searches complete once the background worker finishes.

Credit System#

Every call is charged a flat base price. Details and evidence endpoints additionally charge a per-result fee on top of the base. The response includes:

FieldTypeDescription
credits_consumedintTotal credits charged for this call (base + per-unit, when applicable)
remaining_creditsintCredits remaining in your account after this call

Pricing rules:

EndpointCharge
search_direct_relationsbase
search_indirect_relationsbase
get_biokde_idbase
search_direct_relations_detailsbase + (returned_count × per-relation)
search_indirect_relations_detailsbase + (total relation edges across returned paths × per-relation)
search_direct_relations_evidencesbase + (number of evidences in the returned page × per-evidence)
search_indirect_relations_evidencesbase + (number of evidences in the returned page × per-evidence)

For indirect details, "total relation edges" is summed across all returned paths — a 2-hop path counts as 2 relations, a 3-hop path as 3, and so on. Lowering requested_paths_count (or requested_relations_count on direct details) reduces both the data returned and the credit cost.

Current prices: 10 credits per base call, 1 credit per relation, 10 credits per evidence. Prices may change without notice — always read the actual credits_consumed from the response rather than computing it client-side.

Cache hits return credits_consumed: 0 — the gateway's 5-minute per-user response cache short-circuits billing for identical requests. This applies to all seven endpoints listed above. See the Response Cache section below.

Failed calls are not billed. If request validation, permission check, or balance check fails, the base charge never runs. If the IKraph backend itself returns a non-2xx response (4xx or 5xx), the base pre-authorization is cancelled and your balance is restored. The only edge case where the base is charged but no data is returned: a details/evidence call that succeeds at the base step but is denied at the per-unit step due to insufficient credits — in which case the response is HTTP 403 with credits_consumed reflecting only the base portion already paid.

Response Cache#

All v3 search endpoints listed in this document are fronted by a 5-minute per-user response cache at the API gateway. When the same user re-issues a request with identical parameters within 5 minutes:

  • The previous response body is returned verbatim (with the current request's session_id substituted in).
  • No credits are consumed on the cache hit — credits_consumed is 0 and no billing record is created.
  • The original HTTP status is preserved. A queued search that returned HTTP 202 with a task_id the first time returns 202 with the same task_id on a cache hit; a details/evidences endpoint returning HTTP 200 is re-served as 200.

Cache keying:

  • The cache key is derived from the authenticated user_id combined with a stable hash of the full validated request body. Each user has an independent cache, and any change to any request parameter (including page, page_size, or whitespace inside a name) produces a different cache entry.
  • This is a separate layer from the search-result cache inside the BioKDE backend: that one is user-agnostic, keyed on search parameters only, and has a much longer retention window. The two layers compose transparently.

Endpoints covered:

EndpointNotes on cache hit
search_direct_relationsSame task_id is returned; you can still poll task_status for the live task state.
search_indirect_relationsSame task_id is returned; live polling via task_status continues to reflect the latest state.
search_direct_relations_detailsCached details body with credits_consumed: 0.
search_indirect_relations_detailsCached details body with credits_consumed: 0.
search_direct_relations_evidencesPaginated — each (result_id, relID, page, page_size) combination is cached independently.
search_indirect_relations_evidencesPaginated — each (result_id, relID, page, page_size) combination is cached independently.

Endpoints not cached: task_status (always reflects live task state) and get_biokde_id.

The cache is TTL-only — there is no manual invalidation. To force a fresh response before the 5-minute TTL expires, change any request parameter.

Entity Types#

Valid values for entity_*_type on direct search (search_direct_relations) and entity resolution (get_biokde_id):

Gene, Chemical, Disease, Species, Anatomy, Biological Process, CellLine, Cellular Component, DNAMutation, Molecular Function, Pathway, Pharmacologic Class

Indirect search (search_indirect_relations) accepts only a subset: Chemical, Disease, Gene. Passing any other entity type (e.g., Pathway, Anatomy) to an indirect search returns HTTP 400 with a serializer error listing the accepted values.

Searching for drug entities (is_drug)#

Drug is not a valid entity type in v3 — it was removed because some drug entities exist under both the Chemical and Gene labels and would resolve to two biokdeids, which is not desired.

To search for a drug entity in v3, set entity_*_type to the underlying parent label and add the corresponding is_drug flag:

EndpointDrug-scoping flag(s)Allowed parent type(s)
get_biokde_idis_drug (default "0")Chemical or Gene
search_direct_relationsis_drug_one / is_drug_two (default "0")Chemical or Gene
search_indirect_relationsis_drug_one / is_drug_two (default "0")Chemical or Gene

When is_drug="1", the lookup is scoped to nodes with subtype="Drug" under the chosen parent label. Setting is_drug="1" with any other entity_*_type (e.g. Disease) returns HTTP 400.

Example — small-molecule drug stored under Chemical:

{
  "entity_type": "Chemical",
  "entity_name": "Aspirin",
  "is_drug": "1"
}

Example — biologic/protein drug stored under Gene:

{
  "entity_type": "Gene",
  "entity_name": "Adalimumab",
  "is_drug": "1"
}

Example — direct-search query asking "what diseases is the drug Aspirin related to?":

{
  "entity_one_type": "Chemical",
  "entity_one_name": "Aspirin",
  "is_drug_one": "1",
  "entity_two_type": "Disease",
  "relation_type": ["-1"]
}

The node1Type / node2Type field in relation responses will be the parent label (Chemical or Gene), never Drug.


Curated vs Text-Mined Relations#

Relations in IKraph come from three distinct sources, and they behave differently under date and novelty filters. Knowing which is which is important — depending on the entity-type pair you query, the response can be all text-mined, all non-text-mined (curated and/or RNASeq), or a mix, and certain filter combinations will exclude the non-text-mined edges entirely.

AspectText-mined (method="MP")Curated (method="MA")RNASeq (method="RNASeq")
SourceNLP extraction from PubMed sentencesImported curated knowledge graphs (Hetionet, primeKG, PubChem, GO_annotation, TTD)Computed from RNASeq expression data
source fieldnullThe curated source name (e.g., "Hetionet", "primeKG")null
PMID-anchored evidenceYes — fetchable via the *_evidences endpointsNoevidence_count = 0 and evidences = []Noevidence_count = 0 and evidences = []
Per-mention datesYes — used for date filtering and noveltyNo — relation has no publication dateNo — relation has no publication date
Per-mention probabilityPer sentenceSource-level (often a fixed value such as 0.9 / 1.0)Source-level

Because curated and RNASeq edges share the same shape (no PMID-anchored locations, no per-mention dates), they are handled identically by the filter pipeline. The remainder of this section refers to both as non-text-mined edges.

Filter behavior — date and novelty drop non-text-mined edges#

Non-text-mined edges have no PMID-anchored locations and no per-mention publication date, so date and novelty filters are meaningful only for text-mined edges. Whenever the user activates one of these filters, the search drops non-text-mined edges at query time:

Filter settingNon-text-mined edges (method="MA" or "RNASeq")Text-mined edges (method="MP")
start_date == "" and end_date == "" and novelty == "-1"Included (with evidence_count: 0; source populated for MA only)Included
start_date or end_date set to any non-empty value (e.g. "1900-01-01" / "2027-01-01")ExcludedFiltered to that date range
novelty set to "1" or "0"ExcludedFiltered to that novelty value
Both date and novelty setExcludedFiltered by both

Note that even a deliberately wide-open date range (e.g. "1900-01-01" to "2100-12-31") is treated as an explicit filter and will exclude non-text-mined edges. If you want them included, send start_date and end_date as empty strings (or omit them) and leave novelty at "-1".

This applies symmetrically to direct search (entire relation is dropped) and indirect search (entire path is dropped if any hop is non-text-mined).

Which entity-type pairs return only non-text-mined relations?#

Several entity-type pairs in the underlying graph have no text-mined edges at all — only curated and/or RNASeq. For these pairs, any query that activates a date/novelty filter will return zero results regardless of the entities involved:

  • GenePathway
  • GeneAnatomy
  • GeneBiological Process
  • GeneCellular Component
  • GeneMolecular Function
  • PathwayPathway, ChemicalPathway
  • Pharmacologic Class ↔ anything
  • Most other GO-derived / MeSH-derived combinations

Pairs with both kinds (e.g. GeneDisease, ChemicalDisease, GeneGene, ChemicalChemical) will return a mix when no filter is active and only the text-mined subset when a filter is active.


Direct Relations Search#

Search for direct (1-hop) relationships between biological entities using Neo4j graph traversal. Supports disease sub-tree expansion and date/novelty filtering. Evidence is pre-validated using an LLM-based pipeline; entries that failed validation are excluded automatically.

1. POST /open_api/iexplore/v3/search_direct_relations#

Submits a direct relationship search. Always queues a background task and returns a task_id — regardless of whether the search has been run before. If enforce_refetch is "0" and a cached result exists, the worker resolves the task immediately from history on the first poll; otherwise it runs the full search. The response shape is the same in both cases.

Identical submissions from the same user within the last 5 minutes are served from the gateway response cache and return the same task_id with credits_consumed: 0. See Response Cache.

Request Body#

FieldTypeRequiredDefaultDescription
entity_one_typestringYesEntity type for the first entity
entity_one_namestringCond.Name of the first entity. Required unless entity_one_id is given.
entity_one_idstringNo""Pre-resolved BioKDE ID for entity one (e.g. from get_biokde_id). When set, name resolution is skipped.
entity_one_speciesstringNo"Homo Sapiens"Species for entity one
entity_two_typestringYesEntity type for the second entity
entity_two_namestringNo""Name of the second entity. Leave empty for one-entity search (returns all relations for entity one filtered by entity_two_type).
entity_two_idstringNo""Pre-resolved BioKDE ID for entity two. When set, name resolution is skipped.
entity_two_speciesstringNo"Homo Sapiens"Species for entity two
relation_typearray[string]YesList of relationship type filters. Accepts type names (e.g., ["Positive_Correlation", "Negative_Correlation"]) or numeric codes (e.g., ["2", "3"]). Use ["-1"] for no filter. Invalid values return HTTP 400 with a list of valid options. See Relation Type Reference below.
relation_directionstringNo"-1"Direction filter: "12" (entity one → two), "21" (entity two → one), "0" (no direction), "NA" (not applicable), "-1" (no filter)
correlation_typearray[string]No["-1"]Correlation type filter: "2" (Positive), "0" (Negative), "1" (Unknown). Use ["-1"] for no filter.
noveltystringNo"-1"Novelty filter: "-1" (all), "1" (novel only), "0" (non-novel only). Setting any value other than "-1" also drops curated relations from the result — see Curated vs Text-Mined Relations.
start_datestringNo""Start of date range (YYYY-MM-DD). Filters text-mined evidence by publication date. Setting a non-empty value here or in end_date also drops curated relations from the result — see Curated vs Text-Mined Relations.
end_datestringNo""End of date range (YYYY-MM-DD). Same caveat as start_date.
exclude_sub_typestringNo"1""1" = single entity search. "0" = include disease sub-tree (expands Disease entities into MeSH sub-types and returns grouped results).
prob_methodstringNo"ALL"Method filter: "MP" (model predicted, text-mined), "MA" (manually annotated, curated), "RNASeq" (calculated from RNASeq data), "ALL" (no filter).
enforce_refetchstringNo"0""1" = bypass cached results and re-run search
probability_thresholdfloatNo0.9Minimum probability for returned relations (0.0–1.0). Only relations with prob >= threshold are included.

Example Request#

{
  "entity_one_type": "Disease",
  "entity_one_name": "Lung Cancer",
  "entity_two_type": "Chemical",
  "entity_two_name": "",
  "relation_type": ["Negative_Correlation"],
  "relation_direction": "-1",
  "correlation_type": ["-1"],
  "novelty": "-1",
  "start_date": "",
  "end_date": "",
  "exclude_sub_type": "0",
  "prob_method": "ALL",
  "probability_threshold": 0.9
}

Response (HTTP 202)#

Every request — whether cached or new — returns a queued response with a task_id:

{
  "status": "success",
  "session_id": "1776742838149_92397606-4e17-42be-8dfd-510b2a0c9d91_4288135032",
  "message": "Your search request has been queued. Please poll the task_status endpoint with your task_id.",
  "credits_consumed": 10,
  "remaining_credits": 2870,
  "task_id": "2748b9d8-2bb6-4f93-aedf-cbb06c90a879"
}

Next step: Poll task_status with the task_id until the status is "completed", then use the returned result_id with the details endpoints. Cached searches typically complete on the first poll.


2. POST /open_api/iexplore/v3/search_direct_relations_details#

Retrieves saved search results with evidence counts only (no full evidence data). Use this for lightweight result browsing.

Request Body#

FieldTypeRequiredDefaultDescription
result_idstringYesThe result_id returned by search_direct_relations or task_status
requested_relations_countintNo0Maximum number of relations to return, ordered by relation probability descending. 0 means return all. The per-relation portion of the bill is returned_count × per-relation price, so lowering this value lowers the credit cost.

Example Request#

{
  "result_id": "68ed2cc3869f71a6f40ebabc",
  "requested_relations_count": 20
}

Response — Single Entity (exclude_sub_type: "1")#

{
  "relations_count": 5,
  "returned_count": 5,
  "credits_consumed": 15,
  "remaining_credits": 2855,
  "relations": [
    [
      {
        "relID": "647681.4744.3.0.12.MP",
        "probability": "1.0",
        "method": "MP",
        "source": null,
        "relation_details": {
          "node1Id": "4744",
          "node1Name": "Lung Cancer",
          "node1Type": "Disease",
          "node1Subtype": "NA",
          "node2Id": "647681",
          "node2Name": "Docetaxel",
          "node2Type": "Chemical",
          "node2Subtype": "Drug",
          "score": 43.697,
          "relationship_type": "Negative_Correlation",
          "correlation_type": "0",
          "direction": "21"
        },
        "evidence_count": 5
      },
      {
        "relID": "433451.10452560.10.1.0.Hetionet",
        "probability": "0.9",
        "method": "MA",
        "source": "Hetionet",
        "relation_details": {
          "node1Id": "433451",
          "node1Name": "MAPK1",
          "node1Type": "Gene",
          "node1Subtype": "NA",
          "node2Id": "10452560",
          "node2Name": "Mitotic M-M/G1 Phases",
          "node2Type": "Pathway",
          "node2Subtype": "NA",
          "score": 1.0,
          "relationship_type": "participates",
          "correlation_type": "1",
          "direction": "NA"
        },
        "evidence_count": 0
      }
    ]
  ]
}

Note: The first relation above is text-mined (method: "MP", evidence_count > 0); the second is curated (method: "MA", source: "Hetionet", evidence_count: 0). Curated relations have no PubMed sentence evidence to fetch — calling the evidences endpoint for one returns evidences: [] with the method and source fields populated.

Response — Sub-type (exclude_sub_type: "0")#

Results are grouped by disease sub-type:

{
  "relations_count": 42,
  "returned_count": 42,
  "credits_consumed": 52,
  "remaining_credits": 2818,
  "relations": [
    {
      "disease_name": "SCLC",
      "relations_count": 7,
      "relations": [
        {
          "relID": "647681.4744.3.0.12.MP",
          "probability": "1.0",
          "relation_details": {
            "node1Id": "4744",
            "node1Name": "SCLC",
            "node1Type": "Disease",
            "node1Subtype": "NA",
            "node2Id": "647681",
            "node2Name": "Docetaxel",
            "node2Type": "Chemical",
            "node2Subtype": "Drug",
            "score": 43.697,
            "relationship_type": "Negative_Correlation",
            "correlation_type": "0",
            "direction": "21"
          },
          "evidence_count": 5
        }
      ]
    },
    {
      "disease_name": "NSCLC",
      "relations_count": 12,
      "relations": [...]
    }
  ]
}

Note: The evidences array is stripped from this endpoint. Only evidence_count is included. Use search_direct_relations_evidences with the relID to retrieve evidence PMIDs.


3. POST /open_api/iexplore/v3/search_direct_relations_evidences#

Retrieve evidence PMIDs for a single relation in a saved direct search result. This is a synchronous, paginated endpoint — PMIDs are fetched from Neo4j and returned immediately.

Request Body#

FieldTypeRequiredDefaultDescription
result_idstringYesThe result_id from search_direct_relations or task_status
relIDstringYesThe relID of the relation (from the details endpoint response)
pageintNo1Page number (1-indexed)
page_sizeintNo20Number of PMIDs per page (1–500)
sortstringNo"probability"Evidence ordering: probability (default — highest probability first) or date (most recent publication first)
noveltystringNo"" (inherit)Override the search-time novelty filter: "1" (novel), "0" (non-novel), "-1" (all). Blank inherits the saved value.
start_datestringNo"" (inherit)Override the evidence date-range start (YYYY-MM-DD).
end_datestringNo"" (inherit)Override the evidence date-range end (YYYY-MM-DD).

The same sort / novelty / start_date / end_date overrides apply to search_indirect_relations_evidences. When omitted, the saved search-time filters are used, so the result set is unchanged.

Example Request#

{
  "result_id": "69b3200a04bac13dfd4f45a5",
  "relID": "647681.4744.3.0.12.MP",
  "page": 1,
  "page_size": 20
}

Response (HTTP 200) — Text-mined relation#

{
  "status": "success",
  "session_id": "...",
  "result_id": "69b3200a04bac13dfd4f45a5",
  "relID": "647681.4744.3.0.12.MP",
  "method": "MP",
  "source": null,
  "total_count": 61,
  "page": 1,
  "page_size": 20,
  "total_pages": 4,
  "credits_consumed": 40,
  "remaining_credits": 2778,
  "evidences": [
    {"pmid": "15160349", "probability": 0.98, "pub_date": "2004-05-01"},
    {"pmid": "18294837", "probability": 0.95, "pub_date": "2008-02-15"},
    {"pmid": "20481923", "probability": 0.91, "pub_date": "2010-06-22"}
  ]
}

Response (HTTP 200) — Curated relation#

For relations with method == "MA", the endpoint short-circuits with an empty evidences list and returns the curated source so the client can render an appropriate message (e.g. "Curated relation — source: Hetionet, no PubMed evidence available") instead of an empty page. Only the base price is charged (no per-evidence charge when no evidences are returned).

{
  "status": "success",
  "session_id": "...",
  "result_id": "69b3200a04bac13dfd4f45a5",
  "relID": "433451.10452560.10.1.0.Hetionet",
  "method": "MA",
  "source": "Hetionet",
  "total_count": 0,
  "page": 1,
  "page_size": 20,
  "total_pages": 0,
  "credits_consumed": 10,
  "remaining_credits": 2768,
  "evidences": []
}

Note: For text-mined relations, evidences are deduplicated per PMID and ordered by highest evidence probability. When a PMID appears in multiple sentences for a relation, the highest sentence-level probability is returned. All matching evidences are available (no cap) — use pagination to retrieve them.

Direct Relations — Field Reference#

Relation Object (details endpoint)#

FieldTypeDescription
relIDstringRelation identifier (format: node2Id.node1Id.relType.corrType.direction.method for text-mined, node1Id.node2Id.relType.corrType.direction.<source> for curated). Use this value in evidence requests.
probabilitystringRelation probability score (0-1)
methodstring"MP" for text-mined (NLP-extracted), "MA" for curated (knowledge-graph imports), "RNASeq" for RNA-Seq derived
sourcestring | nullCurated source name when method == "MA": "Hetionet", "primeKG", "PubChem", "GO_annotation", or "TTD". null otherwise.
relation_detailsobjectNode and relationship metadata (see below)
evidence_countintNumber of PMID-anchored evidences. Always 0 for curated relations (method == "MA") — they have no PubMed sentence anchors.
relation_seq_idintSequential ID within the result

Relation Details Object#

FieldTypeDescription
node1IdstringBioKDE ID for entity one (the searched entity)
node1NamestringDisplay name for entity one
node1TypestringEntity type label
node1SubtypestringEntity subtype. "Drug" for drug-subtype Chemicals/Genes; "NA" (or empty) otherwise.
node2IdstringBioKDE ID for entity two
node2NamestringDisplay name for entity two
node2TypestringEntity type label
node2SubtypestringEntity subtype. "Drug" for drug-subtype Chemicals/Genes; "NA" (or empty) otherwise.
scorenumberRelation confidence score
relationship_typestringe.g., Positive_Correlation, Negative_Correlation, Association
correlation_typestring"2" (Positive), "0" (Negative), "1" (Unknown)
directionstring"12" (node1->node2), "21" (node2->node1), "0" (no direction), "NA"

Evidence Response (evidence endpoints)#

FieldTypeDescription
result_idstringThe result this evidence belongs to
relIDstringThe relation these evidences belong to
methodstring"MP" for text-mined, "MA" for curated, "RNASeq" for RNA-Seq derived
sourcestring | nullCurated source name ("Hetionet", "primeKG", "PubChem", "GO_annotation", "TTD") when method == "MA". null for text-mined.
total_countintTotal number of unique PMIDs for this relation. Always 0 for curated relations.
pageintCurrent page number
page_sizeintNumber of evidences per page
total_pagesintTotal number of pages
evidencesarray[object]Evidences for the current page, ordered by probability descending. Each entry is {"pmid": string, "probability": float, "pub_date": string}. probability is the highest sentence-level probability for that PMID in this relation. pub_date is the PubMed publication date in ISO YYYY-MM-DD format. Always [] for curated relations.

Relation Type Reference#

The relation_type field accepts either human-readable names or numeric codes. Use "-1" (or omit the filter) to match all types.

NameCodeNameCode
Association1drug_protein21
Positive_Correlation2contraindication22
Negative_Correlation3indication23
Bind4off-label use24
Cotreatment5drug_drug25
Comparison6phenotype_protein26
Drug_Interaction7phenotype_phenotype27
Conversion8disease_phenotype_negative28
interacts9disease_phenotype_positive29
participates10disease_protein30
causes11disease_disease31
regulates12drug_effect32
covaries13bioprocess_bioprocess33
localizes14molfunc_molfunc34
resembles15cellcomp_cellcomp35
treats16molfunc_protein36
includes17cellcomp_protein37
presents18bioprocess_protein38
palliates19exposure_protein39
protein_protein20exposure_disease40
exposure_exposure41
Drug_Target50exposure_bioprocess42
Target_Disease51exposure_molfunc43
Biomarker_Disease52exposure_cellcomp44
pathway_pathway45
pathway_protein46
anatomy_anatomy47
anatomy_protein_present48
anatomy_protein_absent49

Note: Sending an invalid name or code returns HTTP 400 with the full list of valid values.

How It Works#

  1. Entity resolution: Entity names are resolved to biokdeid values via Neo4j. When exclude_sub_type="0", Disease entities are expanded into their MeSH sub-tree via MongoDB (parent + all child diseases).
  2. Graph traversal: A 1-hop Cypher query finds direct RELATED_TO relationships in Neo4j. For sub-type searches, separate queries run per sub-entity with independent result limits.
  3. Cypher-level filters: relation_type, correlation_type, direction, prob_method, and probability_threshold are applied in the Cypher WHERE clause. When a date filter is set (start_date and end_date non-empty), a minDate/maxDate predicate is appended; this also drops curated relations (which have no per-relation dates) — see Curated vs Text-Mined Relations.
  4. Post-processing filters: novelty and date range filters are applied in Python on the locations JSON of each relationship.
  5. Curated edge handling: Relations with method="MA" (no PubMed-anchored locations) are kept with evidence_count=0 and the source field populated when no date/novelty filter is active. They are excluded when one of those filters is set.
  6. Pre-validated evidence: Each text-mined evidence entry carries pre-baked LLM validation flags. Entries that were attempted and failed validation are dropped before counting. Entries that were never attempted, plus all curated evidence, are kept.
  7. Evidence counting: For each text-mined relation, the remaining evidence locations are counted after applying date/novelty filters. Text-mined relations with evidence_count = 0 after filtering are excluded; curated relations with evidence_count = 0 are kept. Full evidence PMIDs are retrieved on-demand via the evidence endpoint.
  8. Result storage: Results are saved to MongoDB for retrieval via the details endpoints.

Indirect Relations Search#

Search for indirect relationships between two biological entities using Neo4j graph traversal. Returns multi-hop paths connecting the two entities through intermediate nodes.

4. POST /open_api/iexplore/v3/search_indirect_relations#

Submits an indirect relationship search. Always queues a background task and returns a task_id — regardless of whether the search has been run before. If enforce_refetch is "0" and a cached result exists, the worker resolves the task immediately from history on the first poll; otherwise it runs the full search. The response shape is the same in both cases.

Identical submissions from the same user within the last 5 minutes are served from the gateway response cache and return the same task_id with credits_consumed: 0. See Response Cache.

Request Body#

FieldTypeRequiredDefaultDescription
entity_one_typestringYesEntity type for the first entity
entity_one_namestringCond.Name of the first entity. Required unless entity_one_id is given.
entity_one_idstringNo""Pre-resolved BioKDE ID for entity one. When set, name resolution is skipped.
entity_two_typestringYesEntity type for the second entity
entity_two_namestringCond.Name of the second entity. Required unless entity_two_id is given.
entity_two_idstringNo""Pre-resolved BioKDE ID for entity two. When set, name resolution is skipped.
entity_one_speciesstringNo"Homo Sapiens"Species for entity one
entity_two_speciesstringNo"Homo Sapiens"Species for entity two
starting_datestringNo""Start of date range filter (YYYY-MM-DD). Filters text-mined evidence locations. Setting a non-empty value here or in ending_date also drops any path containing a curated hop — see Curated vs Text-Mined Relations.
ending_datestringNo""End of date range filter (YYYY-MM-DD). Same caveat as starting_date.
relation_typestringNo"""Positive_Correlation" or "Negative_Correlation". Empty = no filter.
directionstringNo"""12" or "21". Empty = no filter.
noveltystringNo"-1""-1" (all), "1" (novel only), "0" (non-novel only). Setting any value other than "-1" also drops any path containing a curated hop — see Curated vs Text-Mined Relations.
limitintNo100Maximum number of paths to return (1-1000)
enforce_refetchstringNo"0""1" = bypass cached results and re-run search
intermediate_nodesintNo1Number of intermediate nodes in path. 1 = 2-hop, 2 = 3-hop, 3 = 4-hop, and so on. Larger values search deeper but are exponentially more expensive and may exceed the query timeout.

Example Request#

{
  "entity_one_type": "Gene",
  "entity_one_name": "GST",
  "entity_two_type": "Disease",
  "entity_two_name": "Prostatic Neoplasms",
  "starting_date": "2020-01-01",
  "ending_date": "2025-12-31",
  "relation_type": "Positive_Correlation",
  "novelty": "-1",
  "enforce_refetch": "0",
  "intermediate_nodes": 1
}

Response (HTTP 202)#

Every request — whether cached or new — returns a queued response with a task_id:

{
  "status": "success",
  "session_id": "1776742838149_92397606-4e17-42be-8dfd-510b2a0c9d91_4288135032",
  "message": "Your indirect search request has been queued. Please poll the task_status endpoint with your task_id.",
  "credits_consumed": 10,
  "remaining_credits": 2860,
  "task_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901"
}

Next step: Poll task_status with the task_id until the status is "completed", then use the returned result_id with the details endpoints. Cached searches typically complete on the first poll.


5. POST /open_api/iexplore/v3/search_indirect_relations_details#

Retrieves saved indirect search results with evidence counts only per relationship. Lightweight endpoint for browsing path structure.

Request Body#

FieldTypeRequiredDefaultDescription
result_idstringYesThe result_id returned by search_indirect_relations or task_status
requested_paths_countintNo0Maximum number of paths to return, ordered by net_prob (product of hop probabilities) descending. 0 means return all. The per-relation portion of the bill is (total relation edges across returned paths) × per-relation price — a 2-hop path counts as 2 relations, a 3-hop path as 3 — so lowering this value lowers the credit cost of the call.

Example Request#

{
  "result_id": "69c4311b15cbc24ege5f56b6",
  "requested_paths_count": 10
}

Response#

{
  "status": "success",
  "session_id": "...",
  "paths_count": 42,
  "returned_count": 10,
  "credits_consumed": 30,
  "remaining_credits": 2738,
  "paths": [
    {
      "path_length": 2,
      "net_correlationType": "Positive_Correlation",
      "net_prob": 0.85,
      "nodes": [
        {"biokdeid": "7529", "name": "GSTK1", "officialName": "GSTK1", "commonName": "GST", "type": "Gene"},
        {"biokdeid": "12345", "name": "RPS6KB1", "officialName": "RPS6KB1", "commonName": "S6K1", "type": "Gene"},
        {"biokdeid": "2438", "name": "Prostatic Neoplasms", "officialName": "Prostatic Neoplasms", "commonName": "Prostatic Carcinoma", "type": "Disease"}
      ],
      "relationships": [
        {"relID": "7529.12345.30.1.NA.primeKG", "relationshipType": "Association", "correlationType": "Positive_Correlation", "direction": "NA", "score": 1.0, "prob": 0.95, "method": "MA", "source": "primeKG", "evidence_count": 0},
        {"relID": "12345.2438.27.1.NA.MP", "relationshipType": "Association", "correlationType": "Positive_Correlation", "direction": "NA", "score": 1.0, "prob": 0.89, "method": "MP", "source": null, "evidence_count": 8}
      ]
    }
  ]
}

6. POST /open_api/iexplore/v3/search_indirect_relations_evidences#

Retrieve evidence PMIDs for a single relationship in a saved indirect search result. This is a synchronous, paginated endpoint — PMIDs are fetched from Neo4j and returned immediately.

Request Body#

FieldTypeRequiredDefaultDescription
result_idstringYesThe result_id from search_indirect_relations or task_status
relIDstringYesThe relID of the relationship (from the details endpoint response)
pageintNo1Page number (1-indexed)
page_sizeintNo20Number of PMIDs per page (1–500)

Example Request#

{
  "result_id": "69c4311b15cbc24ege5f56b6",
  "relID": "7529.12345.30.1.NA.primeKG",
  "page": 1,
  "page_size": 20
}

Response (HTTP 200) — Text-mined hop#

{
  "status": "success",
  "session_id": "...",
  "result_id": "69c4311b15cbc24ege5f56b6",
  "relID": "12345.2438.27.1.NA.MP",
  "method": "MP",
  "source": null,
  "total_count": 12,
  "page": 1,
  "page_size": 20,
  "total_pages": 1,
  "credits_consumed": 30,
  "remaining_credits": 2708,
  "evidences": [
    {"pmid": "28651234", "probability": 0.97, "pub_date": "2017-04-12"},
    {"pmid": "31045678", "probability": 0.89, "pub_date": "2019-05-08"},
    {"pmid": "33217890", "probability": 0.82, "pub_date": "2020-11-19"}
  ]
}

Response (HTTP 200) — Curated hop#

For hops with method == "MA", the endpoint short-circuits with an empty evidences list and returns the curated source so the client can render an appropriate message. Only the base price is charged (no per-evidence charge when no evidences are returned).

{
  "status": "success",
  "session_id": "...",
  "result_id": "69c4311b15cbc24ege5f56b6",
  "relID": "7529.12345.30.1.NA.primeKG",
  "method": "MA",
  "source": "primeKG",
  "total_count": 0,
  "page": 1,
  "page_size": 20,
  "total_pages": 0,
  "credits_consumed": 10,
  "remaining_credits": 2698,
  "evidences": []
}

Note: For text-mined hops, evidences are deduplicated per PMID and ordered by highest evidence probability. When a PMID supports the hop in multiple sentences, the highest sentence-level probability is returned. All matching evidences are available (no cap) — use pagination to retrieve them. The endpoint returns evidence for one hop in the indirect path, not aggregated across the full path.

Indirect Relations — Field Reference#

Path Object#

FieldTypeDescription
path_lengthintNumber of hops in the path
net_correlationTypestringNet correlation across the path ("Positive_Correlation" or "Negative_Correlation")
net_probfloatNet probability across the path
nodesarrayOrdered list of nodes (start → intermediate(s) → end)
relationshipsarrayList of relationships along the path

Node Object#

FieldTypeDescription
biokdeidstringIKraph_API entity ID
namestringDisplay name (officialName or commonName)
officialNamestringOfficial entity name
commonNamestringCommon entity name
typestringEntity type label (e.g., Gene, Disease)

Relationship Object (details endpoint)#

FieldTypeDescription
relIDstringRelationship identifier. Use this value in evidence requests.
relationshipTypestringe.g., Association, Positive_Correlation
correlationTypestring"Positive_Correlation" or "Negative_Correlation"
directionstring"12", "21", "NA", or "0"
scorenumberRelationship confidence score
probfloatRelationship probability
methodstring"MP" for text-mined, "MA" for curated, "RNASeq" for RNA-Seq derived
sourcestring | nullCurated source name ("Hetionet", "primeKG", "PubChem", "GO_annotation", "TTD") when method == "MA". null for text-mined.
evidence_countintNumber of evidence locations (after filters). Always 0 for curated hops (method == "MA").
relation_seq_idintSequential ID within the result

How It Works#

  1. Entity resolution: Entity names are resolved to biokdeid values via Neo4j
  2. Graph traversal: A multi-hop Cypher query finds intermediate nodes connecting the two entities through RELATED_TO relationships in Neo4j
  3. Cypher-level filters: relation_type and direction filters are applied in the Cypher WHERE clause for performance. When a date filter is set, a per-hop minDate/maxDate predicate is appended; this also drops paths where any hop is curated (no per-relation dates) — see Curated vs Text-Mined Relations.
  4. Post-processing filters: novelty and date range filters are applied in Python on the locations JSON of each hop. If any hop is curated and a date/novelty filter is active, the entire path is dropped.
  5. Curated hop handling: When no date/novelty filter is active, paths containing curated hops are kept; the curated hops carry evidence_count = 0 and a source field naming the curated database.
  6. Deduplication: Paths through the same intermediate node are deduplicated

Task Status#

7. POST /open_api/iexplore/v3/task_status#

Poll the status of an async search task. Use this after receiving a task_id from search_direct_relations or search_indirect_relations.

Request Body#

FieldTypeRequiredDescription
task_idstringYesThe task_id returned by the search endpoint

Example Request#

{
  "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

Response (Pending/Processing)#

{
  "status": "success",
  "session_id": "...",
  "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "search_type": "direct",
  "created_at": "2026-03-16T10:30:00"
}
{
  "status": "completed",
  "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "search_type": "direct",
  "created_at": "2026-03-16T10:30:00",
  "result_id": "69b3200a04bac13dfd4f45a5",
  "relations_count": 117,
  "message": "Search completed. Use the result_id to retrieve details."
}
{
  "status": "completed",
  "task_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "search_type": "indirect",
  "created_at": "2026-03-16T10:30:00",
  "result_id": "69c4311b15cbc24ege5f56b6",
  "paths_count": 42,
  "message": "Search completed. Use the result_id to retrieve details."
}

Response (Failed)#

{
  "status": "failed",
  "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "search_type": "direct",
  "created_at": "2026-03-16T10:30:00",
  "error": "Error description"
}

Response (Task Not Found — HTTP 404)#

{
  "error": "Task not found. It may have expired (tasks expire after 48 hours)."
}

Response (Rate Limited — HTTP 429)#

{
  "message": "Polling too frequently. Please wait at least 1 second between polls for the same task.",
  "session_id": "..."
}

The gateway enforces a minimum interval of 1 second between polls for the same task_id. Requests that arrive within 1 second of a prior poll on the same task_id are rejected with HTTP 429 and a Retry-After: 1 response header. The reject path is intentionally cheap — it does not call the downstream search service and does not consume credits — but a cooperative client should still:

  1. Honor the Retry-After header (value in seconds) before retrying.
  2. Avoid concurrent polls for the same task_id from multiple workers, threads, or async tasks.
  3. Pace polls at ≥ 3 seconds (as shown in the Python example below), comfortably above the 1-second floor.

Note: the rate cap is per task_id, not per API key. Polling multiple distinct task_ids in parallel is unaffected.

Task Lifetime & Recovery After Expiry#

  • Task records (task_id → status) live in Redis with a 48-hour TTL, starting at submission time. The TTL is not refreshed by polling. After 48 hours, task_status returns HTTP 404 as shown above and the task_id is permanently gone.
  • Search results (result_id → relations/paths) are persisted in MongoDB with no expiry. Once a search has completed, the result_id remains valid for the details and evidence endpoints indefinitely.
  • Evidence responses are served from Neo4j + Redis caches. The Redis evidence cache has a 24-hour TTL by default but is transparent — a miss simply re-queries Neo4j.

Recovery recipe when task_status returns 404:

  1. If you still have the result_id from a prior task_status completion response: proceed directly to the details/evidence endpoints — no need to re-submit.
  2. If you never captured the result_id: re-submit the original search with the same parameters and enforce_refetch: "0". The backend recognizes the repeated search via its own history cache and resolves the new task immediately on the first poll (see Search Flow), returning the original result_id. Note that this re-submission still costs the base price, unless the original submission happened within the last 5 minutes and the gateway's user-level response cache is still warm.

Tip: Capture and persist result_id as soon as task_status reports "completed". As long as you hold either a valid task_id (≤48 h old) or the result_id, recovery is free: task_status will return the result_id again from the task_id, and the result_id remains valid indefinitely. Losing both (or losing the result_id after the 48-hour task TTL) forces a paid re-submission.


Path Search#

Path search finds routes between entities in the knowledge graph. Both endpoints are async (return a task_id) and store results in the indirect result store — poll task_status, then fetch with search_indirect_relations_details and search_indirect_relations_evidences using the returned result_id.

POST /open_api/iexplore/v3/search_path#

Finds the shortest (highest-probability) path — or the k shortest paths — between two entities using the Neo4j Graph Data Science (GDS) plugin. Edge weight is derived from relation probability, so the "shortest" path maximizes the product of per-hop probabilities.

Requires the GDS plugin installed on the graph server.

Request Body#

FieldTypeRequiredDefaultDescription
entity_one_type / entity_two_typestringYesEntity types (Chemical, Disease, Gene)
entity_one_name / entity_two_namestringCond.Entity names. Each is required unless the matching entity_*_id is given.
entity_one_id / entity_two_idstringNo""Pre-resolved BioKDE IDs (skip name resolution)
entity_one_species / entity_two_speciesstringNo"Homo Sapiens"Species context
is_drug_one / is_drug_twostringNo"0"Scope endpoint to drug entities
is_true_namestringNo"1"Name-resolution mode (see get_biokde_id)
kintNo1Number of shortest paths (Yen's algorithm when > 1; max 10)
limitintNo100Maximum paths to return
relation_typestringNo"""Positive_Correlation" / "Negative_Correlation" net-correlation filter
directionstringNo"""12" / "21" direction filter applied per hop
noveltystringNo"-1"Novelty filter
starting_date / ending_datestringNo""Date-range filter (YYYY-MM-DD)
enforce_refetchstringNo"0"Bypass cached result

Example Request#

{
  "entity_one_type": "Chemical", "entity_one_name": "aspirin",
  "entity_two_type": "Disease", "entity_two_name": "colorectal cancer",
  "k": 3
}

Response (HTTP 202)#

{ "task_id": "…", "status": "pending" }

POST /open_api/iexplore/v3/search_specific_path#

Searches for paths that match an explicit hop structure you specify: a root node and an ordered list of hops. Each hop offers one or more match alternatives (matched with OR); a path qualifies if every hop matches one of its alternatives.

Request Body#

FieldTypeRequiredDefaultDescription
rootobjectYes{ "id": "<biokde_id>" } — the starting node
hopsarrayYesOrdered list of hop objects (see below)
limitintNo100Maximum paths to return
noveltystringNo"-1"Novelty filter
starting_date / ending_datestringNo""Date-range filter
enforce_refetchstringNo"0"Bypass cached result

Each hop is { "match": [ <alternative>, … ] }. An <alternative> is either:

  • { "relID": "<relID>" } — match one specific edge by id; or
  • { "edge": { "type", "direction", "starting_year", "end_year" }, "node": { "type", "name", "id" } } where all sub-fields are optional:
    • edge.type — relation type name (see Relation Type Reference)
    • edge.direction"12" (root-side → next), "21" (next → root-side), "0" (either)
    • edge.starting_year / edge.end_year — keep edges within the year range
    • node.type — entity type of the hop's target node
    • node.name — target node official/common name
    • node.id — target node BioKDE id

Example Request#

{
  "root": { "id": "12345" },
  "hops": [
    {
      "match": [
        { "edge": { "type": "treats", "direction": "12", "starting_year": 2010 }, "node": { "type": "Gene" } },
        { "edge": { "type": "causes", "direction": "21", "end_year": 2020 }, "node": { "type": "Disease", "name": "entity_name2" } },
        { "relID": "relid_1234" }
      ]
    }
  ]
}

Response (HTTP 202)#

{ "task_id": "…", "status": "pending" }

Functional / Metadata APIs#

These endpoints return metadata and documentation. They are authenticated but free of charge (no credits consumed) and require no async task.

GET /open_api/iexplore/v3/readme#

Returns this API documentation as raw markdown (text/markdown). Pass ?format=json to receive it as { "readme": "<markdown>" }.

GET /open_api/iexplore/v3/entity-type#

Returns a JSON array of all valid entity type names, e.g. ["Anatomy", "Biological Process", "Chemical", "Disease", "Gene", …].

POST /open_api/iexplore/v3/entity-type-detail#

Returns definitions for the requested entity types. Body: { "name": ["Gene", "Chemical"] } (a single name is allowed). Response:

[
  { "name": "Gene", "definition": "A segment of DNA that encodes a functional product … Examples: TP53, BRCA1, EGFR." },
  { "name": "Chemical", "definition": "A chemical substance or small molecule … Examples: aspirin, glucose, cisplatin." }
]

Unknown names return {"name": "<name>", "definition": null}.

GET /open_api/iexplore/v3/relation-type#

Returns a JSON array of all valid relation type names, e.g. ["Association", "Positive_Correlation", "causes", "treats", …].

POST /open_api/iexplore/v3/relation-type-detail#

Returns definitions for the requested relation types. Body: { "name": ["treats", "causes"] }. Response mirrors entity-type-detail.


Entity Resolution#

8. POST /open_api/iexplore/v3/get_biokde_id#

Resolves an entity name to its BioKDE ID. Useful for verifying that an entity exists in the knowledge graph before submitting a search, or for looking up internal IDs. Has two modes, selected by is_true_name:

  • is_true_name="1" (default): treat the input as the entity's canonical/true name and return the single best match — backward-compatible response shape.
  • is_true_name="0": treat the input as a possible synonym and return a paginated, ranked list of every matching entity ("contains anywhere", case-insensitive) so the caller can disambiguate before searching.

Request Body#

FieldTypeRequiredDefaultDescription
entity_typestringYesEntity type (see Entity Types)
entity_namestringYesName of the entity to resolve
entity_speciesstringNo"Homo Sapiens"Species context for the entity (used when entity_type="Gene")
is_drugstringNo"0""1" scopes the lookup to drug entities — requires entity_type Chemical or Gene (see is_drug)
is_true_namestringNo"1""1" = exact canonical-name match, single result. "0" = fuzzy synonym search, ranked candidate list
pageintNo1Candidate-list page number. Only used when is_true_name="0"
page_sizeintNo20Candidates per page (1–100). Only used when is_true_name="0"
sortstringNo"mentions"Candidate ordering: "mentions" (mention count desc) or "name" (alphabetical). Only used when is_true_name="0"

Example Request (is_true_name="1", exact match)#

{
  "entity_type": "Gene",
  "entity_name": "TP53",
  "entity_species": "Homo Sapiens"
}

Response (HTTP 200)#

{
  "status": "success",
  "session_id": "...",
  "biokde_id": "7157",
  "entity_type": "Gene",
  "entity_name": "TP53",
  "entity_species": "Homo Sapiens",
  "is_drug": "0",
  "credits_consumed": 10,
  "remaining_credits": 2688
}
{
  "entity_type": "Chemical",
  "entity_name": "aspirin",
  "is_true_name": "0",
  "page": 1,
  "page_size": 20,
  "sort": "mentions"
}

Response (HTTP 200, is_true_name="0")#

{
  "status": "success",
  "session_id": "...",
  "entity_type": "Chemical",
  "entity_name": "aspirin",
  "entity_species": "Homo Sapiens",
  "is_drug": "0",
  "is_true_name": "0",
  "sort": "mentions",
  "total_count": 3,
  "page": 1,
  "page_size": 20,
  "total_pages": 1,
  "candidates": [
    {
      "biokde_id": "2244",
      "name": "aspirin",
      "official_name": "Aspirin",
      "type": "Chemical",
      "subtype": "Drug",
      "species": null,
      "cnt": 15234
    }
  ],
  "credits_consumed": 10,
  "remaining_credits": 2678
}

Each entry in candidates is one distinct entity (biokde_id), collapsed from every matching synonym down to its highest-mention-count name. Pick a biokde_id from this list and pass it as entity_one_id / entity_two_id to a search endpoint to skip name resolution entirely.

Response (Not Found — HTTP 404)#

{
  "error": "Entity not found: TP53X (Gene)"
}

Note: is_true_name="1" uses cascading name matching against MongoDB: first tries exact match on the synonym name field, then a capitalized variant, then exact match on official name. is_true_name="0" does a case-insensitive substring match across all synonyms of the given entity_type (and species/is_drug scope, if set) — spaces included, so a query can match across word boundaries in a multi-word synonym.


Error Responses#

StatusCondition
400Invalid signature, validation error, or malformed request
401Authentication failed (invalid/missing API key)
403User does not have permission for this service
404Entity not found in the knowledge graph, or task_id expired
500Neo4j query error, MongoDB error, or internal server error
503Unable to verify user permission (User Center unavailable)

Sample Script (Python)#

A complete example showing the async search flow and synchronous evidence retrieval.

import requests
import json
import time
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

BASE_URL = "https://service.insilicom.com"
API_KEY = "YOUR_API_KEY"
HEADERS = {"Content-Type": "application/json", "X-API-KEY": API_KEY}


def search_direct_relations():
    """Step 1: Submit a direct relation search."""
    url = f"{BASE_URL}/open_api/iexplore/v3/search_direct_relations"
    payload = {
        "entity_one_type": "Disease",
        "entity_one_name": "Lung Cancer",
        "entity_two_type": "Chemical",
        "entity_two_name": "",
        "relation_type": ["Negative_Correlation"],
        "relation_direction": "21",
        "novelty": "-1",
        "start_date": "1999-01-01",
        "end_date": "2005-01-01",
        "exclude_sub_type": "1",
        "enforce_refetch": "0",
        "probability_threshold": 0.9
    }
    response = requests.post(url, headers=HEADERS, json=payload, verify=False)
    print(f"=== Search Direct Relations (HTTP {response.status_code}) ===")
    result = response.json()
    print(json.dumps(result, indent=2))
    return result


def poll_task_status(task_id):
    """Step 2: Poll task_status until completed or failed."""
    url = f"{BASE_URL}/open_api/iexplore/v3/task_status"
    print(f"\nPolling task {task_id}...")
    while True:
        response = requests.post(url, headers=HEADERS, json={"task_id": task_id}, verify=False)

        # The gateway caps polls to one per second per task_id. Honor Retry-After.
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", "1"))
            print(f"  Rate limited; retrying in {retry_after}s")
            time.sleep(retry_after)
            continue

        result = response.json()
        status = result.get("status", "")
        print(f"  Status: {status}")

        if status == "completed":
            print(f"  Result ID: {result.get('result_id')}")
            print(f"  Relations: {result.get('relations_count', result.get('paths_count', 0))}")
            return result
        elif status == "failed":
            print(f"  Error: {result.get('error')}")
            return result
        else:
            time.sleep(3)


def get_details(result_id):
    """Step 3: Retrieve results (evidence counts only)."""
    url = f"{BASE_URL}/open_api/iexplore/v3/search_direct_relations_details"
    response = requests.post(url, headers=HEADERS, json={"result_id": result_id}, verify=False)
    print(f"\n=== Details (HTTP {response.status_code}) ===")
    result = response.json()
    print(json.dumps(result, indent=2))
    return result


def get_evidences(result_id, rel_id, page=1, page_size=20):
    """Step 4: Retrieve evidence PMIDs for a single relation (synchronous, paginated)."""
    url = f"{BASE_URL}/open_api/iexplore/v3/search_direct_relations_evidences"
    payload = {
        "result_id": result_id,
        "relID": rel_id,
        "page": page,
        "page_size": page_size
    }
    response = requests.post(url, headers=HEADERS, json=payload, verify=False)
    print(f"\n=== Evidences (HTTP {response.status_code}) ===")
    result = response.json()
    print(json.dumps(result, indent=2))
    return result


if __name__ == "__main__":
    # Step 1: Submit search — always returns a task_id (cached searches complete on first poll)
    search_result = search_direct_relations()

    task_id = search_result.get("task_id")
    if not task_id:
        print("No task_id returned. Check errors above.")
        exit(1)

    # Step 2: Poll task_status until the task resolves
    task_result = poll_task_status(task_id)
    result_id = task_result.get("result_id")

    if not result_id:
        print("Search failed. No result_id available.")
        exit(1)

    # Step 3: Retrieve details (evidence counts only)
    details = get_details(result_id)

    # Step 4: Pick a relID from the details response and retrieve evidence PMIDs
    relations = details.get("relations", [])
    if relations:
        # Handle both single-entity (list of dicts) and sub-type (grouped) formats
        first = relations[0]
        if isinstance(first, list) and first:
            rel_id = first[0].get("relID")
        elif isinstance(first, dict) and "relations" in first:
            rel_id = first["relations"][0].get("relID")
        else:
            rel_id = first.get("relID")

        if rel_id:
            get_evidences(result_id, rel_id, page=1, page_size=20)