← QENEX Lab

ICH M7 assessment API

You submit the two (Q)SAR methodologies ICH M7(R2) requires. QENEX returns the class, the acceptable intake in µg/day, the control action, and a signed pack your reviewer can verify on their own machine.

QENEX does not supply a (Q)SAR. The intended path is that you send results from the systems your SOP already validates — Derek Nexus, Leadscope, or equivalent. If you omit the expert result we fall back to our own structural-alert screen, measured on the Hansen benchmark at 6,483 of 6,506 compounds given a call: sensitivity 0.726, specificity 0.620 — below the commercial systems most filers license. Send your own.

1. Authenticate

Every call needs a bearer token from the qenex realm carrying the lab-researcher scope. Accounts are provisioned by hand; request one from the portal. Tokens are obtained through your normal OIDC flow against https://auth.qenex.ai/realms/qenex.

export QENEX_TOKEN="<your bearer token>"

A call without the scope returns 403; without a token, 401. Neither is a server fault and neither should be retried unchanged.

2. Submit a batch

A batch is normally the impurity profile of one drug product on one clinical programme, so the treatment duration sits at batch level. Up to 500 compounds per pack.

curl -X POST https://lab.qenex.ai/api/v1/lab/ich-m7/batch \
  -H "Authorization: Bearer $QENEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "batch_id": "DS-2026-014-revB",
    "treatment_dosing_days": 180,
    "compounds": [
      { "compound_id": "IMP-01", "smiles": "CCOS(=O)(=O)C",
        "expert":      { "model_name": "Derek Nexus", "model_version": "6.3", "call": "positive" },
        "statistical": { "model_name": "Leadscope",   "model_version": "2.1", "call": "positive" } },
      { "compound_id": "IMP-02", "smiles": "CCO",
        "expert":      { "model_name": "Derek Nexus", "model_version": "6.3", "call": "negative" },
        "statistical": { "model_name": "Leadscope",   "model_version": "2.1", "call": "negative" } },
      { "compound_id": "IMP-03", "smiles": "Nc1ccccc1",
        "expert":      { "model_name": "Derek Nexus", "model_version": "6.3", "call": "positive" },
        "statistical": { "model_name": "Leadscope",   "model_version": "2.1", "call": "negative" } }
    ]
  }'

treatment_dosing_days

Count the days actually dosed, not the calendar span. The guideline is explicit, and works the example itself: once weekly for two years is 104 dosing days and carries the 20 µg/day limit, not the 10 µg/day two years of elapsed time would suggest. A per-compound treatment_dosing_days overrides the batch value for a degradant that also appears in another product.

Dosing daysBandAcceptable intake
≤ 30<1 month120 µg/day
31 – 365>1 – 12 months20 µg/day
366 – 3,650>1 – 10 years10 µg/day
> 3,650>10 years to lifetime1.5 µg/day

Boundaries are upper-inclusive per section 7.3.1 (“up to 1 month”), which is the permissive reading of a table whose printed bands leave each exact boundary uncovered. Days per month (30) and per year (365) are our conversion, not the guideline’s; if your SOP converts differently near a boundary you will land in a different band.

3. Read the response

The example above returns, verbatim:

CompoundClassAcceptable intakeStatus
IMP-01320.0 µg/daycompliant
IMP-025compliant
IMP-03incomplete

IMP-02 has no limit because none applies. Class 5 is treated as a non-mutagenic impurity, controlled under ICH Q3A/Q3B instead. Class 1 also returns no number: a known mutagenic carcinogen requires a compound-specific limit and the guideline does not sanction a TTC for one. Read control_strategy.basis to tell the cases apart — not_a_mutagenic_impurity, compound_specific_required, or ttc_pending_duration.

IMP-03 has no class because its two methodologies disagree. ICH M7 requires documented expert review when they do, so the pack names it in unresolved with the reason rather than picking one call. The pack status is incomplete while any compound is. Supply an expert_review block with a reviewer id, a rationale and a conclusion, and resubmit.

"expert_review": {
  "reviewer_id": "j.smith@yourcompany.com",
  "conclusion": "negative",
  "rationale": "<at least 40 characters of reasoning, which is stored verbatim in the record>"
}

4. Verify the pack

The pack is the deliverable. Its record_ids are content hashes and its pack_hash covers the ordered set, so an edited limit and a dropped compound are both detectable — by different checks.

curl -O https://lab.qenex.ai/verify/verify_ich_m7_pack.py
python3 verify_ich_m7_pack.py pack.json
RECORD INTEGRITY  ok        3 of 3 hash to their record_id
SET INTEGRITY     ok        no compound added, removed or reordered
SIGNATURE         ok        valid -- signer qenex-lab

This pack is as issued.

It runs offline on the standard library, so your reviewer or your regulator can check the document without a QENEX account and without asking us whether our own document is genuine. Every pack carries a how_to_verify block repeating this.

Integrity is not correctness. A pack can verify perfectly and still record an incomplete assessment, or one whose methodology you disagree with. Read status and blocking_reasons for that.

5. Errors

StatusMeaning
401No token, or not from the qenex realm.
403Token lacks the lab-researcher scope.
422Schema rejection. The body names the field — a treatment_dosing_days of 0 or a malformed call lands here.
400Accepted shape, rejected content: an unparseable SMILES, or a batch-level duration every compound would fail on.
503The compute node is unreachable. A dependency outage, not your input — retry unchanged.

A single malformed compound does not fail the batch: it is named in errors with its reason and the others are assessed. A malformed batch-level field does fail the call, once, rather than returning the same message for every compound.

Reference

Guideline: ICH M7(R2), Step 4, 2023-02-16. Every limit above is transcribed from it and checked against the pinned document in our test suite, not written from memory.