# Question Endpoint

Ask a question against your Context Link and receive a short paragraph answer with citations. Unlike the [context endpoint](/docs/context-endpoint) (which returns the raw matching chunks), this endpoint runs retrieval then passes the context to a small LLM that composes a single concise response grounded in your sources.

> **Pro plan:** this endpoint is available on Pro plans only. Requests from non-Pro accounts return `402 Payment Required`.

## How to invoke it

### From an AI assistant (skill-driven)

With the `ask-question` skill installed, these phrases trigger it automatically:

- "ask context link what our refund policy is"
- "ask my docs when onboarding changed"
- "use context link to answer: …"
- "what does my knowledge base say about …"

In **Claude Code** specifically, the slash command `/ask-question` also invokes the skill directly.

Download the skill on the [Installation](/integrations) page for Claude Chat, Claude Cowork, or Codex.

### Two ways to call the endpoint directly

**1. Via your Context Link subdomain (simplest, paste into any AI chat):**

```
https://YOUR-SUBDOMAIN.context-link.ai/q/what-is-pricing?p=YOUR_PIN
https://YOUR-SUBDOMAIN.context-link.ai/question/what-is-pricing?p=YOUR_PIN
```

Both paths hit the same endpoint. Use dashes-for-spaces in the question slug. Response negotiates on `Accept` (defaults to HTML for ChatGPT / Gemini user agents, JSON or Markdown otherwise).

**2. Via the authenticated API (for servers and integrations):**

```
GET /api/v1/question?query=what-is-pricing
```

## Quick reference

| Property | Value |
|----------|-------|
| **Endpoint** | `GET /api/v1/question` |
| **Authentication** | API key via `Authorization` header |
| **Rate limit** | 2 requests per 10 seconds |
| **Monthly cap** | Counts against your Pro LLM allowance (1,000 requests/month, shared across LLM-powered features) |
| **Response format** | JSON |
| **Cache duration** | 1 hour per unique query + mode combination |

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `query` | string | Yes | The question to ask. Dashes and underscores are converted to spaces. |
| `mode` | string | No | Optional mode name to weight results (e.g. `customer-support`). |
| `connection_type` | string | No | Comma-separated list of connection types to restrict retrieval to (e.g. `site,email`). Only content from those sources is returned. Valid types: `site`, `email`, `files`, `notion`, `google_doc`, `one_drive`, `basecamp`, `monday`, `youtube`, `webmention`, `custom`, `memory`. |
| `response_format` | string | No | Set to `structured` to return the generated answer with full source metadata. If omitted or set to any other value, the response remains the default Markdown payload. |

### Status codes

| Code | Description |
|------|-------------|
| `200` | Answer returned (may be `no_context` when nothing relevant was found) |
| `400` | Query parameter is missing, or `connection_type` names an unknown type |
| `401` | API key is missing or subscription required |
| `402` | Pro plan required |
| `404` | API key is invalid |
| `429` | Rate limit exceeded, or monthly LLM allowance exceeded |
| `503` | LLM service temporarily unavailable, retry |

## Examples

### Basic usage

```bash
curl -X GET "https://context-link.ai/api/v1/question?query=what-is-our-refund-policy" \
     -H "Authorization: your-api-key-here"
```

**Response:**

```json
{
    "result": "ok",
    "answer": "Our refund policy allows full refunds within 30 days of purchase, no questions asked [1]. Refunds are processed back to the original payment method within 5 business days [2].",
    "citations": [
        { "n": 1, "title": "Refund policy", "url": "https://example.com/refunds" },
        { "n": 2, "title": "Billing operations SOP", "url": "https://notion.so/..." }
    ],
    "format": "markdown"
}
```

### No context found

When the query doesn't match any indexed content, the default response has `result` set to `"no_context"` and `answer` set to `null`:

```json
{
    "result": "no_context",
    "answer": null,
    "citations": [],
    "format": "markdown"
}
```

With `response_format=structured`, the same no-context outcome returns HTTP `200` with `{ "format": "structured", "query": "...", "markdown": null, "results": [] }`.

### LLM temporarily unavailable

If the upstream LLM call fails after retries, the default response is HTTP `503` with:

```json
{
    "result": "llm_unavailable",
    "answer": null,
    "citations": [],
    "format": "markdown"
}
```

With `response_format=structured`, the HTTP `503` payload is `{ "format": "structured", "query": "...", "markdown": null, "results": [] }`. The status code distinguishes it from the same empty structured shape returned for `no_context`, so callers can retry rather than telling the end user "nothing found."

### With mode

```bash
curl -X GET "https://context-link.ai/api/v1/question?query=onboarding-checklist&mode=customer-support&connection_type=email,notion" \
     -H "Authorization: your-api-key-here"
```

### Python example

```python
import requests

api_key = "your-api-key-here"
response = requests.get(
    "https://context-link.ai/api/v1/question",
    params={"query": "what are our office hours"},
    headers={"Authorization": api_key}
)
data = response.json()
print(data["answer"])
for c in data["citations"]:
    print(f"[{c['n']}] {c['title']}: {c['url']}")
```

### JavaScript example

```javascript
const axios = require('axios');

axios.get('https://context-link.ai/api/v1/question', {
    params: { query: 'what are our office hours' },
    headers: { 'Authorization': 'your-api-key-here' }
}).then(r => {
    console.log(r.data.answer);
    r.data.citations.forEach(c => console.log(`[${c.n}] ${c.title}: ${c.url}`));
});
```

### Ruby example

```ruby
require 'net/http'
require 'json'
require 'uri'

uri = URI('https://context-link.ai/api/v1/question')
uri.query = URI.encode_www_form(query: 'what are our office hours')

req = Net::HTTP::Get.new(uri)
req['Authorization'] = 'your-api-key-here'

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)
puts data['answer']
```

## Subdomain form (details)

The `/q/{question}` and `/question/{question}` paths both hit the same endpoint on your subdomain. Use whichever reads better in context:

```bash
# JSON
curl -H "Accept: application/json" \
     "https://YOUR-SUBDOMAIN.context-link.ai/q/what-is-pricing?p=YOUR_PIN"

# Markdown (for pasting into docs)
curl -H "Accept: text/markdown" \
     "https://YOUR-SUBDOMAIN.context-link.ai/q/what-is-pricing?p=YOUR_PIN"

# Plain text (just the answer, no citations footer)
curl -H "Accept: text/plain" \
     "https://YOUR-SUBDOMAIN.context-link.ai/q/what-is-pricing?p=YOUR_PIN"
```

If you omit the `Accept` header and you're on ChatGPT or Gemini, you'll get HTML by default (they render Markdown poorly). All other agents get JSON.

## Response format

```json
{
    "result": "ok" | "no_context" | "llm_unavailable",
    "answer": "paragraph with inline [1] citations" | null,
    "citations": [{ "n": 1, "title": "...", "url": "..." }],
    "format": "markdown"
}
```

- `result`:
  - `"ok"`: an answer was produced from your context.
  - `"no_context"`: no relevant source material was found (HTTP 200). Don't retry.
  - `"llm_unavailable"`: retrieval succeeded but the LLM call failed (HTTP 503). Retry.
- `answer`: a single paragraph (≤ ~120 words), with inline `[N]` markers referencing the citations array. `null` when `result` is `"no_context"` or `"llm_unavailable"`.
- `citations`: only the sources the LLM actually used to compose the answer. Citation numbers are contiguous (`[1]`, `[2]`, `[3]` ...) and always resolve to an entry in this array.

Set `response_format=structured` on `GET /api/v1/question` to return the same schema as the structured context endpoint:

```json
{
    "format": "structured",
    "query": "refund rules",
    "markdown": "Refunds take 30 days [1], with exceptions for faulty goods [2].",
    "results": [
        {
            "content": "Refunds take 30 days [1], with exceptions for faulty goods [2].",
            "position": null,
            "rank": 1,
            "score": null,
            "content_sha": "f3bf95d8a3c2c126bd4d10e23260aca476e1bbb9619e470a54bc38cf79ce6d92",
            "sources": [
                {
                    "citation": 1,
                    "uid": "refund-policy",
                    "title": "Refund policy",
                    "url": "https://docs.example.com/refunds",
                    "source_updated_at": "2026-08-12T03:14:15Z",
                    "connection": {
                        "uid": "support-site",
                        "type": "Site",
                        "name": "docs.example.com",
                        "logo_url": "https://www.google.com/s2/favicons?sz=256&domain=docs.example.com"
                    }
                },
                {
                    "citation": 2,
                    "uid": "returns-guide",
                    "title": "Returns guide",
                    "url": "https://docs.example.com/returns",
                    "source_updated_at": null,
                    "connection": {
                        "uid": "support-site",
                        "type": "Site",
                        "name": "docs.example.com",
                        "logo_url": "https://www.google.com/s2/favicons?sz=256&domain=docs.example.com"
                    }
                }
            ]
        }
    ]
}
```

A structured question response has `markdown: null` and zero results when no answer is generated, or one result containing the generated answer. Its `position` and `score` are `null`, `rank` is `1`, `content_sha` is the SHA-256 of `content`, and `sources` contains every cited source with its citation number. Only the exact value `structured` opts in; any other value returns the unchanged default response shown above.

## Error responses

```json
// 400 Bad Request
{ "message": "A query is required" }

// 402 Payment Required
{ "message": "The question endpoint is a Pro-plan feature." }

// 429 Too Many Requests (allowance)
{ "message": "Monthly question allowance exceeded. Resets at the start of next month." }

// 503 Service Unavailable (default response)
{ "result": "llm_unavailable", "answer": null, "citations": [], "format": "markdown" }

// 503 Service Unavailable (response_format=structured)
{ "format": "structured", "query": "refund rules", "markdown": null, "results": [] }
```
