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

# Quickstart

> One authenticated call to /v1/context/select, in cURL, Python and TypeScript.

You need two things: a key that starts `np_live_`, and the host it was issued
for. Everything else is one HTTP POST.

<Note>
  There is no SDK to install yet. The clients below are plain HTTP against the
  published contract, which is exactly what the SDKs will wrap — so nothing you
  write here is throwaway. See [Integrations](/integrations).
</Note>

## 1. Set your credentials

<CodeGroup>
  ```bash Shell theme={null}
  export NEEDLEPATH_API_KEY="np_live_…"
  export NEEDLEPATH_HOST="api.nextmoca.com"
  ```

  ```python Python theme={null}
  import os

  API_KEY = os.environ["NEEDLEPATH_API_KEY"]
  HOST = os.environ["NEEDLEPATH_HOST"]
  ```

  ```typescript TypeScript theme={null}
  const API_KEY = process.env.NEEDLEPATH_API_KEY!;
  const HOST = process.env.NEEDLEPATH_HOST!;
  ```
</CodeGroup>

<Warning>
  Keys are org-scoped and grant the ability to spend your organisation's
  allowance. Keep them server-side. Never put one in a browser bundle, a mobile
  app, or a URL — and never in `request_id`, which reaches our logs
  ([why](/authentication#what-never-to-put-in-a-request)).
</Warning>

## 2. Check you can reach the service

```bash theme={null}
curl -sS "https://$NEEDLEPATH_HOST/v1/health" \
  -H "Authorization: Bearer $NEEDLEPATH_API_KEY"
```

```json theme={null}
{
  "status": "ok",
  "service": "needlepath-hosted-adapter",
  "endpoint": "/v1/context/select",
  "default_operating_point": "np-2026-07-r2",
  "operating_points": ["np-2026-07-r1", "np-2026-07-r2"],
  "build_id": "9f1c2b7e4a"
}
```

A `401` here means no usable `Authorization` header reached us; a `403` means
one did and it was refused. The difference is worth wiring separately — see
[Errors](/errors#401-vs-403).

## 3. Make a selection

Send every record you are considering, the task they must serve, and the budget
they have to fit inside.

<CodeGroup>
  ```bash cURL theme={null}
  curl -sS "https://$NEEDLEPATH_HOST/v1/context/select" \
    -H "Authorization: Bearer $NEEDLEPATH_API_KEY" \
    -H "content-type: application/json" \
    -d '{
      "request_id": "req-quickstart-0001",
      "records": [
        {
          "id": "r-812",
          "kind": "tool_result",
          "title": "get_invoice(4471)",
          "source": "billing-api",
          "text": "{\"invoice_id\": 4471, \"total_cents\": 128400, \"status\": \"open\", \"issued\": \"2026-07-02\"}"
        },
        {
          "id": "r-813",
          "kind": "external_data",
          "title": "Refund policy v4",
          "source": "handbook",
          "text": "Refunds are issued to the original payment method within 10 business days of approval."
        },
        {
          "id": "r-814",
          "kind": "llm_response",
          "text": "I have pulled up the account. One moment."
        }
      ],
      "task": {
        "prompt": "Can this invoice still be refunded?",
        "tool_name": "answer_customer"
      },
      "budget": {
        "max_context_tokens": 4000,
        "operating_point": "np-2026-07-r2"
      },
      "render": true,
      "return_per_record": true
    }'
  ```

  ```python Python theme={null}
  import os
  import requests

  API_KEY = os.environ["NEEDLEPATH_API_KEY"]
  HOST = os.environ["NEEDLEPATH_HOST"]

  # Your own records, in whatever shape you already hold them.
  records = [
      {
          "id": "r-812",
          "kind": "tool_result",
          "title": "get_invoice(4471)",
          "source": "billing-api",
          "text": '{"invoice_id": 4471, "total_cents": 128400, "status": "open"}',
      },
      {
          "id": "r-813",
          "kind": "external_data",
          "title": "Refund policy v4",
          "source": "handbook",
          "text": "Refunds are issued to the original payment method within 10 business days.",
      },
      {
          "id": "r-814",
          "kind": "llm_response",
          "text": "I have pulled up the account. One moment.",
      },
  ]

  response = requests.post(
      f"https://{HOST}/v1/context/select",
      headers={
          "Authorization": f"Bearer {API_KEY}",
          "content-type": "application/json",
      },
      json={
          "request_id": "req-quickstart-0001",
          "records": records,
          "task": {
              "prompt": "Can this invoice still be refunded?",
              "tool_name": "answer_customer",
          },
          "budget": {
              "max_context_tokens": 4000,
              # Always pin this. See /concepts/operating-points.
              "operating_point": "np-2026-07-r2",
          },
          "render": True,
          "return_per_record": True,
      },
      timeout=30,
  )
  response.raise_for_status()
  result = response.json()

  print(result["rendered_context"])
  print(f"{result['records_selected']}/{result['records_available']} records, "
        f"{result['tokens_saved']} tokens saved, "
        f"{result['engine_latency_ms']:.1f} ms")
  ```

  ```typescript TypeScript theme={null}
  const API_KEY = process.env.NEEDLEPATH_API_KEY!;
  const HOST = process.env.NEEDLEPATH_HOST!;

  const records = [
    {
      id: "r-812",
      kind: "tool_result",
      title: "get_invoice(4471)",
      source: "billing-api",
      text: '{"invoice_id": 4471, "total_cents": 128400, "status": "open"}',
    },
    {
      id: "r-813",
      kind: "external_data",
      title: "Refund policy v4",
      source: "handbook",
      text: "Refunds are issued to the original payment method within 10 business days.",
    },
    {
      id: "r-814",
      kind: "llm_response",
      text: "I have pulled up the account. One moment.",
    },
  ];

  const response = await fetch(`https://${HOST}/v1/context/select`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      request_id: "req-quickstart-0001",
      records,
      task: {
        prompt: "Can this invoice still be refunded?",
        tool_name: "answer_customer",
      },
      budget: {
        max_context_tokens: 4000,
        // Always pin this. See /concepts/operating-points.
        operating_point: "np-2026-07-r2",
      },
      render: true,
      return_per_record: true,
    }),
    signal: AbortSignal.timeout(30_000),
  });

  if (!response.ok) throw new Error(`needlepath ${response.status}`);
  const result = await response.json();

  console.log(result.rendered_context);
  console.log(
    `${result.records_selected}/${result.records_available} records, ` +
      `${result.tokens_saved} tokens saved, ` +
      `${result.engine_latency_ms.toFixed(1)} ms`,
  );
  ```
</CodeGroup>

## 4. Read the response

```json theme={null}
{
  "request_id": "req-quickstart-0001",
  "rendered_context": "[tool_result] get_invoice(4471)\n{\"invoice_id\": 4471, …}\n\n[external_data] Refund policy v4\nRefunds are issued to …",
  "policy_version": "np-2026-07-r2",
  "selected": [
    {
      "record_id": "r-812",
      "kind": "tool_result",
      "title": "get_invoice(4471)",
      "source": "billing-api",
      "score": 0.91,
      "reason": "keyword_and_entity_match",
      "excerpt": "{\"invoice_id\": 4471, \"total_cents\": 128400, \"status\": \"open\"}",
      "excerpt_format": "plain",
      "selected_tokens": 31
    }
  ],
  "tokens_before": 96,
  "tokens_after": 49,
  "tokens_saved": 47,
  "records_available": 3,
  "records_selected": 2,
  "fallback_used": false,
  "selection_error": null,
  "engine_latency_ms": 11.4,
  "budget_tokens": 4000,
  "attempted_budget_tokens": [],
  "reduction_ratio": 0.489,
  "gate": { "engaged": true, "reason": "engage:needle", "signals": {} },
  "format_metrics": {}
}
```

Four fields carry most of the meaning:

| Field              | Read it as                                                                                                                                                                                                                                            |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rendered_context` | The block to put in your prompt. `render: false` is meant to suppress it and is [currently ignored](/api-reference/introduction#fields-accepted-but-not-yet-honoured) — discard it client-side if you assemble the prompt yourself from `selected[]`. |
| `selected[]`       | Which records made it, with a `score` and a `reason`. `excerpt` is your text verbatim.                                                                                                                                                                |
| `fallback_used`    | `true` means the engine stood aside and handed your context back essentially intact. A normal outcome, **not** an error.                                                                                                                              |
| `policy_version`   | The frozen configuration that actually ran. Log it with your results.                                                                                                                                                                                 |

<Note>
  `tokens_before` and `tokens_after` are the **engine's own** accounting, useful
  for seeing how much a selection moved. They are not the billing quantity and
  cannot be recomputed from your payload — see
  [How to recompute your own bill](/billing/recompute-your-bill).
</Note>

## 5. Fail open

This is the part to get right before you ship. Selection is an optimisation, so
a failure must degrade to *no optimisation* — never to an empty prompt.

<CodeGroup>
  ```python Python theme={null}
  def select_context(records, prompt, *, budget=4000):
      """Returns a rendered context block, or None to mean 'send everything'."""
      try:
          response = requests.post(
              f"https://{HOST}/v1/context/select",
              headers={"Authorization": f"Bearer {API_KEY}",
                       "content-type": "application/json"},
              json={
                  "request_id": new_request_id(),
                  "records": records,
                  "task": {"prompt": prompt},
                  "budget": {"max_context_tokens": budget,
                             "operating_point": "np-2026-07-r2"},
              },
              timeout=10,
          )
          response.raise_for_status()
          result = response.json()
      except Exception:
          # Timeout, non-2xx, unparseable body: send what you were going to send.
          return None

      # An empty selection is also a pass-through case, not a valid prompt.
      if result.get("records_selected", 0) == 0 or not result.get("rendered_context"):
          return None

      return result["rendered_context"]


  context = select_context(records, prompt) or render_everything(records)
  ```

  ```typescript TypeScript theme={null}
  async function selectContext(
    records: unknown[],
    prompt: string,
    budget = 4000,
  ): Promise<string | null> {
    try {
      const response = await fetch(`https://${HOST}/v1/context/select`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${API_KEY}`,
          "content-type": "application/json",
        },
        body: JSON.stringify({
          request_id: newRequestId(),
          records,
          task: { prompt },
          budget: {
            max_context_tokens: budget,
            operating_point: "np-2026-07-r2",
          },
        }),
        signal: AbortSignal.timeout(10_000),
      });
      if (!response.ok) return null;

      const result = await response.json();
      // An empty selection is a pass-through case, not a valid prompt.
      if (!result.records_selected || !result.rendered_context) return null;
      return result.rendered_context as string;
    } catch {
      return null;
    }
  }

  const context = (await selectContext(records, prompt)) ?? renderEverything(records);
  ```
</CodeGroup>

<Warning>
  Do not retry a `403` — none of its causes are transient. Do retry a `429` with
  exponential backoff and jitter, and a `5xx` only if the request is unchanged.
  [Errors](/errors) has the full table.
</Warning>

## Next

<CardGroup cols={2}>
  <Card title="Records and tasks" icon="layer-group" href="/concepts/records-and-tasks">What to put in each field, and what each one buys you.</Card>
  <Card title="Adaptive budget" icon="stairs" href="/concepts/adaptive-budget">Let the engine escalate its own budget instead of guessing one.</Card>
  <Card title="Limits" icon="gauge" href="/limits">Rate, quota and the one exact ceiling.</Card>
  <Card title="Authentication" icon="key" href="/authentication">Key format, rotation, and what a leaked key means.</Card>
</CardGroup>
