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

Classify

Request

{
  "blocks": [
    { "id": "a", "text": "He kept the money for himself.", "before": "…", "after": "…" }
  ],
  "client": { "name": "my-app" }
}

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 }
}

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_…"
  }
}
StatusCodeMeaning
400invalid_requestThe body isn't JSON, or a field is missing, the wrong type or too long.
400unknown_implementationThe request named an implementation this observer doesn't offer.
401unauthorizedThe key is missing, malformed, unknown or revoked.
413payload_too_largeThe body is over 256 KB.
429rate_limitedToo many requests for this key. Wait for the Retry-After header.
502upstream_errorThe model couldn't score any block. Check retryable before trying again.
503model_loadingThe model is starting. Wait for the Retry-After header.
503unavailableThe observer isn't configured to classify yet.
500internalSomething went wrong on our side.

Limits

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.