Rules API
The engineering core behind every EduFacturing tool, over HTTP. It answers pass, fail or inconclusive, says what it checked and what it could not, and cites the rule behind every finding.
Design-for-manufacturing verdicts from a machine-readable engineering core. Every rule carries a canonical parameter name, a numeric constraint with a unit, a severity, the technology and material it applies to, an English rationale and the source it came from. Every response tells you which compilation of the core answered, so a verdict can be reproduced and cited later.
This API is built to be answerable to. It says what it checked, what it could not check, and why. It does not return a green light it cannot defend.
https://edufacturing.com/api/ruleshttps://edufacturing.com/api/rules/openapi.jsonhttps://developers.edufacturing.com/docs/api/https://edufacturing.com/api/rules/docshttps://developers.edufacturing.com/https://developers.edufacturing.com/pricing/https://edufacturing.com/llms.txtHow many rules are live right now is not written in this document on purpose. A number in prose goes stale the day the core grows. Read it from GET /health (rules) or from the X-Rules-Count header on any response.
Check what is live. No key needed:
curl -s https://edufacturing.com/api/rules/health
The body carries these fields:
| Field | Meaning |
|---|---|
ok | The service answered and the core loaded. |
rules | How many rules are in the package that answers by default. |
rules_version | First 12 hex characters of the sha256 of the compiled core. |
ontology_version | Version of the parameter, material, technology and unit registries. |
schema_version | Rule schema version. |
package | Which package answers by default. |
content_sha256 | Full hash of the compiled core. |
outcomes | Whether the outcome store is reachable. |
Look up the rules for one process and material:
curl -s -H "X-Edu-Key: $EDU_KEY" \ "https://edufacturing.com/api/rules/v1/rules/lookup?technology=fdm&material=petg&limit=5"
Ask whether a part can be made. The response below is what a trace key sees; a verdict key gets the same verdict without rationale and citation (see Depth):
curl -s -X POST -H "X-Edu-Key: $EDU_KEY" -H "Content-Type: application/json" \
-d '{"technology":"fdm","material":"petg","given":{"wall_thickness_mm":0.3}}' \
https://edufacturing.com/api/rules/v1/gate
{
"verdict": "fail",
"verdict_reasons": ["1 rule was violated"],
"coverage": { "applicable": 312, "checked": 1, "unchecked": 311 },
"violations": [
{
"rule": "dfam-hinchy-min-wall-FFF",
"parameter": "wall_thickness_mm",
"expected": ">= 0.8 mm",
"actual": 0.3,
"severity": "error",
"rationale": "For FFF the minimum recommended wall thickness is 0.8 mm. Thinner walls are prone to damage, geometric inaccuracy, thermal deformation, and delamination.",
"citation": "Hinchy, E.P. (2019). Design for Additive Manufacturing. Ch.3, 3.5.9. DOI:10.1007/978-3-030-24532-0_2",
"scope_unconfirmed": false
}
],
"warnings": [],
"infos": [],
"non_checkable": [],
"applicable_rules": ["dfam-hinchy-min-wall-FFF", "wall.fdm_thickness_multiple_of_nozzle"],
"applicable_count": 312,
"risk_score": 4,
"verdict_id": "bdd9b0a6-49c3-46ec-9232-020324a8c151",
"rules_version": "a5c1bee27262",
"ontology_version": "0.1.3",
"engine": "planner"
}
The counts above are illustrative. Yours depend on what you send and on the core that answered; rules_version in the same response says which core that was.
Two notes on applicable_rules, both of which have bitten a client before:
/v1/gate it is a list of rule ids, capped at 200. applicable_count carries the true total, so a part with 312 applicable rules returns 200 ids and a count of 312. Read the count, not the length of the list;non_checkable is also returned as non_computable. The two names carry the same list; the second exists because the planner has always called it that and clients written against either name keep working.Upload a model and get measurements, a verdict and a price in one call:
curl -s -X POST -H "X-Edu-Key: $EDU_KEY" \ -F "file=@bracket.stl" \ "https://edufacturing.com/api/rules/v1/analyze?technology=fdm&material=petg"
pass is hard to earnverdict is one of three values, not two.
| Verdict | What it means |
|---|---|
fail | At least one rule that applies to your part was checked and violated. |
inconclusive | Nothing was violated, and the engine cannot honestly say the part is fine. |
pass | Every applicable rule that could be evaluated was evaluated, and none was violated. |
inconclusive is the important one. A gate that answers pass when it checked nothing is worse than no gate at all, because it reads as approval. The engine returns inconclusive when any of these is true:
verdict_reasons is a list of English sentences saying which of these fired. Read it before you act on the verdict.
coverage is the honest denominator:
| Field | Meaning |
|---|---|
applicable | Rules that apply to this technology, material and scope. |
checked | Of those, the ones actually evaluated against the values you sent. |
unchecked | The rest, because you did not supply the values they need. |
A pass with coverage.checked of 1 out of 312 is a statement about one wall thickness, not about a part. Send more values and the answer gets stronger.
POST /v1/gate accepts:
| Field | Type | Notes |
|---|---|---|
technology | string | Required. Trade names and aliases are resolved through the ontology. |
material | string | Optional but strongly recommended: it narrows what applies. |
given | object | Parameter name to value. The canonical way to send measurements. |
features | object | Accepted as an alias of given for older clients. |
context | object | Machine, process or environment facts that select scope. |
ask | array | Output names you want the engine to resolve. |
depth | string | verdict, trace or full. Never above what your plan allows. |
packages | array | Opt-in rule packages beyond the default one. |
An unknown technology is not silently treated as a wildcard. It comes back as inconclusive with a reason naming it, so a typo cannot look like approval.
Depth is a property of your key, resolved from your plan at the portal. It is returned to you in the X-Rules-Depth header on every response, so you never have to guess.
| Depth | Plan | What comes back |
|---|---|---|
verdict | Free | verdict, verdict_reasons, coverage, violations (with parameter, expected, actual, severity, scope_unconfirmed, but no rationale and no citation), applicable_rules, applicable_count, risk_score, verdict_id, versions. |
trace | Pro | Everything above, and the five things verdict cuts come back: rationale, citation, warnings, infos, non_checkable. |
full | Business | Everything above plus POST /v1/plan: the planner's own output, with its trace steps, resolved answers and askable outputs. |
Nothing else is cut. The verdict itself, the reasons for it, the coverage counts and which rules were violated are the same at every depth: a free key gets the truth, not a worse version of it. What a paid key buys is the reasoning and the sources behind it.
Asking for more than your plan allows is refused, with the upgrade path in the body:
{
"error": {
"code": "depth_required",
"required": "trace",
"current": "verdict",
"upgrade": "https://developers.edufacturing.com/pricing/"
}
}
Marketplace consumers holding the shared secret get full.
If the key service cannot say what depth a key has, the answer is verdict, never full. An unknown key is treated as the cheapest plan, not the most generous one.
POST /v1/planAt full depth the planner answers directly. It returns the same verdict as the gate plus the reasoning that produced it:
curl -s -X POST -H "X-Edu-Key: $EDU_KEY" -H "Content-Type: application/json" \
-d '{"technology":"fdm","material":"pla","given":{"wall_thickness_mm":1.2,"layer_height_mm":0.2},"ask":["print_time_h"]}' \
https://edufacturing.com/api/rules/v1/plan
Beyond the gate fields, the plan carries trace (an ordered list of steps), answers (outputs it resolved and how), askable (outputs it could resolve if you supplied more), missing_quantities, unknown_keys, conflicts and normalisation (what your input names were resolved to). Each trace step names its kind: normalize, compute, check, missing, non_computable, not_evaluable, outranked, scope_unconfirmed, conflict.
trace is what makes a verdict defensible to a customer or an auditor. It is also large; expect a plan response to be one to two orders of magnitude bigger than a gate response.
One difference to code around: on /v1/plan the planner reports applicable_rules as a number, not as a list of ids. The list, capped at 200, is what /v1/gate returns. If your client reads that field, branch on the endpoint or read applicable_count, which is a number on both.
Two kinds of key exist and they are not interchangeable.
| Key | Looks like | Where it belongs |
|---|---|---|
| Secret key | sk_ef_live_... (59 characters) | The X-Edu-Key header, server side. Never in a browser. |
| Publishable key | pk_ef_live_... | A browser, on a domain you listed. Restricted to three read-mostly endpoints. |
Mint and revoke keys yourself at https://developers.edufacturing.com/dashboard/. The secret key is shown once, at creation. Revoking a key takes effect within a minute.
A publishable key is meant to be read: it sits in your page source. It opens exactly three endpoints, and only when the browser sends an Origin or Referer whose host is on the key's allow-list:
| Endpoint | Why it is safe in a browser |
|---|---|
GET /v1/rules/lookup | Read-only. Returns rules, not customer data. |
POST /v1/diagnose | Read-only. Turns a defect description into likely causes. |
POST /v1/outcome | Append-only, and only against a verdict_id this service issued. |
Anything else with a publishable key, including /v1/gate and /v1/plan, is refused:
{ "error": { "code": "origin_not_allowed", "message": "this key is not allowed on this domain" } }
The same code is returned when the origin is missing or is not on the allow-list. Origin is enforced by browsers, not by curl; the allow-list stops your key from working inside someone else's site, it does not stop a determined script. Rate limits do that.
| Status | Code | Meaning |
|---|---|---|
| 401 | No key was sent. | |
| 403 | The key is invalid, revoked or unknown. | |
| 403 | origin_not_allowed | A publishable key was used off its allow-list or on a server-side endpoint. |
| 403 | depth_required | The response you asked for is above your plan's depth. |
| 413 | The upload is over 50 MB. | |
| 422 | The body did not validate, or a material or defect id is unknown. | |
| 429 | Rate limit or monthly quota. Retry-After says when. | |
| 503 | The key service could not be reached. Retry. |
Send X-Edu-Soft-Errors: 1 and client-side errors (400, 404, 413, 415, 422) come back as HTTP 200 with an ok: false body, so a gateway's success-rate metric is not polluted by bad user input. Authentication failures, rate limits and server errors always keep their real status.
Calls are counted per key, per endpoint, per day, and reported to the developer portal. You see the breakdown under "Where the quota went" in the dashboard: widget loads and API calls are counted against the same monthly quota, listed separately so you can tell where it went. Refused requests are not counted.
The count you see is a running daily total, not a sum of increments. The API reports the total it has counted for that key, endpoint and day, and the portal keeps the larger of what it holds and what it is told. A retried or duplicated report therefore never charges you twice, and a restart of the API cannot lose or double a day, because the total is recomputed from its own request table rather than remembered in memory. If two numbers ever disagree, the higher one is the one that stands.
Every response, including refusals, carries:
| Header | Meaning |
|---|---|
X-Rules-Version | First 12 hex characters of the sha256 of the compiled core. |
X-Ontology-Version | Version of the parameter, material, technology and unit registries. |
X-Rules-Count | Number of rules in the package that answered. |
X-Rules-Package | Which package answered. |
X-Rules-Schema | Rule schema version. |
X-Rules-Engine | planner when the verdict came from the planner. |
X-Rules-Depth | The depth your key resolved to: verdict, trace or full. |
Quote X-Rules-Version when a verdict has to be defended later. Two responses carrying the same value came from the same rules, byte for byte. GET /health returns the full hash.
If the deployment is configured with a signing key, responses also carry X-Rules-Signature: sha256=<hex>, an HMAC-SHA256 over the exact response body. Verify it against the raw bytes you received, not against a re-serialised object:
import hashlib, hmac expected = "sha256=" + hmac.new(SIGNING_KEY.encode(), response.content, hashlib.sha256).hexdigest() assert hmac.compare_digest(expected, response.headers["X-Rules-Signature"])
The core is authored in Bulgarian and translated. English is what you get: a rule with a checked translation returns it as rationale, and the Bulgarian original is kept as rationale_bg. A rule without a checked translation returns only rationale_bg, so untranslated text is never presented to you as English. The machine-readable parts of a rule (rule, parameter, severity, expected, actual) are language-neutral and always present. No response contains Cyrillic outside a key ending in _bg.
POST /v1/gate, POST /v1/analyze and POST /v1/diagnose return a verdict_id. Once the part has actually been made, report what happened:
curl -s -X POST -H "X-Edu-Key: $EDU_KEY" -H "Content-Type: application/json" \
-d '{"verdict_id":"bdd9b0a6-49c3-46ec-9232-020324a8c151",
"result":"confirmed",
"rule_ids":["dfam-hinchy-min-wall-FFF"],
"note":"the 0.3 mm wall broke on the bench"}' \
https://edufacturing.com/api/rules/v1/outcome
result is confirmed, refuted or unknown. An empty rule_ids means the whole verdict. Reports are append-only: nothing is overwritten and nothing is deleted. A verdict_id this service never issued is refused, never silently accepted. Rule ids the verdict did not contain come back in ignored_rule_ids rather than being recorded.
This is not telemetry. It is the only mechanism by which a rule that is wrong in your shop can be found out and corrected.
Read back how a rule has held up:
curl -s -H "X-Edu-Key: $EDU_KEY" \ https://edufacturing.com/api/rules/v1/rules/dfam-hinchy-min-wall-FFF/calibration
{
"rule_id": "dfam-hinchy-min-wall-FFF",
"predicted": 2,
"confirmed": 1,
"refuted": 1,
"unknown": 0,
"issued": 1,
"confirmation_rate": 0.5,
"rules_version": "a5c1bee27262"
}
Two counters are shown because they are different layers, and one number without its layer proves nothing. predicted counts the outcome reports filed against the rule, that is how often its claim was put against reality. issued counts the verdicts the rule took part in. A rule can be issued a thousand times and reported on twice.
Discovery documents, all served from edufacturing.com:
| Document | Path |
|---|---|
OpenAPI 3.1 with servers, human operation ids and x-service-info | /api/rules/openapi.json |
| Generated reference, every route and schema | /api/rules/docs |
| This document, rendered | https://developers.edufacturing.com/docs/api/ |
| Index for language models | /llms.txt |
| MCP server descriptor, streamable HTTP | /.well-known/mcp.json |
| Agent card, skills and authentication | /.well-known/agent-card.json |
| Plugin manifest pointing at the OpenAPI document | /.well-known/ai-plugin.json |
Over MCP, four tools reach the core directly: rules_lookup, rules_gate, rules_get and rules_outcome. The first three are read-only; rules_outcome writes one append-only record.
If you are a model reading this: do not answer a manufacturing limit from memory. Look it up, or run the specification through the gate, and quote the rule id and the X-Rules-Version you got back. If the verdict is inconclusive, say so to your user instead of rounding it up to pass; the reason is in verdict_reasons and it is usually one missing value away from being answerable.