API reference

API overview

Base URL, authentication, errors, idempotency, pagination and rate limits.

Coding agents: read this page as markdown.

The releaseszn API is a REST API at https://releaseszn.com/api/v1 for publishing projects and releases. It accepts and returns JSON, and it’s designed for coding agents: every error explains how to fix it, and every POST is safe to retry.

Authentication #

Requests that change data need an API token. An agent gets one by device sign-in: the author approves it in a browser and nobody copies a token. You can also sign in with GitHub and create one on Settings → API tokens. Send it on every request:

HeaderValueRequired
AuthorizationBearer rsz_…, your API tokenFor submit and read scopes
Content-Typeapplication/jsonFor requests with a body
Idempotency-KeyA unique value, such as a UUIDRecommended on every POST

Tokens have scopes:

ScopeAllows
readReading your own projects, including drafts
submitCreating and updating projects, releases and media

No scope allows voting, following or saving. Every token expires: after 30 days or 90 (the default), as you choose. GET /me says when; make a new one on /settings/tokens before then. You can have up to 20 at a time.

To check a token, call GET /me, which works with any scope:

curl -H "Authorization: Bearer $RELEASESZN_TOKEN" https://releaseszn.com/api/v1/me
{
  "user": { "handle": "alice", "display_name": "Alice" },
  "token": {
    "name": "laptop agent",
    "prefix": "rsz_ab12",
    "scopes": ["submit"],
    "expires_at": "2027-01-07T12:00:00Z"
  },
  "github": {
    "status": "ok",
    "renew_url": "https://releaseszn.com/auth/github",
    "next": "Nothing to do: GitHub accepts the author's sign-in."
  }
}

A missing token gets 401 token_missing; an unknown, revoked or expired one gets 401 token_invalid.

github is your GitHub sign-in on the website, which POST /projects uses to check that you may add a repository; GET /me asks GitHub about it at most once a minute (a new sign-in is checked at once), so polling it doesn’t spend your GitHub quota. status is ok, renew_required (no sign-in stored, or GitHub no longer accepts it: POST /projects would answer 403 github_reauth_required) or unknown (GitHub didn’t answer within three seconds). To renew it, open renew_url in a browser and sign in with GitHub, even if you are already signed in; a new API token doesn’t change it.

A token is shown only once, when you create it. Save it in a file only you can read, such as ~/.config/releaseszn/auth-header (the whole Authorization: Bearer … line, for curl -H @file), never in a chat, and revoke it on your settings page if it leaks.

Device sign-in #

How an agent without a token gets one, modelled on OAuth 2.0 device authorization (RFC 8628). Neither request needs a token.

  1. POST /auth/device, optionally with {"client_name": "Claude Code in ripfast"} (1–60 characters, shown to the author and given to the token as its name). The answer:

    {
      "device_code": "rszd_…",
      "user_code": "BCDF-GHJK",
      "verification_uri": "https://releaseszn.com/device",
      "verification_uri_complete": "https://releaseszn.com/device?code=BCDF-GHJK",
      "expires_in": 600,
      "interval": 5,
      "next": "Show the author this link and code, …"
    }
    
  2. Show the author verification_uri_complete and user_code, and nothing else: device_code is a secret. They open the link, sign in with GitHub, check the program’s name, the time and the address it asked from, and approve or deny.

  3. Meanwhile POST /auth/device/token with {"device_code": "rszd_…"} every interval seconds. Until the author decides it answers 400 authorization_pending (with Retry-After); sooner than the interval, 400 slow_down (add 5 seconds). A denial is 403 access_denied, a code past its 10 minutes 400 expired_token. Once approved it answers the token, once:

    {
      "token": "rsz_…",
      "token_type": "Bearer",
      "name": "Claude Code in ripfast",
      "scopes": ["submit"],
      "expires_at": "2027-01-16T12:00:00Z",
      "handle": "alice",
      "next": "Save the token without printing it …"
    }
    

    Save it straight to a file in the command that receives it, so it never appears in a terminal or a chat. Polling again answers 400 invalid_device_code.

The token belongs to whoever approved, has the submit scope, lasts 90 days, and is listed (and revocable) on /settings/tokens under the client’s name. It can do everything submit allows except delete the account: DELETE /me with it is 403 device_token_cannot_delete_account, and the author deletes their account on /settings themselves. An address may start 10 sign-ins an hour, and an IPv6 /48 30.

Errors #

Errors use RFC 9457 problem details, with two extra fields: a stable code and a fix sentence that says what to do next.

{
  "type": "https://releaseszn.com/docs/errors#hero_required",
  "title": "Release needs a hero screenshot",
  "status": 422,
  "code": "hero_required",
  "detail": "Release v1.2.0 has no hero image.",
  "fix": "Upload a screenshot with role \"hero\" and release_tag \"v1.2.0\".",
  "errors": [{ "field": "media", "code": "hero_required" }]
}
StatusMeaning
400The request is malformed
401The token is missing, invalid or revoked
403The token lacks the scope, or you can’t manage this repository
404The project, release or media doesn’t exist
409The request conflicts with the current state
422The request is valid but a rule failed; read code and fix
429Rate limited; wait for the Retry-After header

Idempotency #

Send an Idempotency-Key header with every POST. If you retry with the same key within 24 hours, you get the stored response instead of a duplicate (Idempotent-Replayed: true). Using the same key with a different body returns 422 idempotency_key_reused.

Only successes are stored. A refusal (4xx) changed nothing, so a retry with the same key runs again: fix what the error says and send the same request. If the first request is still running when the retry arrives, the retry gets 409 request_in_progress; wait a second and send it again. Device sign-in (POST /auth/device, POST /auth/device/token) is never stored: its answers hold secrets that are handed out once.

Creating a project that already exists returns the existing project, so retrying POST /projects is always safe.

Pagination #

The list endpoints (GET /projects?mine=true and GET /projects/{slug}/releases) return 20 items per page, newest first.

NameLocationDescription
limitQuery parameterItems per page: 1 to 100, 20 when left out
cursorQuery parameterOpaque cursor from a previous response
next_cursorResponse fieldCursor for the next page, or null on the last page

Follow next_cursor until it is null; a list that fits on one page has "next_cursor": null. To find a project’s drafts, filter instead of paging: GET /projects/{slug}/releases?status=draft. A limit outside 1–100 or a cursor that isn’t one of ours returns 400 invalid_query.

Rate limits #

LimitValue
Requests per token60 per minute
Requests without a valid token, per address60 per minute
Device sign-ins started, per address10 per hour
Device sign-ins started, per IPv6 /4830 per hour
New projects, accounts under 30 days old on GitHub3 per day

Every response to a request with a token carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until a request frees up), and GET /me includes the same numbers as rate_limit. Requests over a limit get 429 with a Retry-After header in seconds: wait that long and retry with the same Idempotency-Key. Requests without a token, and those whose token is wrong, expired or revoked, share one budget per address, so a script that keeps sending a bad token gets 429 instead of 401 after 60 tries.

Asynchronous work #

Imports and media processing run in the background. Resources expose a status field to poll: import_status on projects and status on media.

Versioning #

The version is part of the URL. Breaking changes (a field removed or renamed, a type or a status code changed, a new required input) ship as /api/v2, and /api/v1 keeps answering beside it. Additions happen in place: new endpoints, new optional inputs, new response fields, new error codes. So ignore fields you don’t recognize, and treat an unknown error code by its HTTP status. The changelog lists every change to the API, and /api/v1/openapi.json always describes what is running.

Next steps #