---
title: Reports
description: File a bug report or a feature request, list the ones your organization has filed, and read the status of each — the shapes, the limits, and the notification step.
order: 9
---

# Reports

Reports are how you tell us something is broken or missing. Filing one stores it against your organization and notifies us; listing them shows you everything your organization has filed and where each one stands.

For the workflow, including what makes a report actionable, see [Support](/support).

All endpoints require an organization API key. **A read-only key is enough to file** — reporting a problem is not a change to your content.

## File a report

```
POST /v1/reports
```

```bash
curl -s -X POST "$DOCSARY_API/v1/reports" \
  -H "authorization: Bearer $DOCSARY_KEY" \
  -H "content-type: application/json" \
  -d '{
    "kind": "bug",
    "title": "Search returns nothing for hyphenated terms",
    "body": "Searching for drop-in on our site returns zero results, but drop in finds the page. Reproduced on two versions.",
    "page_url": "https://docs.acme.com/guide/search",
    "contact": "docs@acme.com",
    "meta": { "client": "ci", "version": "2.4.1" }
  }'
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `kind` | string | yes | `bug` or `feature`. Nothing else is accepted. |
| `title` | string | yes | 3–200 characters after trimming. One line; the detail goes in `body`. |
| `body` | string | yes | 10–10000 characters after trimming. Plain text. |
| `page_url` | string | no | The page the report is about. An `http`/`https` URL, a site-relative path, or a `#fragment`. |
| `contact` | string | no | An email address, at most 200 characters. We reply to it. |
| `meta` | object | no | Anything that helps — client name, version, browser. At most 2000 characters once serialized. |

```json
{
  "id": "rep_9f2c4a1b7e6d8035c1a4b9e2f7d60a83",
  "kind": "bug",
  "title": "Search returns nothing for hyphenated terms",
  "status": "open",
  "created_at": 1756300000000,
  "email": { "status": "sent" }
}
```

`201` on success. The report is stored before we try to notify ourselves, so the `id` in this response is a record that exists whatever the `email` block says.

### The `email` block

Every report is stored first and notified second. This block reports what the second step did, so you never have to guess whether it happened.

| `status` | Meaning |
| --- | --- |
| `sent` | We were notified. |
| `skipped` | Notifications are not configured right now, with a `reason`. |
| `failed` | The notification could not be delivered, with an `error`. |

**A `skipped` or `failed` notification does not lose your report.** It is stored, it has an id, it appears in your list, and we work from the stored record. This block exists so the response tells you exactly what happened rather than implying more than did.

### Errors

| Response | Cause |
| --- | --- |
| `400 {"error":"kind must be one of: bug, feature"}` | `kind` missing or not one of the two |
| `400 {"error":"title required (string)"}` | `title` missing or not a string |
| `400` containing `title must be 3..200 characters` | Title too short or too long |
| `400 {"error":"body required (string)"}` | `body` missing or not a string |
| `400` containing `body must be 10..10000 characters` | Body too short or too long |
| `400` containing `unsafe page_url` | `page_url` is not an allowed URL shape |
| `400 {"error":"contact must look like an email address"}` | `contact` is not an email address |
| `400 {"error":"meta must be an object"}` | `meta` is an array or a scalar |
| `400` containing `meta is too large` | `meta` serializes to more than 2000 characters |
| `401 {"error":"missing bearer token"}` | No key. There is no anonymous filing path |
| `403 {"error":"org API key required (not admin token)"}` | A report belongs to an organization |
| `429` containing `reports per hour` | Your organization's hourly filing limit |

The `page_url` allow-list is the one used for site settings and markdown links: `http:`, `https:`, `mailto:`, a path beginning with a single `/`, or a `#fragment`. `javascript:`, `data:`, protocol-relative `//host`, and schemeless relative paths are refused:

```
{"error":"unsafe page_url (allowed: http:, https:, mailto:, or a /site-relative path)"}
```

### Filing limit

An organization may file 20 reports per hour. The 21st is refused with `429`:

```json
{
  "error": "too many reports: this organization may file 20 reports per hour. Add the detail to an existing report, or wait and file again.",
  "limit": 20,
  "window": "hour"
}
```

Nothing is stored, and the reports you already filed are untouched. The limit is per organization and counted over the preceding hour rather than reset on the clock hour. If you have a real reason to exceed it — a batch of findings from an audit, say — ask us rather than looping.

## List your reports

```
GET /v1/reports
```

```bash
curl -s -H "authorization: Bearer $DOCSARY_KEY" \
  "$DOCSARY_API/v1/reports?status=open&limit=20"
```

```json
{
  "reports": [
    {
      "id": "rep_9f2c4a1b7e6d8035c1a4b9e2f7d60a83",
      "kind": "bug",
      "title": "Search returns nothing for hyphenated terms",
      "body": "Searching for drop-in on our site returns zero results...",
      "page_url": "https://docs.acme.com/guide/search",
      "contact": "docs@acme.com",
      "meta": { "client": "ci", "version": "2.4.1" },
      "status": "open",
      "created_at": 1756300000000
    }
  ],
  "next_offset": null
}
```

Newest first. You see your own organization's reports and nothing else — the organization is taken from your key, never from the request.

| Parameter | Default | Notes |
| --- | --- | --- |
| `status` | all | `open`, `triaged`, or `closed`. Any other value is a `400`. |
| `limit` | `50` | Capped at `200`. |
| `offset` | `0` | Rows to skip. |

`next_offset` is the `offset` to pass for the next page, or `null` once you have reached the end — the same paging shape as [rerender](/api/publish#rerender).

| `status` | Meaning |
| --- | --- |
| `open` | Received, not yet looked at |
| `triaged` | Read and understood, and on a list |
| `closed` | Done, or decided against |

Status is set by us. There is no endpoint for an organization to change it; to reopen something, file a new report and quote the old id in the body.

### Errors

| Response | Cause |
| --- | --- |
| `400` containing `unknown status` | `status` is not one of the three |
| `400 {"error":"limit must be a positive number"}` | `limit` is zero, negative, or not a number |
| `400 {"error":"offset must be zero or a positive number"}` | `offset` is negative or not a number |
| `401 {"error":"missing bearer token"}` | No key |
| `403 {"error":"org API key required (not admin token)"}` | The list is scoped to an organization |
