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

# Answer from your content with Grounded Responses

> Ground Responses API answers in your own publisher content: scope retrieval, check whether sources were used, handle questions your content does not cover, and show readers where an answer came from.

This tutorial builds a question-answering helper on **Grounded Responses**, `POST /ai/v1/grounded/responses`. Gloo finds relevant passages in your uploaded content and adds them to the model's context before it answers: [Retrieval-Augmented Generation (RAG)](https://docs.gloo.com/ai-learning-center/gloo-ai-103/how-rag-actually-works.md), in the same request and response shape as the Responses API.

You'll scope retrieval to your publisher, check whether an answer was actually [grounded](https://docs.gloo.com/ai-learning-center/gloo-ai-103/the-generation-step.md#grounded-vs-ungrounded-generation), handle questions your content doesn't cover, and show readers the sources behind an answer.

Want the concepts first? Read the [Grounded Responses guide](https://docs.gloo.com/api-guides/grounded-responses.md). For every parameter and response field, see the [Grounded Responses API reference](https://docs.gloo.com/api-reference/responses/grounded.md).

> [!NOTE]
>
> **Grounding never fails the request.** When nothing relevant is found, the model still answers, from its own knowledge, and the response reports `sources_returned: false`. That flag is the one signal that separates an answer from your content from one that isn't. This tutorial checks it on every call.

## Prerequisites

You need:

* A Gloo AI Studio account
* Your API key from the [API Keys page](https://docs.gloo.com/studio/manage-api-credentials.md), exported as `GLOO_API_KEY`
* **A publisher with content uploaded.** See [Manage Publishers](https://docs.gloo.com/studio/manage-publishers.md) and the [Upload Files tutorial](https://docs.gloo.com/tutorials/upload-files.md)
* **Authentication setup**: complete the [Authentication Tutorial](https://docs.gloo.com/tutorials/authentication.md) first
* Python 3.9+ with `requests` (`pip install requests`), or Node.js 18+ (for built-in `fetch`)

```bash theme={null}
export GLOO_API_KEY="your_api_key_here"
export GLOO_PUBLISHER="YourPublisherName"
```

***

## What You'll Build

A helper, `answer(question)`, that returns one of four results your interface can show safely:

| Status | Meaning | What to show |
| - | - | - |
| `verified` | Sources were used, and every quotation was found in a passage Search returned for the same question. That confirms the quotation is in your content, not that the model read that exact passage | The answer and its sources |
| `unverified` | Sources were used, but not every quotation could be checked: the answer quotes nothing, quotes something too short to check (such as a single word), or has an unclosed quotation | The answer and its sources, without saying the quotations were checked |
| `ungrounded` | No sources were retrieved | A plain "I couldn't find that in our content" message |
| `unsupported` | Sources were used, but a quotation couldn't be found in them (or Search returned no passages to check it against), even after widening retrieval once | A plain decline, or the answer without the quotation |

You'll build it in five steps, add streaming in Step 6, then put it together in [Complete Code](#complete-code).

***

## Step 1: Your First Grounded Request

A grounded request is a Responses request plus grounding parameters. Set `rag_publisher` to your publisher name.

```bash cURL theme={null}
curl -X POST 'https://platform.ai.gloo.com/ai/v1/grounded/responses' \
  -H "Authorization: Bearer $GLOO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "gloo-anthropic-claude-sonnet-4.6",
    "input": [{ "role": "user", "content": "What resources do you have on family counseling?" }],
    "rag_publisher": "'"$GLOO_PUBLISHER"'",
    "sources_limit": 3
  }'
```

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

API_KEY = os.environ["GLOO_API_KEY"]
PUBLISHER = os.environ["GLOO_PUBLISHER"]
GROUNDED_URL = "https://platform.ai.gloo.com/ai/v1/grounded/responses"
MODEL = "gloo-anthropic-claude-sonnet-4.6"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}


def output_text(response: dict) -> str:
    """Join the text of every message item in output[]."""
    return "".join(
        part.get("text", "")
        for item in response.get("output", [])
        if item.get("type") == "message"
        for part in item.get("content", [])
        if part.get("type") == "output_text"
    )


r = requests.post(GROUNDED_URL, headers=HEADERS, json={
    "model": MODEL,
    "input": [{"role": "user", "content": "What resources do you have on family counseling?"}],
    "rag_publisher": PUBLISHER,
    "sources_limit": 3,
}, timeout=120)
r.raise_for_status()
result = r.json()

print("Grounded:", result["sources_returned"])
print(output_text(result))
```

```typescript TypeScript theme={null}
const API_KEY = process.env.GLOO_API_KEY!;
const PUBLISHER = process.env.GLOO_PUBLISHER!;
const GROUNDED_URL = "https://platform.ai.gloo.com/ai/v1/grounded/responses";
const MODEL = "gloo-anthropic-claude-sonnet-4.6";
const HEADERS = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };

/** Join the text of every message item in output[]. */
function outputText(response: any): string {
  return (response.output ?? [])
    .filter((item: any) => item.type === "message")
    .flatMap((item: any) => item.content ?? [])
    .filter((part: any) => part.type === "output_text")
    .map((part: any) => part.text ?? "")
    .join("");
}

const res = await fetch(GROUNDED_URL, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    model: MODEL,
    input: [{ role: "user", content: "What resources do you have on family counseling?" }],
    rag_publisher: PUBLISHER,
    sources_limit: 3,
  }),
});
if (!res.ok) throw new Error(`Request failed: ${res.status} ${await res.text()}`);
const result = await res.json();

console.log("Grounded:", result.sources_returned);
console.log(outputText(result));
```

### Understanding the Response

The response is standard Responses output, a typed `output[]` array plus `usage`, with one addition: a top-level `sources_returned` boolean.

```json theme={null}
{
  "id": "resp_...",
  "object": "response",
  "model": "gloo-anthropic-claude-sonnet-4.6",
  "output": [
    {
      "type": "message",
      "role": "assistant",
      "content": [
        { "type": "output_text", "text": "Based on your uploaded resources, here are three approaches to family counseling..." }
      ]
    }
  ],
  "usage": { "input_tokens": 1874, "output_tokens": 312, "total_tokens": 2186 },
  "sources_returned": true
}
```

Notice the input tokens: the retrieved passages are part of the prompt, so grounded requests carry larger prompts than ungrounded ones. Grounded Responses is billed per token at the model's [published rate](https://docs.gloo.com/getting-started/pricing.md), with nothing added for retrieval. See [Pricing](https://docs.gloo.com/api-guides/grounded-responses.md#pricing).

***

## Step 2: Scope Retrieval to Your Content

Three parameters decide which content reaches the model:

| Parameter | What it does |
| - | - |
| `rag_publisher` | Whose content to search. Omit it to use **GlooGrounded**, a shared dataset assembled by Gloo. Set your publisher name to ground in your own content. An empty string or `null` turns retrieval off. |
| `sources_limit` | How many passages to add, from 1 to 10 (default 3). More passages give broader context but a larger prompt, more latency and more cost. |
| `certainty_threshold` | The minimum similarity, from 0.0 to 1.0, a passage needs to be used. Omit it for the platform default. A higher value keeps only close matches; see [the relevance threshold](https://docs.gloo.com/ai-learning-center/gloo-ai-103/the-retrieval-step.md#the-relevance-threshold). |

For an assistant that should speak only from your material, always set `rag_publisher`.

```python theme={null}
payload = {
    "model": MODEL,
    "input": [{"role": "user", "content": question}],
    "rag_publisher": PUBLISHER,   # your content, not the default dataset
    "sources_limit": 5,           # room for a few related passages
    "certainty_threshold": 0.75,  # optional: only close matches
}
```

> [!TIP]
>
> Start with the defaults and tune against real questions from your users. If good questions come back with `sources_returned: false`, a threshold that's too high is a likely cause; if answers drift off topic, raise it or lower `sources_limit`.

***

## Step 3: Tell the Model How to Use the Sources

The grounded endpoint adds the passages; your `instructions` decide how the model treats them. Encode a few rules from [The Prompt Contract](https://docs.gloo.com/best-practices/trustworthy-grounded-applications.md#the-prompt-contract):

```python Python theme={null}
INSTRUCTIONS = """Answer only from the provided sources. Do not add outside knowledge, even if it is correct.
Put quotation marks only around wording copied exactly from a source; label paraphrase as paraphrase.
If the sources don't answer the question, say so plainly instead of guessing.
If the question assumes something the sources don't support, correct that first."""
```

```typescript TypeScript theme={null}
const INSTRUCTIONS = `Answer only from the provided sources. Do not add outside knowledge, even if it is correct.
Put quotation marks only around wording copied exactly from a source; label paraphrase as paraphrase.
If the sources don't answer the question, say so plainly instead of guessing.
If the question assumes something the sources don't support, correct that first.`;
```

Add `"instructions": INSTRUCTIONS` to every request. The quotation rule matters for Step 5: it means anything in quotation marks claims to be verbatim, so your code can check it.

> [!WARNING]
>
> Instructions shift behavior a great deal, but they're best-effort: they reduce how often a check has to catch something, not replace it.

***

## Step 4: Handle Questions Your Content Doesn't Cover

When retrieval finds nothing, because the publisher has no matching content or no passage clears `certainty_threshold`, the model is told no publisher sources were retrieved and still answers. The answer can be fluent and confident, and it isn't from your content.

Check `sources_returned` before anything else, and give the user an honest message instead of the ungrounded answer:

```python Python theme={null}
NO_SOURCES = "I couldn't find that in our resources. Try rephrasing, or ask about a topic we cover."


def grounded(question: str, sources_limit: int = 5) -> dict:
    r = requests.post(GROUNDED_URL, headers=HEADERS, json={
        "model": MODEL,
        "instructions": INSTRUCTIONS,
        "input": [{"role": "user", "content": question}],
        "rag_publisher": PUBLISHER,
        "sources_limit": sources_limit,
    }, timeout=120)
    r.raise_for_status()
    return r.json()


result = grounded("What's the capital of Mongolia?")  # probably not in your content
if not result["sources_returned"]:
    print(NO_SOURCES)  # or show the answer clearly labeled as not from your content
else:
    print(output_text(result))
```

```typescript TypeScript theme={null}
const NO_SOURCES =
  "I couldn't find that in our resources. Try rephrasing, or ask about a topic we cover.";

async function grounded(question: string, sourcesLimit = 5): Promise<any> {
  const res = await fetch(GROUNDED_URL, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
      model: MODEL,
      instructions: INSTRUCTIONS,
      input: [{ role: "user", content: question }],
      rag_publisher: PUBLISHER,
      sources_limit: sourcesLimit,
    }),
  });
  if (!res.ok) throw new Error(`Request failed: ${res.status} ${await res.text()}`);
  return res.json();
}

const result = await grounded("What's the capital of Mongolia?"); // probably not in your content
console.log(result.sources_returned ? outputText(result) : NO_SOURCES);
```

Whether you decline or show a labeled answer is a product decision; what matters is that an ungrounded answer never looks like a grounded one. Count how often it happens with real traffic: a narrow library fielding broad questions will see it often, and that's a signal about your content, not an error.

> [!NOTE]
>
> `sources_returned: true` tells you sources reached the model, not that the answer reflects them. A model can still produce a quotation or a title no source contains, a [hallucination](https://docs.gloo.com/ai-learning-center/gloo-ai-101/challenges-and-risks.md#hallucinations). Step 5 adds that check.

***

## Step 5: Show Sources and Verify Quotations

[Citations](https://docs.gloo.com/ai-learning-center/gloo-ai-103/the-generation-step.md#citations-and-verifiability) build trust only if they're real. Grounded Responses returns `sources_returned` but not the passages themselves, so fetch them from the [Search API](https://docs.gloo.com/api-guides/search.md) with the same question, against the same publisher. Use the results two ways:

* **Show them** as "Sources from our library": each result has `properties.item_title` and the passage text in `properties.snippet`.
* **Verify against them**: every quotation in the answer should appear in one of the passages.

[Citations and Verification](https://docs.gloo.com/ai-learning-center/gloo-ai-103/citations-and-verification.md) in AI 103 explains the ideas behind this step, including what to show when no sources come back.

> [!NOTE]
>
> Search uses your publisher's **tenant** name, which has spaces removed (`Example Publisher` becomes `ExamplePublisher`). The surest way to get it is `GET /ai/v1/data/tenant/names`. The collection is always `GlooProd`.

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

SEARCH_URL = "https://platform.ai.gloo.com/ai/v1/data/search"
TENANT = "".join(PUBLISHER.split())  # remove all whitespace

_QUOTED = re.compile(r'"([^"]*)"|“([^”]*)”')  # straight and curly quotes each close their own kind
_EDGE = " .,;:!?"


def quoted_spans(text: str, min_length: int = 12) -> list[str]:
    """Quoted spans, paired in order; by default short ones such as a single quoted word are skipped."""
    spans = [straight or curly for straight, curly in _QUOTED.findall(text)]
    return [span for span in spans if len(span) >= min_length]


def balanced_quotes(text: str) -> bool:
    """True when every opening quote mark has a closing one."""
    depth = 0  # each curly closing mark must follow an opening one
    for ch in text:
        if ch == "“":
            depth += 1
        elif ch == "”":
            depth -= 1
            if depth < 0:
                return False
    return depth == 0 and text.count('"') % 2 == 0


def search_passages(question: str, limit: int = 10) -> list[dict]:
    r = requests.post(SEARCH_URL, headers=HEADERS, json={
        "query": question, "collection": "GlooProd", "tenant": TENANT,
        "certainty": 0.5, "limit": limit,
    }, timeout=60)
    r.raise_for_status()
    return [hit["properties"] for hit in r.json().get("data", [])]


def normalize(text: str) -> str:
    text = text.replace("’", "'").replace("‘", "'").replace("“", '"').replace("”", '"')
    return re.sub(r"\s+", " ", text.replace("…", "...")).strip().lower()


def unverified_quotes(answer: str, sources: list[str]) -> list[str]:
    """Quoted spans in `answer` that no single source contains."""
    normalized = [normalize(s) for s in sources]
    missing = []
    for span in quoted_spans(answer):
        parts = [p for p in (p.strip(_EDGE) for p in normalize(span).split("...")) if p]
        if not parts or not any(all(p in src for p in parts) for src in normalized):
            missing.append(span)
    return missing
```

```typescript TypeScript theme={null}
const SEARCH_URL = "https://platform.ai.gloo.com/ai/v1/data/search";
const TENANT = PUBLISHER.replace(/\s+/g, "");

const QUOTED = /"([^"]*)"|“([^”]*)”/g; // straight and curly quotes each close their own kind
const EDGE = /^[\s.,;:!?]+|[\s.,;:!?]+$/g;

/** Quoted spans, paired in order; by default short ones such as a single quoted word are skipped. */
function quotedSpans(text: string, minLength = 12): string[] {
  const spans = [...text.matchAll(QUOTED)].map((m) => m[1] ?? m[2]);
  return spans.filter((span) => span.length >= minLength);
}

/** True when every opening quote mark has a closing one. */
function balancedQuotes(text: string): boolean {
  let depth = 0; // each curly closing mark must follow an opening one
  for (const ch of text) {
    if (ch === "“") depth += 1;
    else if (ch === "”" && --depth < 0) return false;
  }
  return depth === 0 && (text.match(/"/g) ?? []).length % 2 === 0;
}

async function searchPassages(question: string, limit = 10): Promise<any[]> {
  const res = await fetch(SEARCH_URL, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({ query: question, collection: "GlooProd", tenant: TENANT, certainty: 0.5, limit }),
  });
  if (!res.ok) throw new Error(`Search failed: ${res.status} ${await res.text()}`);
  return ((await res.json()).data ?? []).map((hit: any) => hit.properties);
}

function normalize(text: string): string {
  return text.replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/…/g, "...")
    .replace(/\s+/g, " ").trim().toLowerCase();
}

/** Quoted spans in `answer` that no single source contains. */
function unverifiedQuotes(answer: string, sources: string[]): string[] {
  const normalized = sources.map(normalize);
  const missing: string[] = [];
  for (const span of quotedSpans(answer)) {
    const parts = normalize(span).split("...").map((p) => p.replace(EDGE, "")).filter(Boolean);
    const ok = parts.length > 0 && normalized.some((src) => parts.every((p) => src.includes(p)));
    if (!ok) missing.push(span);
  }
  return missing;
}
```

Each quotation is checked against one passage at a time, so a "quote" stitched together from two different documents doesn't pass. The Search results may not be exactly the passages the grounded call used, so treat a quotation that fails as **unconfirmed**, not proven false, and handle it the safe way: widen retrieval once and regenerate, then decline or drop the quotation if it still doesn't verify. [Verification Checks](https://docs.gloo.com/best-practices/trustworthy-grounded-applications.md#verification-checks) covers the same check for titles of works, and when to paraphrase, withhold or decline.

When rendering sources, show the title and a short excerpt, and link to `properties.item_url` when it isn't empty.

> [!TIP]
>
> Need the exact passages the model saw? [Grounded Completions](https://docs.gloo.com/api-guides/grounded-completions.md) returns citation metadata (`include_citations`) on the Chat Completions shape.

***

## Step 6: Stream a Grounded Answer

Set `"stream": true` to receive the same Server-Sent Events as the Responses API. The grounding outcome is known before the first token, so it arrives as the `X-Sources-Returned` response header instead of a body field. Read it before you render anything:

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

with requests.post(GROUNDED_URL, headers=HEADERS, stream=True, timeout=300, json={
    "model": MODEL, "instructions": INSTRUCTIONS, "stream": True,
    "input": [{"role": "user", "content": "How do we sign up to volunteer?"}],
    "rag_publisher": PUBLISHER,
}) as r:
    r.raise_for_status()
    if r.headers.get("X-Sources-Returned", "").lower() != "true":
        print(NO_SOURCES)
    else:
        r.encoding = "utf-8"  # SSE is always UTF-8; don't guess from the header
        event, parts = None, []
        for line in r.iter_lines(decode_unicode=True):
            if line.startswith("event:"):
                event = line[len("event:"):].strip()
            elif line.startswith("data:"):
                data = json.loads(line[len("data:"):].strip())
                if event == "response.output_text.delta":
                    parts.append(data["delta"])  # buffer, then verify before showing
                elif event == "response.failed" or (event and not event.startswith("response.")):
                    raise RuntimeError(f"Stream ended with {event}: {data}")
        print("".join(parts))
```

```typescript TypeScript theme={null}
const res = await fetch(GROUNDED_URL, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    model: MODEL, instructions: INSTRUCTIONS, stream: true,
    input: [{ role: "user", content: "How do we sign up to volunteer?" }],
    rag_publisher: PUBLISHER,
  }),
});
if (!res.ok) throw new Error(`Request failed: ${res.status} ${await res.text()}`);

if ((res.headers.get("X-Sources-Returned") ?? "").toLowerCase() !== "true") {
  console.log(NO_SOURCES);
} else {
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  const parts: string[] = [];
  let buffer = "";
  let event: string | null = null;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";
    for (const line of lines.map((l) => l.trim())) {
      if (line.startsWith("event:")) event = line.slice(6).trim();
      else if (line.startsWith("data:")) {
        const data = JSON.parse(line.slice(5).trim());
        if (event === "response.output_text.delta") parts.push(data.delta); // buffer, then verify
        else if (event === "response.failed" || (event && !event.startsWith("response."))) {
          throw new Error(`Stream ended with ${event}: ${JSON.stringify(data)}`);
        }
      }
    }
  }
  console.log(parts.join(""));
}
```

Compare the header without regard to case. This example buffers the text instead of [streaming](https://docs.gloo.com/ai-learning-center/gloo-ai-104/streaming-responses.md) each delta to the screen: a quotation shown live is on screen before it can be checked. For quote-heavy or high-stakes answers, buffer and verify; see [Verify Before You Stream](https://docs.gloo.com/best-practices/trustworthy-grounded-applications.md#verify-before-you-stream). If a stream breaks partway through, see [Handling Streaming Failures](https://docs.gloo.com/best-practices/completions-streaming-failures.md).

***

## Complete Code

The `answer()` helper from [What You'll Build](#what-youll-build): check grounding first, verify quotations against passages from Search, widen both once, and decline rather than show a quotation that couldn't be confirmed. An answer with no quotations, with a quotation too short to check (under 12 characters, such as a single quoted word), or with an unclosed quotation comes back as `unverified`: show it with its sources, but don't label it as checked. An answer with quotations but no Search passages to check them against comes back as `unsupported`. It uses `output_text`, `INSTRUCTIONS`, `grounded`, `search_passages`, `quoted_spans`, `balanced_quotes` and `unverified_quotes` from Steps 1 to 5; put them in one file with this code.

```python Python theme={null}
def answer(question: str) -> dict:
    """Returns {"status": "verified" | "unverified" | "ungrounded" | "unsupported", ...}.
    Show "verified" and "unverified" with their sources; only "verified" had quotations checked."""
    unverified: list[str] = []

    for sources_limit in (5, 10):  # one escalation, not a loop
        result = grounded(question, sources_limit=sources_limit)
        # Check grounding first: with no sources there is nothing to verify against.
        if not result["sources_returned"]:
            return {"status": "ungrounded", "message": NO_SOURCES}

        passages = search_passages(question, limit=sources_limit * 2)
        sources = [p.get("snippet", "") for p in passages]
        text = output_text(result)
        unverified = unverified_quotes(text, sources)
        if not unverified:
            # Verified only when every quotation was closed, long enough to check, and checked out.
            spans = quoted_spans(text)
            checked = (bool(passages) and bool(spans) and balanced_quotes(text)
                       and len(spans) == len(quoted_spans(text, 1)))
            return {
                "status": "verified" if checked else "unverified",
                "text": text,
                "sources": [{"title": p.get("item_title"), "url": p.get("item_url") or None,
                             "excerpt": p.get("snippet", "")[:200]} for p in passages],
            }

    return {"status": "unsupported", "unverified": unverified,
            "message": "I couldn't confirm that from our resources."}


if __name__ == "__main__":
    reply = answer("What resources do you have on family counseling?")
    if reply["status"] in ("verified", "unverified"):
        print(reply["text"], "\n\nSources:")
        for s in reply["sources"]:
            print(f'- {s["title"]}' + (f' ({s["url"]})' if s["url"] else ""))
    else:
        print(reply["message"])
```

```typescript TypeScript theme={null}
type Answer =
  | {
      status: "verified" | "unverified";
      text: string;
      sources: { title: string; url: string | null; excerpt: string }[];
    }
  | { status: "ungrounded" | "unsupported"; message: string; unverified?: string[] };

/** Show "verified" and "unverified" with their sources; only "verified" had quotations checked. */
async function answer(question: string): Promise<Answer> {
  let unverified: string[] = [];

  for (const sourcesLimit of [5, 10]) { // one escalation, not a loop
    const result = await grounded(question, sourcesLimit);
    // Check grounding first: with no sources there is nothing to verify against.
    if (!result.sources_returned) return { status: "ungrounded", message: NO_SOURCES };

    const passages = await searchPassages(question, sourcesLimit * 2);
    const sources = passages.map((p) => p.snippet ?? "");
    const text = outputText(result);
    unverified = unverifiedQuotes(text, sources);
    if (unverified.length === 0) {
      // Verified only when every quotation was closed, long enough to check, and checked out.
      const spans = quotedSpans(text);
      const checked = passages.length > 0 && spans.length > 0 && balancedQuotes(text)
        && spans.length === quotedSpans(text, 1).length;
      return {
        status: checked ? "verified" : "unverified",
        text,
        sources: passages.map((p) => ({
          title: p.item_title, url: p.item_url || null, excerpt: (p.snippet ?? "").slice(0, 200),
        })),
      };
    }
  }
  return { status: "unsupported", unverified, message: "I couldn't confirm that from our resources." };
}

async function main() {
  const reply = await answer("What resources do you have on family counseling?");
  if ("text" in reply) { // "verified" or "unverified"
    console.log(reply.text, "\n\nSources:");
    for (const s of reply.sources) console.log(`- ${s.title}${s.url ? ` (${s.url})` : ""}`);
  } else {
    console.log(reply.message);
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
```

[Putting It Together](https://docs.gloo.com/best-practices/trustworthy-grounded-applications.md#putting-it-together) shows the same pattern with title checks added, and [Evaluate Before You Ship](https://docs.gloo.com/best-practices/trustworthy-grounded-applications.md#evaluate-before-you-ship) shows how to measure it on your own questions.

***

## Troubleshooting

**`sources_returned` is always `false`**
: Check that `rag_publisher` matches your publisher name exactly, as shown in [Manage Publishers](https://docs.gloo.com/studio/manage-publishers.md), and that your uploads have finished processing. Then try a question your documents clearly answer. If you set `certainty_threshold`, remove it to fall back to the platform default.

**Answers draw on content that isn't yours**
: `rag_publisher` is missing, so the request grounded in the default GlooGrounded dataset. Set it on every request.

**Follow-up questions like "tell me more" come back ungrounded**
: Retrieval keys on the latest user message, and a short follow-up has little to search on. Carry the original question's terms into the follow-up, or reuse the sources from the earlier turn. See [Multi-Turn Conversations](https://docs.gloo.com/best-practices/trustworthy-grounded-applications.md#multi-turn-conversations).

**Search returns `400` or `403`**
: `tenant` must be the normalized tenant name with spaces removed, which you can get from `GET /ai/v1/data/tenant/names`, and `collection` must be `GlooProd`. You can only search your own publisher's tenant.

**A correct quotation is flagged as unconfirmed**
: The Search results may not include the passage the model quoted. Raise the Search `limit`, or move to [Grounded Completions](https://docs.gloo.com/api-guides/grounded-completions.md) to check against the citation snippets the model was given.

**Every streamed answer is treated as ungrounded**
: Compare `X-Sources-Returned` without regard to case (`true` / `True`), and read it from the response headers, not the event stream.

**Prompts are large and costly**
: Retrieved passages count as input tokens. Lower `sources_limit`; it's your main lever over the cost of each grounded request.

***

## Next Steps

> [card] [Grounded Responses guide](https://docs.gloo.com/api-guides/grounded-responses.md)
>
> Parameters, streaming, reasoning and how it compares to Grounded Completions

> [card] [Grounded Responses API reference](https://docs.gloo.com/api-reference/responses/grounded.md)
>
> Every request parameter and response field

> [card] [Building Trustworthy Grounded Applications](https://docs.gloo.com/best-practices/trustworthy-grounded-applications.md)
>
> Verification, prompt contracts, multi-turn handling and evaluation

> [card] [Handling Streaming Failures](https://docs.gloo.com/best-practices/completions-streaming-failures.md)
>
> Retry ceilings, continuation and partial-output UX

> [card] [Upload Files tutorial](https://docs.gloo.com/tutorials/upload-files.md)
>
> Get more of your content into your publisher

> [card] [Build with Direct Responses](https://docs.gloo.com/tutorials/direct-responses.md)
>
> The same request shape, without retrieval


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