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

# Responses & pagination

> The response envelope, offset and cursor pagination, request ids and filtering.

Every response is JSON in a fixed envelope. Successes carry `data` and `meta`; failures
carry `error`. Both always include a request id.

<CodeGroup>
  ```json Success theme={null}
  {
    "data": { "id": "…", "name": "Backend hire — Q1" },
    "meta": {
      "request_id": "req_4f2a9c1e8b7d4a5f9e3c2b1a8d7f6e5c",
      "page": 1,
      "page_size": 20,
      "total": 42,
      "has_more": true
    }
  }
  ```

  ```json Failure theme={null}
  {
    "error": {
      "code": "NOT_FOUND",
      "message": "Resource not found",
      "request_id": "req_4f2a9c1e8b7d4a5f9e3c2b1a8d7f6e5c"
    }
  }
  ```
</CodeGroup>

`data` is a single object for a detail endpoint and an array for a listing. A `204` has no
body at all — `DELETE /v1/assessments/{id}`, and the invitation resend and revoke actions.

## Request ids

Every response carries one, in `meta.request_id` (or `error.request_id`) and in the
`X-Request-Id` header.

<Tip>
  Log it on every call, especially failures. With a request id we can find your exact
  request; without one, a `500` from three days ago is unfindable.
</Tip>

## Offset pagination

Used where the collection is bounded: challenges, assessments, invitations.

```bash theme={null}
curl "https://app.dotportion.com/api/v1/assessments?page=2&page_size=50" \
  -H "Authorization: Bearer $DOTPORTION_TEST_KEY"
```

`meta` returns `page`, `page_size`, `total` and `has_more`. Page size defaults to 20 and
maxes at 100 — asking for more is a `400`, not a silent clamp.

## Cursor pagination

Used where the collection grows without bound: submissions and the event log. An offset
walk would both drift as new rows arrive and get slower the deeper you go.

Follow `meta.next_cursor` until it comes back `null`:

<CodeGroup>
  ```bash Bash theme={null}
  cursor=""
  while : ; do
    page=$(curl -sS "https://app.dotportion.com/api/v1/submissions?limit=50&cursor=$cursor" \
      -H "Authorization: Bearer $DOTPORTION_TEST_KEY")
    echo "$page" | jq '.data[]'
    cursor=$(echo "$page" | jq -r '.meta.next_cursor // empty')
    [ -z "$cursor" ] && break
  done
  ```

  ```javascript JavaScript theme={null}
  let cursor = null;
  do {
    const url = new URL('https://app.dotportion.com/api/v1/submissions');
    url.searchParams.set('limit', '50');
    if (cursor) url.searchParams.set('cursor', cursor);

    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.DOTPORTION_TEST_KEY}` },
    });
    const { data, meta } = await res.json();
    for (const submission of data) handle(submission);

    cursor = meta.next_cursor;
  } while (cursor);
  ```

  ```python Python theme={null}
  cursor = None
  while True:
      params = {"limit": 50}
      if cursor:
          params["cursor"] = cursor
      body = requests.get(
          "https://app.dotportion.com/api/v1/submissions",
          params=params,
          headers={"Authorization": f"Bearer {key}"},
      ).json()

      for submission in body["data"]:
          handle(submission)

      cursor = body["meta"]["next_cursor"]
      if not cursor:
          break
  ```
</CodeGroup>

<Warning>
  Treat the cursor as **opaque**. Its encoding is ours to change, and a hand-built one is a
  `400`. Pass back exactly what `next_cursor` gave you.
</Warning>

The event log paginates on a sequence number instead: pass the last `seq` you saw as
`since_seq`. `meta.next_cursor` is that number.

## Filtering

Listings share a filter vocabulary where it applies:

| Parameter         | Meaning                                                           |
| ----------------- | ----------------------------------------------------------------- |
| `status`          | Filter by the resource's status. Allowed values are per endpoint. |
| `since` / `until` | ISO-8601 bounds on `created_at`.                                  |
| `assessment_id`   | Restrict submissions to one assessment.                           |
| `candidate_email` | Restrict submissions to one candidate.                            |

<Note>
  There is no `sort` parameter. Listings return newest first; cursor ordering depends on
  that, so exposing a sort needs its own design. Filter down and sort client-side.
</Note>

## Dates and numbers

* Timestamps are **ISO-8601 UTC** strings: `2026-01-15T12:00:00.000Z`.
* Money is a **number** in dollars, not a string and not cents: `1.25`.
* A field that isn't available is `null` rather than absent, except where noted — an
  ungraded report omits `verdict_label`, `summary` and `scored_as_role` entirely.
