> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moonbounce.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> From an API key to your first evaluation and feedback.

The Moonbounce Decisions API scores content against a **decision** you describe in code, records every answer, and learns from the feedback you send back. This page takes you from a key to an evaluation and its feedback in about ten minutes, with curl and with TypeScript.

You need `curl` 7.76 or later, `jq`, `uuidgen` (built into macOS; `uuid-runtime` on Debian and Ubuntu) and, for the TypeScript part, [Bun](https://bun.sh) or Node 20+.

> [!NOTE]
>
> This isn't a public API. Access is by invitation: join the waitlist at [app.moonbounce.ai](https://app.moonbounce.ai) and sign up once you're granted access. The API is in early access and unstable: names and shapes change often, breaking changes are expected, and there are no compatibility guarantees.

## 1. Your key and the base URL

Once you have access, a team admin creates API keys on the team's **Keys** page ([app.moonbounce.ai/team/keys](https://app.moonbounce.ai/team/keys)). For this guide, make a **Test** key with all three scopes (`decisions:evaluate`, `decisions:feedback` and `decisions:develop`): a test key keeps these calls out of your production data. The full value is shown once. Keep it out of files and your shell history:

```sh theme={null}
read -rs MOONBOUNCE_API_KEY && export MOONBOUNCE_API_KEY
```

Every call goes to the same base URL and sends the key as a bearer token:

```bash theme={null}
export MOONBOUNCE_API_URL="${MOONBOUNCE_API_URL:-https://api.moonbounce.ai}"
curl -sS --fail-with-body "$MOONBOUNCE_API_URL/health"
```

The full reference is the OpenAPI document at `$MOONBOUNCE_API_URL/openapi.json`; every operation there takes `Authorization: Bearer <key>` except `GET /health` and `POST /v1/specs/hash`.

## 2. Describe the decision

A decision is a small JSON spec of named `nodes`: **features** are indicators an engine detects in your content, an **aggregator** (`combine`) turns their answers into one score, `calibrate` maps that score onto the probability that `target` is true, and `output` names the node whose value is the decision's score. This one decides whether a support message needs a person:

```bash theme={null}
cat > decision.json <<'EOF'
{
  "format": "moonbounce.spec/0",
  "meta": {
    "title": "Refund request",
    "owner": "support",
    "display": { "approve": { "text": "Auto-reply" }, "escalate": { "text": "Send to a person" } }
  },
  "guidance": "A support message from a customer. Answer from the message alone.",
  "nodes": {
    "asks_for_refund": { "type": "boolean", "prompt": "Does the customer ask for their money back?" },
    "is_angry": { "type": "boolean", "prompt": "Is the customer angry or threatening?" },
    "risk": { "type": "combine", "terms": [{ "node": "asks_for_refund" }, { "node": "is_angry" }] },
    "risk_calibrated": { "type": "calibrate", "node": "risk" }
  },
  "output": "risk_calibrated",
  "target": "The message needs a person to handle it."
}
EOF
```

Keep this file in your repository: your code is the source of truth for the decision. Its **structure hash** identifies this exact set of features and how they combine. Hashing needs no key and stores nothing:

```bash theme={null}
curl -sS --fail-with-body -X POST "$MOONBOUNCE_API_URL/v1/specs/hash" \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --slurpfile spec decision.json '{spec: $spec[0]}')" | jq -r .structure
```

Reordering keys or reformatting the file doesn't change the hash; changing a prompt does.

## 3. Evaluate an item

`POST /v1/evaluate` scores one item. Send the spec inline (it is registered the first time the API sees it), a `decision_id` of your choosing, the content as `subject` parts, your own `item_id`, and the cut points that turn the score into an outcome. Send an `Idempotency-Key` on every POST and keep it: if the call times out, send it again with the same key, and you get the first answer instead of a second evaluation:

```bash theme={null}
EVALUATE_KEY=$(uuidgen)
EVALUATION=$(curl -sS --fail-with-body -X POST "$MOONBOUNCE_API_URL/v1/evaluate" \
  -H "Authorization: Bearer $MOONBOUNCE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $EVALUATE_KEY" \
  -d "$(jq -n --slurpfile spec decision.json '{
    decision_id: "quickstart.refund-request",
    spec: $spec[0],
    weights: "default",
    item_id: "ticket-1042",
    subject: [{type: "text", name: "message", text: "My order arrived broken. I want a refund, today."}],
    outcomes: {below: "approve", cuts: [{at: 0.5, outcome: "escalate"}]}
  }')")
echo "$EVALUATION" | jq '{id, outcome, score, nodes}'
EVALUATION_ID=$(echo "$EVALUATION" | jq -r .id)
```

The response carries the outcome, the full-precision score, every node's value and the hashes that produced them:

```json theme={null}
{
  "id": "evl_01a17a7b-da6f-71a4-b3d0-f7b5bb80ade7",
  "status": "scored",
  "outcome": "escalate",
  "score": 0.81,
  "nodes": {
    "asks_for_refund": { "type": "boolean", "value": 0.97 },
    "is_angry": { "type": "boolean", "value": 0.65 },
    "risk": { "type": "combine", "value": 0.81, "contributions": { "asks_for_refund": 0.485, "is_angry": 0.325 } },
    "risk_calibrated": { "type": "calibrate", "value": 0.81, "calibrated": false }
  }
}
```

`"weights": "default"` uses the starting weights every spec gets before any feedback: a combine is the plain mean of its answers, and calibrate passes the score through. The response says so with `version.weights_fitted: false` and `calibrated: false`. A `json` part (`{"type": "json", "name": "order", "data": {...}}`) can sit next to the text in `subject`. Read any evaluation back by its id:

```bash theme={null}
curl -sS --fail-with-body "$MOONBOUNCE_API_URL/v1/evaluations/$EVALUATION_ID" \
  -H "Authorization: Bearer $MOONBOUNCE_API_KEY" | jq '{id, outcome}'
```

## 4. Send feedback

When your app or a person learns the right answer, send it back. A claim says what was true: `outcome` (one of your cut points' outcomes), `target` (whether the target statement held), `verdict` (`approve` or `reject` of our answer), or `feature` (the right value of one feature). Feedback is about one evaluation, or about the latest evaluation of an item in your key's environment (`{"decision_id", "item_id"}`; seed examples don't count). A `verdict` claim needs `evaluation_id`:

```bash theme={null}
FEEDBACK_KEY=$(uuidgen)
curl -sS --fail-with-body -X POST "$MOONBOUNCE_API_URL/v1/feedback" \
  -H "Authorization: Bearer $MOONBOUNCE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $FEEDBACK_KEY" \
  -d "$(jq -n --arg id "$EVALUATION_ID" '{
    about: {evaluation_id: $id},
    claims: [{kind: "outcome", value: "escalate"}],
    source: "owner",
    reason: "Broken on arrival: a person should handle it."
  }')" | jq '{id, evaluation_id}'
```

`source` says who judged. Use `owner` when you, the decision's owner, or someone you trust decided the case: owner feedback is what previews and suggestions learn from. `app_native` (your app's own approve or reject feedback) and `app_resolution` (your process settled the case) are stored too, but count only as weak feedback: they never decide a case's right answer, though their reasons can reach the suggestion writer. The API answers `201` with the feedback id.

## 5. The same in TypeScript

No SDK is needed: the API is plain JSON over HTTPS.

```ts theme={null}
import { readFileSync } from "node:fs";

const base = process.env.MOONBOUNCE_API_URL ?? "https://api.moonbounce.ai";
const headers = {
  Authorization: `Bearer ${process.env.MOONBOUNCE_API_KEY}`,
  "Content-Type": "application/json",
};

// One Idempotency-Key per request: keep it, and pass the same one again to retry that request.
async function post(path: string, body: unknown, idempotencyKey: string) {
  const res = await fetch(`${base}${path}`, {
    method: "POST",
    headers: { ...headers, "Idempotency-Key": idempotencyKey },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${json.error?.reason ?? json.error?.type}: ${json.error?.message}`);
  return json;
}

const evaluateKey = crypto.randomUUID();
const evaluation = await post(
  "/v1/evaluate",
  {
    decision_id: "quickstart.refund-request",
    spec: JSON.parse(readFileSync("decision.json", "utf8")),
    weights: "default",
    item_id: "ticket-1043",
    subject: [{ type: "text", name: "message", text: "Thanks, the replacement works fine." }],
    outcomes: { below: "approve", cuts: [{ at: 0.5, outcome: "escalate" }] },
  },
  evaluateKey,
);
console.log(evaluation.outcome, evaluation.score);

const feedbackKey = crypto.randomUUID();
const feedback = await post(
  "/v1/feedback",
  { about: { evaluation_id: evaluation.id }, claims: [{ kind: "outcome", value: "approve" }], source: "owner" },
  feedbackKey,
);
console.log(feedback.id);
```

Save it as `quickstart.mts` in the folder that holds `decision.json` and run `bun quickstart.mts` or `npx tsx quickstart.mts` (the `.mts` extension makes it a module, which top-level `await` needs).

## 6. Connect your coding agent (MCP)

The same operations are MCP tools at `$MOONBOUNCE_API_URL/mcp`, with the same key. In Claude Code, install the Moonbounce plugin, or sign in without it; [Connect your agent](https://docs.moonbounce.ai/connect-your-agent.md) covers both options. Neither sign-in option needs a key. Signed-in admins and members can run real and dry-run evaluations and read them in Production, attributed to them. Test needs an `env:test` key; unattended agents and backends use keys. For Claude Code, put this in your repository's `.mcp.json` and start `claude` from a shell where both variables are exported:

```json theme={null}
{
  "mcpServers": {
    "moonbounce": {
      "type": "http",
      "url": "${MOONBOUNCE_API_URL:-https://api.moonbounce.ai}/mcp",
      "headers": { "Authorization": "Bearer ${MOONBOUNCE_API_KEY}" }
    }
  }
}
```

To check the connection without an agent, list the tools:

```bash theme={null}
curl -sS --fail-with-body -X POST "$MOONBOUNCE_API_URL/mcp" \
  -H "Authorization: Bearer $MOONBOUNCE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' | grep -o '"name":"evaluate"'
```

## Errors, retries and limits

Every error under `/v1` has one shape: `{"error": {"type", "message", "reason"?, "param"?, "details"?}}`, and every response carries a `Request-Id` header. When you report a problem, quote it and email [support@moonbounce.ai](mailto:support@moonbounce.ai?subject=Moonbounce%20API%20problem\&body=Request-Id%3A%20%0A%0AWhat%20happened%3A%0A). `param` names the field at fault when there is one; `reason` is set where you can act on it, and the OpenAPI document lists them (`ErrorReason`).

| Status | Meaning | What to do |
| - | - | - |
| 400 | The request failed validation (`param` says where, when it can) | Fix the request |
| 401 `KEY_INVALID` | Missing, malformed or revoked key | Check the `Authorization` header |
| 403 | This key may not do this | Use another key |
| 404 `VERSION_UNKNOWN` | Evaluating by `structure` hash that was never registered | Send the `spec` inline once, or `POST /v1/decisions/{id}/structures` |
| 409 | Conflicts with stored state, for example `VERSION_MISMATCH` when `spec` and `structure` disagree | Send one or make them match |
| 410 | The first result for this `Idempotency-Key` was deleted, so it can't be replayed | Use a fresh key |
| 413 | The body is larger than 1 MB | Send less content |
| 422 `IDEMPOTENCY_KEY_REUSED` | The same `Idempotency-Key` with a different request | Use a fresh key per distinct request |
| 429 `RATE_LIMITED` | Too many requests | Wait for `Retry-After` seconds |
| 429 `QUOTA_EXCEEDED` | Your team's daily work budget is used up (`details.reset_at`), or one request is bigger than the whole budget (`details.too_large`) | Wait until `reset_at`, or send smaller requests |
| 502 `ENGINE_FAILED` | The engine could not answer a prompt the score needs | Route the item to a person; a retry may not help |
| 503 | Temporarily unavailable (`STORE_UNAVAILABLE`, `ENGINE_UNAVAILABLE`, `AUTH_UNAVAILABLE`) | Retry with the same `Idempotency-Key` after `Retry-After` |

* **Retries.** Retry 429 `RATE_LIMITED` and 503, and a timeout, with the same `Idempotency-Key`, after `Retry-After` when it is set. If the first attempt was stored, its result comes back with `Idempotent-Replayed: true` and nothing runs twice; if it failed before it was stored, the retry runs it. A key is remembered for 24 hours and is scoped to your organization and the key's environment, across every operation, so use a fresh one per request.
* **Time.** An evaluation answers within 10 seconds or fails, with a 503 you can retry with the same key, or a 502 when an engine answer the score depends on is missing; `Prefer: wait=<whole seconds>` shortens that budget (it never lengthens it). The response is sent only after the evaluation is stored.
* **Limits.** Request bodies up to 1 MB; up to 64 `subject` parts and 64 `context` parts per item; up to 64 nodes per spec. Rate limits apply per key and per client IP, counted per Cloudflare location (currently 600 requests a minute per key and 300 per IP); a 429 says how long to wait in `Retry-After`. Evaluate, seeds, previews and suggestions also count against your team's rate and daily work budget ([Rate limits](https://docs.moonbounce.ai/learn/engines-and-environments.md#rate-limits)).
* **One decision per call.** Chain decisions in your own code.

## Next

* Build the mental model behind these calls: [How Moonbounce works](https://docs.moonbounce.ai/learn/how-it-works.md).
* Seed a decision with a dozen labeled examples so previews have cases to replay: `POST /v1/decisions/{id}/seeds`.
* Pin the structure: `POST /v1/decisions/{id}/structures` once, then evaluate with `"structure": "<hash>"` instead of the inline spec.
* Ask for a suggested improvement and preview it against your past cases: `POST /v1/decisions/{id}/suggestions` and `POST /v1/decisions/{id}/previews`. Both see only your key's environment, so with this guide's test key they learn from the test cases and seeds you sent, and their suggestions are never accepted. Use a production key to improve a live decision.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.