# HTTP API

Anonymous JSON reads of the profiles, projects, activity, and evidence a developer chose to publish.

## Quickstart

No key, no account, and no headers. Every read answers `no-store`, because a projection carries its own freshness rather than borrowing it from a cache. An empty `items` array is a successful answer, not an error.

```sh
curl -sS 'https://seorak-public-directory.glendonchin.workers.dev/v1/search?q=harbor&limit=10'
```

## What is in a response

Every surface is a separate grant, so a profile visible on one can be absent from another. Measured values arrive with their coverage, sample size, and freshness, and a measurement that could not be taken is absent rather than zero.

### Included when the publisher grants it

- Public slugs, names, headlines, bios, locations, and links
- Frozen activity days and streak state
- Project stories, roles, periods, and technologies
- Selected measurements with coverage and sample size
- Publication version and timestamps

### Never included

- Prompts, messages, code, or commands
- Tool arguments, diffs, stdout, or stderr
- File paths, raw events, or commit messages
- Owner, session, repository, or device identifiers
- Credentials or private integration payloads

## Reading a measurement

Read `availability`, `coverage`, and `freshness` before the value itself. Each case below returns a number that would be wrong to chart on its own.

### unavailable: The source could not support the measurement

A capability gap, not an absence of work. The value is null, and `availability.reason` names what was missing.

- Render the reason, or omit the measurement entirely.
- Never render zero, an empty bar, or anything a reader could mistake for a measured low.

```json
{
  "value": null,
  "unit": "percent",
  "availability": {
    "state": "unavailable",
    "reason": "not-captured"
  },
  "sampleSize": 0,
  "coverage": {
    "period": {
      "from": "2026-05-03",
      "through": "2026-08-01"
    },
    "complete": false
  },
  "generatedAt": "2026-08-01T06:00:00.000Z",
  "freshness": {
    "state": "fresh",
    "generatedAt": "2026-08-01T06:00:00.000Z",
    "dataThrough": null,
    "staleAt": "2026-08-08T06:00:00.000Z"
  }
}
```

### partial: The measured window is incomplete

The value is real, but `coverage.complete` is false, so it describes only the days the record can account for.

- Render the value as a floor, with its period and sample size beside it.
- Never render the value as a total, or a comparison against a complete window.

```json
{
  "value": 0.62,
  "unit": "percent",
  "availability": {
    "state": "partial",
    "reason": "not-retained"
  },
  "sampleSize": 88,
  "coverage": {
    "period": {
      "from": "2026-05-03",
      "through": "2026-08-01"
    },
    "complete": false
  },
  "generatedAt": "2026-08-01T06:00:00.000Z",
  "freshness": {
    "state": "fresh",
    "generatedAt": "2026-08-01T06:00:00.000Z",
    "dataThrough": "2026-08-01T00:00:00.000Z",
    "staleAt": "2026-08-08T06:00:00.000Z"
  }
}
```

### stale: The measurement is past its freshness boundary

Freshness travels with the measurement. After `freshness.staleAt` the value remains true of its window and is no longer true of today.

- Render the value with its `dataThrough` date, described as stale.
- Never render the value as current, or a trend drawn through to today.

```json
{
  "value": 412,
  "unit": "count",
  "availability": {
    "state": "available",
    "reason": null
  },
  "sampleSize": 412,
  "coverage": {
    "period": {
      "from": "2026-05-03",
      "through": "2026-06-30"
    },
    "complete": true
  },
  "generatedAt": "2026-06-30T06:00:00.000Z",
  "freshness": {
    "state": "stale",
    "generatedAt": "2026-06-30T06:00:00.000Z",
    "dataThrough": "2026-06-30T00:00:00.000Z",
    "staleAt": "2026-07-07T06:00:00.000Z"
  }
}
```

## Endpoints

Every read is a `GET` and takes no credentials. Each one has its own page, with its parameters, its whole response, and a worked call. A field the publisher did not grant to this surface is absent from the response rather than null.

- [GET Search](https://seorak.app/docs/api/search.md)
- [GET Get profile](https://seorak.app/docs/api/profile.md)
- [GET Get activity](https://seorak.app/docs/api/activity.md)
- [GET List projects](https://seorak.app/docs/api/projects.md)
- [GET Get project](https://seorak.app/docs/api/project.md)

## Pagination

Listing reads accept `limit` and return `nextCursor`. A null cursor means there is no further page.

A cursor is opaque and bound to the query that produced it. Send it back to the same endpoint with the same parameters. Do not decode a cursor, construct one, or reuse it against a different profile or query.

## Errors

A failure returns a JSON body carrying a single `error` code. Honor `Retry-After` whenever the response includes it.

- `bad_request` (400) A path, query, cursor, or bounded request is invalid.
- `not_published` (404) No current projection is available to this public surface.
- `rate_limited` (429) The public read budget is exhausted. Honor Retry-After when present.
- `unavailable` (503) A required hosted dependency or security binding is unavailable.

## More

- Surface overview: https://seorak.app/docs/api.md
- Whole reference in one document: https://seorak.app/llms-full.txt
- Machine-readable HTTP schema: https://seorak.app/openapi.json

---

Source: https://seorak.app/docs/api
This document: https://seorak.app/docs/api.md
