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.
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.
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" } }
]
}'
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 days | Band | Acceptable intake |
|---|---|---|
| ≤ 30 | <1 month | 120 µg/day |
| 31 – 365 | >1 – 12 months | 20 µg/day |
| 366 – 3,650 | >1 – 10 years | 10 µg/day |
| > 3,650 | >10 years to lifetime | 1.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.
The example above returns, verbatim:
| Compound | Class | Acceptable intake | Status |
|---|---|---|---|
| IMP-01 | 3 | 20.0 µg/day | compliant |
| IMP-02 | 5 | — | compliant |
| IMP-03 | — | — | incomplete |
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>"
}
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.
status and blocking_reasons for that.
| Status | Meaning |
|---|---|
| 401 | No token, or not from the qenex realm. |
| 403 | Token lacks the lab-researcher scope. |
| 422 | Schema rejection. The body names the field — a
treatment_dosing_days of 0 or a malformed call lands here. |
| 400 | Accepted shape, rejected content: an unparseable SMILES, or a batch-level duration every compound would fail on. |
| 503 | The 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.
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.