API guide
Base URL: https://www.open-morals.com/v1. JSON over HTTPS. The API version is in the path (/v1); changes within it are additions only.
Authentication
Send Authorization: Bearer om_live_… on POST /v1/classify. GET /v1/meta and GET /v1/health are public.
Keys are issued by the project, one for each app that connects. A key has its own rate limit and can be revoked. To run without a key, download the weights: see Models.
Quick start
curl https://www.open-morals.com/v1/meta
curl https://www.open-morals.com/v1/classify \
-H "Authorization: Bearer om_live_…" \
-H "Content-Type: application/json" \
-d '{"blocks":[{"id":"a","text":"He kept the money for himself."}]}'const response = await fetch("https://www.open-morals.com/v1/classify", {
method: "POST",
headers: { authorization: "Bearer om_live_…", "content-type": "application/json" },
body: JSON.stringify({ blocks: [{ id: "a", text: "He kept the money for himself." }] }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { results, labels, provenance } = await response.json();
console.log(results[0].scores, provenance.model);Routes
GET /v1/metareturns everything a client needs to attach the observer: its identity, the 8 labels in order with their groups and meanings, how scores are to be read, the model and revision, and the limits.POST /v1/classifyscores blocks of text. One result per block, in request order.GET /v1/healthreports whether the observer is up and whether the model is warm.
Classify
Request
{
"blocks": [
{ "id": "a", "text": "He kept the money for himself.", "before": "…", "after": "…" }
],
"client": { "name": "my-app" }
}blocks: 1 to 32 blocks. Each hastext(up to 4,000 characters) and an optionalidthat is echoed back.beforeandafter: optional context, up to 600 characters each. It is accepted but not scored.client.name: optional, to identify your app in our logs.
Response
{
"results": [
{ "id": "a", "scores": [0.02, 0.01, …] }
],
"labels": "mfrc@1",
"provenance": {
"observer": { "id": "open-morals", "version": "0.2.0" },
"implementation": "…",
"model": { "id": "…", "revision": "…" },
"requestId": "req_…",
"at": "2026-01-01T00:00:00.000Z",
"durationMs": 120
},
"usage": { "blocks": 1, "scored": 1, "chars": 30 }
}scoresholds 8 numbers from 0 to 1, in the label order that/v1/metareturns: the six foundations, then thin morality and non-moral.labelsnames the label set they belong to.- A block that couldn't be scored has
"scores": nullwith areasonandretryable. A blank block has the reasonempty. provenancenames the model and revision that produced the scores. Store it with them.
Reading the scores
Each score is the model's confidence that the text is framed in terms of that foundation. The labels are independent, so several can be high at once. Text with no moral framing scores high on non_moral. A score is not an intensity, and not a judgement of right and wrong. See limitations.
Errors
Every error has the same shape. retryable says whether the same request can succeed later.
{
"error": {
"code": "rate_limited",
"message": "Too many requests for this key. Try again in 12 seconds.",
"retryable": true,
"retryAfterSeconds": 12,
"requestId": "req_…"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The body isn't JSON, or a field is missing, the wrong type or too long. |
| 400 | unknown_implementation | The request named an implementation this observer doesn't offer. |
| 401 | unauthorized | The key is missing, malformed, unknown or revoked. |
| 413 | payload_too_large | The body is over 256 KB. |
| 429 | rate_limited | Too many requests for this key. Wait for the Retry-After header. |
| 502 | upstream_error | The model couldn't score any block. Check retryable before trying again. |
| 503 | model_loading | The model is starting. Wait for the Retry-After header. |
| 503 | unavailable | The observer isn't configured to classify yet. |
| 500 | internal | Something went wrong on our side. |
Limits
- 60 requests per minute for each key.
- 32 blocks per request, 4,000 characters per block, 256 KB per request body.
Browsers
The API answers cross-origin requests from any origin and never uses cookies. A key in browser code is visible to anyone, so call the API from your server.