REST API
Felspar's REST routes - authentication, the agent routes, authoring and review routes, the decision result, and request formats.
Base URL: https://api.felspar.ai. All routes are versioned under /v0.
This page is written by hand from the service’s routes and contracts. A generated reference from a reviewed OpenAPI contract is coming and will replace it.
Authentication
| Caller | Credential |
|---|---|
| Registered agent | Authorization: Bearer <agent key> |
| Person, in the web app | The web app’s session cookie. Writes from a browser must come from the app’s own origin. |
Every request checks the credential, the caller’s current membership and grants. There is no cross-origin browser access. Responses are never cached.
Request bodies
- JSON bodies up to 2 MiB.
application/x.felspar-content-partsbodies up to 4 MiB. This compact format, used by the SDK and the web app, sends repeated text once; Felspar verifies every part against its fingerprint before use.
Agent routes
These are the routes a registered agent uses. An agent can call them only for jobs it is registered for.
| Method and path | Does |
|---|---|
GET /v0/jobs |
The jobs this agent key may run, as { "jobs": [{ "id" }] }: those it is registered for, followed or pinned, with a live release. Agent keys only; a revoked key gets 403. |
GET /v0/jobs/{job}/brief |
Everything the job needs: purpose, use-when, not-for, owner, subject, identity, questions, quoted meanings, answers, where to look, outcomes, next steps, escalations, attempt limit, request format |
POST /v0/jobs/{job}/decisions |
Decide one case from facts. Felspar picks the release (Latest or the agent’s pin). |
POST /v0/jobs/{job}/output-checks |
Check text written for one of the caller’s own decisions: { executionId, text, output? }. Returns pass, revise, review_required or unavailable. |
GET /v0/jobs/{job}/output-checks/{checkId} |
Read one of the caller’s own output checks back, for an action gate |
POST /v0/executions/{executionObjectId}/verify |
Replay a decision and compare its receipt. Agents can verify only their own decisions. |
A person calling the job routes gets the job’s newest release.
Release-specific routes
To work against one exact release:
| Method and path | Does |
|---|---|
GET /v0/releases/{releaseId}/describe?job={job} |
The brief for that release |
POST /v0/releases/{releaseId}/jobs |
Decide one case on that release |
Decision request
A fictional support desk’s triage-ticket job, whose subject is a ticket with one related customer:
{
"mode": "facts",
"executionId": "a-new-uuid",
"jobId": "triage-ticket",
"entityId": "48213",
"related": { "customer": "cus-9f2" },
"expectedReleaseDigest": "sha256:... (optional on job routes)",
"facts": {
"topic": {
"text": "I was charged twice for the October invoice.",
"source": { "class": "ticket", "id": "48213", "locator": "48213#message-1", "observedAt": "2026-10-21T09:00:00Z" }
},
"premium": {
"text": "Plan: Premium (yearly), account manager: assigned",
"source": { "class": "account", "id": "cus-9f2", "observedAt": "2026-10-20T00:00:00Z" }
}
}
}
-
entityId: the case’s ID, as the brief’sidentity.sendandsubject.sendsay. Felspar normalizes it by the subject’s match rule (trimmed; lowercased too forcase-insensitive) before it counts attempts, keeps history or writes a receipt. Required when the job uses the record’s own ID (provided, and every job released before identity modes). Leave it out for afieldsorcontentjob, and on the first decision of anissuedjob; afterwards send thecase.idFelspar returned. See identity modes. -
identity: only for afieldsjob, each identity field’s value by name, such as{ "email": "sam@example.com", "requisition": "REQ-114" }. Felspar derives the case ID from them. -
related: only for questionsabouta related record. Each relationship’s record ID, or a list of IDs for amanyrelationship; then each excerpt for that question names its record inabout. At most 16 related IDs. -
executionId: unique per decision. Resending the same ID with the same body returns the original result and is never decided twice. A different body with a used ID is refused (execution_id_reused). A request whose earlier attempt is still running or unknown is never re-run automatically. -
facts: per question, one excerpt, a list of up to 3, or (number and date questions only) a typed{ value, source }. A typed value from a CRM looks like{ "value": 1200, "source": { "class": "crm-record", "id": "acct-881", "observedAt": "..." } }.sourcemay also carryexcerpt, the exact words that state the value. -
expectedReleaseDigest: optional on/v0/jobs/{job}/decisions; if left out, the decision is bound to the release Felspar chose, which the result records. Required on/v0/releases/{id}/jobs.
Decision result
{
"status": "ready",
"reason": null,
"result": {
"executionId": "a-new-uuid",
"executionObjectId": "execution-...",
"resultId": "result-...",
"releaseId": "release-...",
"releaseDigest": "sha256:...",
"evaluationTime": "2026-10-21T09:00:03.120Z",
"evidenceMode": "felspar-interpreted",
"outcome": "billing",
"scoring": null,
"context": null,
"subject": { "type": "ticket", "id": "48213", "related": [{ "relationship": "customer", "subject": "customer", "id": "cus-9f2", "questions": ["premium"] }] },
"receipt": { "receiptDigest": "sha256:...", "...": "..." },
"explanation": { "...": "see below" },
"diagnostics": [],
"interpretation": { "...": "how each excerpt was read", "judgmentsDigest": "sha256:...", "modelCalls": 3 }
}
}
| Field | Meaning |
|---|---|
status, reason |
ready when a rule decided. Otherwise the most specific cause, such as gap, stale, conflict or a context reason. |
result.outcome |
The outcome, or null |
result.explanation.decision |
The decision’s own status and reasons |
result.explanation.why |
The deciding rules, quoted from the policy, with any overrides |
result.explanation.needs[] |
Each open question: accepted source classes, maximum age, how each excerpt was read |
result.explanation.reviewNeeds |
Review the pack requires |
result.explanation.evidenceFreshUntil |
When the supporting evidence stops being fresh |
result.retry[] |
When undecided: only the open questions, each with fix, notUsed, the answers Felspar can choose and where to look |
result.attempts |
When undecided: used, remaining, next (retry or escalate) |
result.nextStep |
When undecided: the pack’s “when unresolved” instruction, quoted |
result.interpretation |
Per excerpt: method, value, leading answer, probability, threshold, close alternatives, whether cached; plus judgmentsDigest and modelCalls |
result.outputs |
When decided: the outputs this outcome asks the agent to write, each with its approved context and checks. Missing or empty means write nothing. |
result.case |
{ id, mode } for a job released with identity modes: the case ID Felspar used (the record’s own ID normalized, derived from fields or content, or issued) and the mode: provided, fields, content or issued. Send case.id as entityId for every later decision on the case. |
result.subject |
{ type, id, related[] }: the normalized case ID and, for each related record, the questions it supplied evidence for |
result.escalation |
{ id, trigger, why } when the release names a hand-off for this result: trigger is outcome, unresolved or attempt_limit, and why quotes the policy. Felspar names it and stops; your code acts on it. |
result.missing |
With status gap: required related records the request lacked, such as ["missing: counterparty"]. Nothing was decided and there is no receipt. |
result.receipt |
The receipt. Verify it before acting. |
result.context |
Context the release allows for this outcome, if any |
The next step for a decided outcome is in the brief’s outcomes[].nextStep.
A wrong or missing case ID is refused with one of the identity refusals. A caller may open at most 200 new issued cases per job in any 24 hours (issued_case_limit).
Verify request and result
{ "receipt": { "...": "result.receipt" }, "expectedReceiptDigest": "sha256:...", "deliveredCapsule": null }
Result: { "authority": "unapproved", "status": "reproduced" | "mismatch", "mismatches": [], "decided": { "jobId", "entityId", "releaseId", "outcome", "case", "subject", "escalation" } }. case ({ id, mode }) is recomputed from the retained request by the release’s identity mode; subject ({ type, related }) and escalation ({ id, trigger }) are re-derived from the release and the replayed outcome. Each appears when the release declares it. See Verify receipts.
Authoring and review routes
For signed-in people. The authoring MCP tools call these same routes.
| Method and path | Permission | Does |
|---|---|---|
GET /v0/sources |
Inspect | Connected documents, with your access to each and watch state |
POST /v0/sources |
Add sources | Admit a source from supplied text (not available to an MCP sign-in) |
GET /v0/sources/{id} |
Inspect, plus your own document access | The exact stored source |
POST /v0/sources/{id}/current |
Add sources | Read the document’s current version with your own connection and admit it as a new source |
POST /v0/drafts/check |
Propose | Check a felspar.pack-draft (v2, v1 or v0) against a source; saves nothing. Lists breaking changes against the current release. |
POST /v0/drafts/propose |
Propose | Check and submit as a proposal; returns the review link and any breaking changes |
POST /v0/proposals |
Propose | Submit a full proposal (advanced) |
GET /v0/proposals |
Inspect | The 25 newest proposals with their approvals and releases, and the 25 newest releases |
GET /v0/proposals/{id} |
Inspect, plus document access | One proposal |
POST /v0/proposals/{id}/backtest |
Propose and inspect, plus document access; never an agent | Backtest the proposal: { reinterpret?, executionIds?, limit? }. Reports each side’s escalation per case, escalationChanged and breaking. |
POST /v0/proposals/{id}/approve |
Approve (web app) | Record approval of that exact proposal |
GET /v0/approvals/{id} |
Inspect, plus document access | One approval |
POST /v0/approvals/{id}/release |
Release (web app) | Release that exact approval |
GET /v0/releases/{id} |
Inspect, plus document access | The verified release bundle |
GET /v0/runs/{id} |
Inspect | A compact decision result |
GET /v0/executions/{id} |
Inspect | The retained material needed to replay a decision |
Approval and release are used by the web app’s review pages. An MCP sign-in and agent keys can never call them.
Errors
Every deliberate refusal is JSON:
{
"error": "too_many_excerpts",
"path": ["facts", "topic"],
"issues": [
{ "code": "too_many_excerpts", "path": ["facts", "topic"], "fix": "Send at most 3 excerpts per question." },
{ "code": "unknown_question", "path": ["facts", "severity"], "fix": "This question is not in the released job. Use the question keys from describe." }
]
}
Every mistake in a request is reported together, so you can fix them all in one pass. Refusals name codes and paths, never your submitted values or source text. Anything not deliberately described is 403 operation_unavailable. See Refusal codes.
Health
GET /health answers when the service is running. It says nothing about your workspace.