---
title: Get research
description: Read a research run: its status, its last stage, and its answer or error.
doc_version: 0.1.0-preview
last_updated: 2026-09-14
---

# Get research

`GET /namespaces/{namespace}/collections/{collection}/research/{research}`

Read a research run: its status, its last stage, and its answer or error.

**Permission:** The key that started the run, or a cluster key.

[API key types and authentication](/engine/reference/authentication.md).

progress is the last stage of the run. When the run succeeds, answer, sources, image_description, and usage are set. When it fails, error is set, with retryable.

Another key gets 404, as for a run that does not exist.

## Example request

```http
GET /namespaces/acme/collections/incident-logs/research/rsr_01K6Q6N7D8E9F0G1H2J3K4M5N6 HTTP/1.1
Host: engine.example.com
Authorization: Bearer <GRAPHON_ENGINE_API_KEY>
```

## Path parameters

### `namespace`

**string · required**

Namespace name or ns_ ID. It must be visible to this key.

### `collection`

**string · required**

Collection name or col_ ID within the namespace. It must be visible to this key.

### `research`

**string · required**

Research run ID with the rsr_ prefix, returned by Create research.

## Responses

### 200 · Success

A run that succeeded, with its answer and its sources.

Content type: `application/json`.

#### Response headers

##### `x-engine-request-id`

**string · required**

The Engine request ID. Retain it when investigating a failed or unexpected request.

##### `x-request-id`

**string · required**

The X-Request-Id you sent, echoed when it is a valid ID. Otherwise, the Engine request ID.

```json
{
  "id": "rsr_01K6Q6N7D8E9F0G1H2J3K4M5N6",
  "namespace": "acme",
  "collection": "incident-logs",
  "status": "succeeded",
  "progress": {
    "stage": "synthesizing",
    "label": "Synthesizing answer",
    "step": 3,
    "total_steps": 3
  },
  "answer": "Checkout requests stalled because the database connection pool was exhausted [[SRC:0001]].",
  "sources": [
    {
      "ref": "SRC:0001",
      "span": {
        "file_id": "fil_01K6Q6N7D8E9F0G1H2J3K4M5N6",
        "unit": "chars",
        "start": 0,
        "end": 77
      },
      "path": "/reports/incident.txt",
      "content_type": "text/plain",
      "score": 0.81,
      "cited": true
    }
  ],
  "image_description": null,
  "usage": {
    "credits": 1
  },
  "error": null,
  "created_at": "2026-10-06T10:00:00Z",
  "updated_at": "2026-10-06T10:00:00Z",
  "completed_at": "2026-10-06T10:00:00Z"
}
```

#### Response fields

##### `answer`

**string · required · nullable**

Answer text, once the run succeeds. A citation marker such as [[SRC:0001]] names the source whose ref is SRC:0001. The answer has no markers when the request set sources to none. Otherwise null.

##### `collection`

**string · required**

Name of the collection that the run answers from.

##### `completed_at`

**string (date-time) · required · nullable**

Time the run ended, as an ISO 8601 UTC timestamp. Null while it runs.

##### `created_at`

**string (date-time) · required**

Time the resource was created, as an ISO 8601 UTC timestamp.

##### `error`

**object · required · nullable**

Why the run failed, with retryable. Null unless the run failed.

###### `error.code`

**string · required**

Machine-readable reason for the failure. Use this value for program logic and message for display or diagnostics.

###### `error.message`

**string · required**

Human-readable explanation of the failure. Do not parse this text to decide how to retry.

###### `error.retryable`

**boolean · required**

Whether starting the same research again may succeed.

##### `id`

**string · required**

Immutable research run ID with the rsr_ prefix. Use it to read or cancel the run.

##### `image_description`

**string · required · nullable**

How Engine read the query image, once the run succeeds. Null without an image.

##### `namespace`

**string · required**

Name of the namespace of the collection.

##### `progress`

**object · required · nullable**

The last stage of the run, or null before the first one.

###### `progress.label`

**string · required**

Readable name of the stage, for display.

###### `progress.stage`

**string · required**

Machine-readable name of the stage.

###### `progress.step`

**integer · required**

Number of this stage, from 1.

###### `progress.total_steps`

**integer · required**

Number of stages that Engine expects for this run.

##### `sources`

**array<object> · required · nullable**

The sources in ref order, once the run succeeds: the cited sources first, then the others by score. Empty when the request set sources to none. Otherwise null.

###### `sources[].cited`

**boolean · required**

Whether the answer cites this source.

###### `sources[].content_type`

**string · required**

Content type of the source file.

###### `sources[].file`

**object · optional · nullable**

Not returned yet. The whole stored file.

###### `sources[].file.content_type`

**string · required**

Content type of the part.

###### `sources[].file.data`

**string · optional · nullable**

The bytes, as base64. Present when the part is inline.

###### `sources[].file.expires_at`

**string (date-time) · optional · nullable**

ISO 8601 UTC time after which the URL expires.

###### `sources[].file.page`

**integer · optional · nullable**

Not returned yet. The page of a page screenshot, from 1.

###### `sources[].file.seconds`

**number · optional · nullable**

Not returned yet. The time of a video frame, in seconds from the start.

###### `sources[].file.size_bytes`

**integer · required**

Size of the part in bytes, before base64.

###### `sources[].file.url`

**string · optional · nullable**

Signed URL to the bytes. It works for 15 minutes. On a client deployment it is a gs:// URI. Present when the part did not fit inline, or delivery is url.

###### `sources[].matches`

**array<object> · optional · nullable**

Not returned yet. On a cited source, one match: the passage that supports the answer.

###### `sources[].matches[].highlight`

**object · optional · nullable**

The text around the match, and where the matched words are.

###### `sources[].matches[].highlight.match_ranges`

**array<[integer, integer]> · required**

The [start, end) code point ranges of the matched words in text.

###### `sources[].matches[].highlight.text`

**string · required**

About 300 characters around the match.

###### `sources[].matches[].raw`

**object · optional · nullable**

The match cut from the file in its own format.

###### `sources[].matches[].raw.content_type`

**string · required**

Content type of the part.

###### `sources[].matches[].raw.data`

**string · optional · nullable**

The bytes, as base64. Present when the part is inline.

###### `sources[].matches[].raw.expires_at`

**string (date-time) · optional · nullable**

ISO 8601 UTC time after which the URL expires.

###### `sources[].matches[].raw.page`

**integer · optional · nullable**

Not returned yet. The page of a page screenshot, from 1.

###### `sources[].matches[].raw.seconds`

**number · optional · nullable**

Not returned yet. The time of a video frame, in seconds from the start.

###### `sources[].matches[].raw.size_bytes`

**integer · required**

Size of the part in bytes, before base64.

###### `sources[].matches[].raw.url`

**string · optional · nullable**

Signed URL to the bytes. It works for 15 minutes. On a client deployment it is a gs:// URI. Present when the part did not fit inline, or delivery is url.

###### `sources[].matches[].screenshots`

**array<object> · optional · nullable**

Images of the match.

###### `sources[].matches[].screenshots[].content_type`

**string · required**

Content type of the part.

###### `sources[].matches[].screenshots[].data`

**string · optional · nullable**

The bytes, as base64. Present when the part is inline.

###### `sources[].matches[].screenshots[].expires_at`

**string (date-time) · optional · nullable**

ISO 8601 UTC time after which the URL expires.

###### `sources[].matches[].screenshots[].page`

**integer · optional · nullable**

Not returned yet. The page of a page screenshot, from 1.

###### `sources[].matches[].screenshots[].seconds`

**number · optional · nullable**

Not returned yet. The time of a video frame, in seconds from the start.

###### `sources[].matches[].screenshots[].size_bytes`

**integer · required**

Size of the part in bytes, before base64.

###### `sources[].matches[].screenshots[].url`

**string · optional · nullable**

Signed URL to the bytes. It works for 15 minutes. On a client deployment it is a gs:// URI. Present when the part did not fit inline, or delivery is url.

###### `sources[].matches[].span`

**object · required**

The place of the match, inside the span of its result.

###### `sources[].matches[].span.bbox`

**object · optional · nullable**

Not returned yet. The box of a figure on one page. Read spans accepts it and reads the whole page.

###### `sources[].matches[].span.bbox.bottom`

**number · required**

The bottom edge, as a fraction of the page height from the top.

- Minimum: 0
- Maximum: 1

###### `sources[].matches[].span.bbox.left`

**number · required**

The left edge, as a fraction of the page width from the left.

- Minimum: 0
- Maximum: 1

###### `sources[].matches[].span.bbox.right`

**number · required**

The right edge, as a fraction of the page width from the left.

- Minimum: 0
- Maximum: 1

###### `sources[].matches[].span.bbox.top`

**number · required**

The top edge, as a fraction of the page height from the top.

- Minimum: 0
- Maximum: 1

###### `sources[].matches[].span.end`

**integer · optional · nullable**

First unit after the span. Absent when unit is file.

- Minimum: 0
- Maximum: 2147483647

###### `sources[].matches[].span.file_id`

**string · required**

ID of the file within the collection.

###### `sources[].matches[].span.pages`

**object · optional · nullable**

The pages that a chars span of a PDF or Office file covers, from 1, so a client can show a page number. Absent on every other span. Read spans refuses pages that do not match the chars.

###### `sources[].matches[].span.pages.end`

**integer · required**

First page after the range.

- Minimum: 1

###### `sources[].matches[].span.pages.start`

**integer · required**

First page of the range, from 1.

- Minimum: 1

###### `sources[].matches[].span.start`

**integer · optional · nullable**

First unit of the span. Pages count from 1. Chars and seconds count from 0. Absent when unit is file.

- Minimum: 0
- Maximum: 2147483647

###### `sources[].matches[].span.unit`

**string · required**

pages (a section or a figure of a PDF or Office file), chars (Unicode code points of the text form), seconds (whole seconds), or file (the whole file).

- `pages`: Pages of a PDF or an office file, from 1. Page 7 is start 7, end 8.
- `chars`: Unicode code points of the text form of a file, from 0: a txt, md, or mail file, or a PDF or office file.
- `seconds`: Whole seconds of a video or an audio file, from 0.
- `file`: The whole file, with no start or end.

###### `sources[].matches[].text`

**string · optional · nullable**

The text of the match.

###### `sources[].path`

**string · required**

Logical path of the source file within the collection.

###### `sources[].ref`

**string · required**

The marker of this source without its brackets, such as SRC:0001. It stands in answer as [[SRC:0001]].

###### `sources[].score`

**number · required**

Ranking value for this result. It is not a confidence percentage.

###### `sources[].span`

**object · required**

Place in the source file. Send it to Read spans to get its content.

###### `sources[].span.bbox`

**object · optional · nullable**

Not returned yet. The box of a figure on one page. Read spans accepts it and reads the whole page.

###### `sources[].span.bbox.bottom`

**number · required**

The bottom edge, as a fraction of the page height from the top.

- Minimum: 0
- Maximum: 1

###### `sources[].span.bbox.left`

**number · required**

The left edge, as a fraction of the page width from the left.

- Minimum: 0
- Maximum: 1

###### `sources[].span.bbox.right`

**number · required**

The right edge, as a fraction of the page width from the left.

- Minimum: 0
- Maximum: 1

###### `sources[].span.bbox.top`

**number · required**

The top edge, as a fraction of the page height from the top.

- Minimum: 0
- Maximum: 1

###### `sources[].span.end`

**integer · optional · nullable**

First unit after the span. Absent when unit is file.

- Minimum: 0
- Maximum: 2147483647

###### `sources[].span.file_id`

**string · required**

ID of the file within the collection.

###### `sources[].span.pages`

**object · optional · nullable**

The pages that a chars span of a PDF or Office file covers, from 1, so a client can show a page number. Absent on every other span. Read spans refuses pages that do not match the chars.

###### `sources[].span.pages.end`

**integer · required**

First page after the range.

- Minimum: 1

###### `sources[].span.pages.start`

**integer · required**

First page of the range, from 1.

- Minimum: 1

###### `sources[].span.start`

**integer · optional · nullable**

First unit of the span. Pages count from 1. Chars and seconds count from 0. Absent when unit is file.

- Minimum: 0
- Maximum: 2147483647

###### `sources[].span.unit`

**string · required**

pages (a section or a figure of a PDF or Office file), chars (Unicode code points of the text form), seconds (whole seconds), or file (the whole file).

- `pages`: Pages of a PDF or an office file, from 1. Page 7 is start 7, end 8.
- `chars`: Unicode code points of the text form of a file, from 0: a txt, md, or mail file, or a PDF or office file.
- `seconds`: Whole seconds of a video or an audio file, from 0.
- `file`: The whole file, with no start or end.

##### `status`

**string · required**

queued, running, succeeded, failed, or canceled. succeeded, failed, and canceled are terminal states.

- `queued`: Accepted and waiting to start.
- `running`: Engine is working on the answer.
- `succeeded`: The answer is ready.
- `failed`: The run ended with an error.
- `canceled`: The run ended after cancellation.

##### `updated_at`

**string (date-time) · required**

Time the resource last changed, as an ISO 8601 UTC timestamp.

##### `usage`

**object · required · nullable**

What the run used, once it succeeds. Otherwise null.

###### `usage.credits`

**integer · required**

Credits metered for this query.

## Errors

[Error format and retry guidance](/engine/reference/errors.md).

### 400 · `invalid_request`

A field, path, parameter, or conditional request rule is invalid.

Correct the request. Unknown fields are rejected.

```json
{
  "error": {
    "code": "invalid_request",
    "message": "A field, path, parameter, or conditional request rule is invalid.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 401 · `unauthenticated`

The API key is missing, invalid, or revoked.

Send an active key in Authorization: Bearer or X-API-Key.

```json
{
  "error": {
    "code": "unauthenticated",
    "message": "The API key is missing, invalid, or revoked.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 403 · `forbidden`

The key can see the resource but lacks permission for this operation.

Use a key with the required action or scope kind.

```json
{
  "error": {
    "code": "forbidden",
    "message": "The key can see the resource but lacks permission for this operation.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 404 · `not_found`

The resource is absent, deleted, or outside this key’s scope.

Check the identifier and the key’s namespace and collection scope.

```json
{
  "error": {
    "code": "not_found",
    "message": "The resource is absent, deleted, or outside this key’s scope.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

### 503 · `temporarily_unavailable`

A dependency, cluster readiness condition, or capacity limit prevents this operation.

Inspect retryable and details. Retry only when permitted, preserving the idempotency key for the same write.

```json
{
  "error": {
    "code": "temporarily_unavailable",
    "message": "A dependency, cluster readiness condition, or capacity limit prevents this operation.",
    "request_id": "req_example",
    "retryable": true,
    "details": {}
  }
}
```

### 500 · `internal_error`

An unexpected server failure prevented this request from completing.

Retain request_id for investigation. Follow retryable before retrying a write.

```json
{
  "error": {
    "code": "internal_error",
    "message": "An unexpected server failure prevented this request from completing.",
    "request_id": "req_example",
    "retryable": false,
    "details": {}
  }
}
```

## Other request languages

### cURL

```bash
curl -sS "$GRAPHON_ENGINE_URL/namespaces/acme/collections/incident-logs/research/rsr_01K6Q6N7D8E9F0G1H2J3K4M5N6" \
  -H "Authorization: Bearer $GRAPHON_ENGINE_API_KEY"
```

### Python

```python
import json
import os
import urllib.request

request = urllib.request.Request(
    os.environ["GRAPHON_ENGINE_URL"] + "/namespaces/acme/collections/incident-logs/research/rsr_01K6Q6N7D8E9F0G1H2J3K4M5N6",
    headers={
        "Authorization": "Bearer " + os.environ["GRAPHON_ENGINE_API_KEY"],
    },
    method="GET",
)
with urllib.request.urlopen(request) as response:
    print(response.read().decode())
```

### JavaScript

```javascript
const response = await fetch(process.env.GRAPHON_ENGINE_URL + "/namespaces/acme/collections/incident-logs/research/rsr_01K6Q6N7D8E9F0G1H2J3K4M5N6", {
  method: "GET",
  headers: {
    "Authorization": `Bearer ${process.env.GRAPHON_ENGINE_API_KEY}`,
  },
});
console.log(response.status, await response.text());
```

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
