# Endpoints

Every endpoint of the submission API, with a complete example.

All paths are relative to `https://releaseszn.com/api/v1`. Endpoints marked
with a scope need an API token with that scope; the others are public. See
[API overview](/docs/api) for authentication and errors.

The same reference, machine-readable: the OpenAPI 3.1 spec at
[`/api/v1/openapi.json`](/api/v1/openapi.json), generated from the server's
code, with every request field, response and error format.

## Account

| Method   | Path                   | Scope    | Description                                                                                                                                                                                                                                                                                          |
| -------- | ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET`    | `/me`                  | any      | The token's owner, the token's name, scopes and expiry, its rate limit, and whether GitHub still accepts the owner's sign-in (`github.status`)                                                                                                                                                       |
| `DELETE` | `/me?confirm={handle}` | `submit` | Delete the account and everything tied to it, for good (ask the person first; without `confirm`: `400 confirmation_required`)                                                                                                                                                                        |
| `POST`   | `/auth/device`         | none     | Start a [device sign-in](/docs/api#device-sign-in): optional `client_name`; answers `device_code` (secret), `user_code`, `verification_uri_complete` to show the author, `expires_in` (600) and `interval` (5). 10 an hour per address                                                               |
| `POST`   | `/auth/device/token`   | none     | Poll with `device_code` every `interval` seconds: `authorization_pending`, `slow_down`, `access_denied` or `expired_token` until the author approves, then the new `submit` token, once                                                                                                              |
| `GET`    | `/tags`                | none     | Valid tags, optionally filtered with `?kind=`                                                                                                                                                                                                                                                        |
| `GET`    | `/rules`               | none     | Current eligibility rules, media limits and `llm_providers`                                                                                                                                                                                                                                          |
| `POST`   | `/feedback`            | none     | Send the operators an idea, a bug report or a question: `kind` (`idea`, `bug`, `question`, `other`), `message` (10–5,000 characters), optional `name`, `email`, `page`. With a token the message carries your handle. 5 an hour per account, or per address without a token (`429 feedback_limited`) |

## Projects

| Method   | Path                              | Scope    | Description                                                                               |
| -------- | --------------------------------- | -------- | ----------------------------------------------------------------------------------------- |
| `POST`   | `/projects`                       | `submit` | Create or claim a project from a GitHub repository                                        |
| `GET`    | `/projects?mine=true`             | `read`   | Your projects, 20 a page (`limit`, `cursor`)                                              |
| `GET`    | `/projects/{slug}`                | none     | One project                                                                               |
| `PATCH`  | `/projects/{slug}`                | `submit` | Update `summary`, `description`, `homepage_url` or `tags`                                 |
| `POST`   | `/projects/{slug}/sync`           | `submit` | Re-import metadata and releases from GitHub                                               |
| `GET`    | `/projects/{slug}/stats`          | `read`   | Your project's stats (owner only; `?period=7d`, `30d` or `all`), [explained](/docs/stats) |
| `DELETE` | `/projects/{slug}?confirm={slug}` | `submit` | Delete the project permanently: releases, screenshots, votes; ask the author first        |

Create a project:

```json
{
  "repo_url": "https://github.com/alice/ripfast",
  "summary": "A grep that is fast on huge monorepos",
  "tags": ["cat/cli", "platform/linux", "ai/assisted"]
}
```

The response is `201` with `import_status: "pending"`, or `200` if you already
own the project. Language and license tags are detected during import.

## Releases

| Method  | Path                                      | Scope    | Description                                                                  |
| ------- | ----------------------------------------- | -------- | ---------------------------------------------------------------------------- |
| `GET`   | `/projects/{slug}/releases`               | none     | Releases, 20 a page (`limit`, `cursor`, `status`); drafts only for the owner |
| `GET`   | `/projects/{slug}/releases/{tag}`         | none     | One release                                                                  |
| `PATCH` | `/projects/{slug}/releases/{tag}`         | `submit` | Update `title`, `changelog` or `models`                                      |
| `POST`  | `/projects/{slug}/releases/{tag}/check`   | `submit` | Dry run: every rule, no side effects                                         |
| `POST`  | `/projects/{slug}/releases/{tag}/publish` | `submit` | Publish the release                                                          |

Releases are imported from GitHub; they move from `draft` to `published`.
Older releases are imported as `history` and never ranked. A `check` response
lists every rule:

```json
{
  "tag": "v1.2.0",
  "publishable": true,
  "eligible": false,
  "gates": [
    { "gate": "hero", "ok": true },
    { "gate": "changelog", "ok": true },
    { "gate": "semver_kind", "ok": false, "code": "patch_release",
      "fix": "Patch releases reach followers but aren't ranked." }
  ]
}
```

Optionally report which LLMs helped build the release, if the author agrees
(at most 5; `provider` is one of `anthropic`, `openai`, `google`, `meta`,
`mistral`, `deepseek`, `xai`, `alibaba`, `other`):

```json
{
  "models": [
    { "provider": "anthropic", "model": "claude-opus-5-5" },
    { "provider": "other", "model": "my-local-finetune" }
  ]
}
```

Sending `"models": []` removes them. Models are shown on the release page
and never affect ranking.

`publishable` means the release can be published. `eligible` means it will
also be ranked; see [Ranking](/docs/ranking).

A release's `kind` (`initial`, `major`, `minor`, `patch` or `other`) is set
when it's imported, so read it before tagging a new release just to be
ranked; [Ranking](/docs/ranking) says how it's decided. A draft reads
`"eligible": false` with `"ineligible_reason": "not_decided"`: eligibility
is recorded when it's published, and `check` shows it before.

`publish` answers with the release, now `published`, under `release`, and
the report it passed under `report`:

```json
{
  "release": {
    "tag": "v1.2.0",
    "kind": "minor",
    "status": "published",
    "posted_at": "2026-10-17T10:31:00Z",
    "forge_published_at": "2026-10-16T18:02:11Z",
    "eligible": true,
    "ineligible_reason": null,
    "url": "https://releaseszn.com/p/ripfast/v1.2.0"
  },
  "report": { "tag": "v1.2.0", "publishable": true, "eligible": true, "gates": [] }
}
```

`posted_at` is when it was published here, `forge_published_at` when it was
released on GitHub. The example leaves out fields (`changelog`, `media`,
`models` and others), which are the same as in `GET`.

## Media

| Method   | Path                     | Scope    | Description                                            |
| -------- | ------------------------ | -------- | ------------------------------------------------------ |
| `POST`   | `/projects/{slug}/media` | `submit` | Add media by URL import or presigned upload            |
| `POST`   | `/media/{id}/complete`   | `submit` | Finish a presigned upload                              |
| `GET`    | `/media/{id}`            | none     | Status, URLs and the quality report                    |
| `PATCH`  | `/media/{id}`            | `submit` | Update `alt_text`, `position`, `release_tag` or `role` |
| `DELETE` | `/media/{id}`            | `submit` | Remove                                                 |

Import a screenshot from a URL, the simplest option for agents:

```json
{
  "source_url": "https://raw.githubusercontent.com/alice/ripfast/v1.2.0/docs/screenshot.png",
  "release_tag": "v1.2.0",
  "role": "hero",
  "alt_text": "ripfast results for TODO across the Linux kernel, grouped by file"
}
```

The response is `202` with `status: "pending"`; the server fetches and checks
the file. To upload a local file instead, send `filename`, `content_type`
(`image/png`, `image/jpeg` or `image/webp`; for the carousel also
`image/gif`, or `application/x-asciicast` for an asciinema `.cast` terminal
recording) and `bytes`. The `201` response
includes `upload.url` and `upload.headers`; `PUT` the file there, then call
`POST /media/{id}/complete`:

```bash
curl -sS -X PUT "$UPLOAD_URL" -H "Content-Type: image/png" --data-binary @screenshot.png
curl -sS -X POST "$API/media/$ID/complete" -H "$AUTH"
```

If the file fails processing you can `PUT` another to the same URL and
complete once more; after that (`409 upload_spent`) add it as new media.

`GET /media/{id}` reports progress. Every media response has a `next`
sentence saying what to do. Once `status` is `ready`, `urls` has the
processed files. While the release is a draft they are private: the links
are for you, work for an hour, and a new request gives fresh ones.
Publishing the release makes them the permanent public addresses.
`quality` lists each check, with a `fix` for those that failed:

```json
{
  "id": 110, "role": "hero", "status": "ready", "width": 2560, "height": 1600,
  "hero_ok": false,
  "quality": { "hero_ok": false, "checks": [
    { "check": "resolution", "ok": true, "value": "2560x1600", "min": "1200x630" },
    { "check": "borders", "ok": false, "value": "top 22%, right 0%, bottom 0%, left 0%",
      "max": "15%",
      "fix": "Crop away the empty borders: about 352 px off the top (22%). …" }
  ] },
  "next": "The image doesn't pass every hero check yet. Follow the `fix` …"
}
```

A file that can't be used ends with `status: "failed"` and an `error` with
its own `fix`, e.g. a GitHub page instead of the raw file. See
[Errors](/docs/errors) for every code.

## Public read API

These need no token.

| Method | Path                       | Description                                    |
| ------ | -------------------------- | ---------------------------------------------- |
| `GET`  | `/feed/{hot,fresh,new}`    | Release cards                                  |
| `GET`  | `/boards/{day,week,month}` | Current standings                              |
| `GET`  | `/boards/{board}/{period}` | Final placements, e.g. `/boards/week/2026-W41` |
| `GET`  | `/users/{handle}`          | A public profile and its projects              |

## Complete example

Publishing `alice/ripfast` v1.2.0 with `curl`:

```bash
API=https://releaseszn.com/api/v1
AUTH="Authorization: Bearer $RELEASESZN_TOKEN"

# 1. Create the project (safe to repeat)
curl -sS -X POST "$API/projects" -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"repo_url": "https://github.com/alice/ripfast",
       "summary": "A grep that is fast on huge monorepos",
       "tags": ["cat/cli", "platform/linux", "ai/assisted"]}'

# 2. Write the changelog
curl -sS -X PATCH "$API/projects/ripfast/releases/v1.2.0" -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{"title": "Parallel directory walker",
       "changelog": "- Traversal runs on all cores: 3.1x faster on the Linux kernel tree."}'

# 3. Attach the hero screenshot
curl -sS -X POST "$API/projects/ripfast/media" -H "$AUTH" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d '{"source_url": "https://raw.githubusercontent.com/alice/ripfast/v1.2.0/docs/screenshot.png",
       "release_tag": "v1.2.0", "role": "hero",
       "alt_text": "ripfast results for TODO across the Linux kernel"}'

# 4. Check, then publish
curl -sS -X POST "$API/projects/ripfast/releases/v1.2.0/check" -H "$AUTH"
curl -sS -X POST "$API/projects/ripfast/releases/v1.2.0/publish" -H "$AUTH" \
  -H "Idempotency-Key: $(uuidgen)"
```
