---
title: "List sites belonging to the authenticated principal."
method: GET
path: "/sites"
tags: ["Sites"]
---

# List sites belonging to the authenticated principal.

`GET /sites`

## Request

- Base URL: `https://api.stacktr.ee`
- URL: `https://api.stacktr.ee/sites`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.stacktr.ee/oauth/authorize`, token URL `https://api.stacktr.ee/oauth/token`, scopes: `sites:write`, `sites:read`)

Viewer numbers are plan-gated: on a plan without them each site comes back with `metrics_locked: true` and null `view_count`, `unique_viewers` and `last_viewed_at`, plus a boolean `opened`. The redaction is server-side, so an agent sees exactly what the dashboard does. Each site carries its `client` space (or null). Pages that expired or were deleted stay in this list rather than disappearing: check `deleted_at` before handing anyone a `url`, because a row with `deleted_at` set is a dead link that POST /sites/{idOrSlug}/restore can revive until `restorable_until`.

## Query parameters

- `client` string
- `limit` integer
- `before` integer
- `before_id` string

## Response `200`

OK. Newest-updated first. When `has_more` is true, call again with `before` = `next_before` and `before_id` = `next_before_id`, and repeat until it is false.

- object
  - `sites` Site[]
    - `id` string, required: Stable site identifier.
    - `slug` string, nullable: Public subdomain slug, if the site is public.
    - `visibility` 'unlisted' | 'public' | 'burn', required: Privacy mode.
    - `url` string, uri: Canonical URL to view the site.
    - `created_at` integer, required: Unix epoch seconds.
    - `updated_at` integer: Unix epoch seconds.
    - `expires_at` integer, nullable: Unix epoch seconds; null = no expiry. Clamped to the plan ceiling at write time (Free: 7 days, anonymous: 24 hours), so this can come back sooner than you asked for.
    - `expires_at_iso` string, nullable: The same instant as `expires_at`, RFC 3339 UTC, second granularity (e.g. "2026-09-08T12:00:00Z"). Present so a client can show a deadline without reconstructing it from the epoch.
    - `ttl_seconds` integer, nullable: Seconds from now until the page stops serving. Null when it does not expire; 0 when it is already out of time. Never negative.
    - `expiry_clamped` boolean: True when the lifetime THIS request asked for was longer than the plan allows and `expires_at` is the shortened reality. Always present, false included. False on reads, which ask for no lifetime. Asking for a page that never expires on a capped plan is refused (409 `expiry_clamped`) rather than clamped, unless `accept_clamp` is sent.
    - `expiry_ceiling_hours` integer, nullable: The longest lifetime this plan allows a page, in hours: 24 anonymous, 168 on Free, null on plans with no ceiling and on the paid one-off rails.
    - `expiry_source` 'request' | 'plan_ceiling' | 'plan_default' | 'stored': What decided the date: `request` (the caller named it), `plan_ceiling` (the plan cap did), `plan_default` (the plan default did), or `stored` (this call only read the row).
    - `byte_size` integer: Total content size in bytes.
    - `email_gate` object, nullable
      - `domain` string
      - `required` boolean
    - `metrics_locked` boolean: True when the plan has no viewer numbers (anonymous and Free). The three viewer fields below are then null; there is no number to reveal, blur, or approximate.
    - `opened` boolean: Whether anyone has opened the page. Returned on every plan, including when metrics_locked is true, so "someone read this" survives the lock.
    - `badged` boolean: Whether THIS page carries the "Made with Stacktree" footer as served on its stacktr.ee URL. Returned by GET /sites/{idOrSlug} only. A fact about the page, not about the plan: pages published before 2026-08-03, generated client portals and end-to-end encrypted pages carry no badge even on Free, and a remove_badge entitlement lifts it off one page or a whole account. Never assume it from the plan; anything offering to remove the badge should read this.
    - `view_count` integer, nullable: Raw view count. Null when metrics_locked.
    - `unique_viewers` integer, nullable: Distinct human viewers over the last 30 days. Null when metrics_locked.
    - `last_viewed_at` integer, nullable: Unix epoch seconds of the most recent open. Null when metrics_locked, or when never opened.
    - `reading` object, nullable: Returned by GET /sites. How closely the newest named reader read this page: their visits and reading time, the sender's own previews excluded. Null when that open carried no name, or when metrics_locked. `seconds` is null on a plan without reading detail (Solo).
      - `by` string
      - `others` integer: How many other named readers opened it.
      - `visits` integer
      - `seconds` integer, nullable
    - `client` object, nullable: Client space this page is filed under, or null for a floating page.
      - `slug` string
      - `name` string
    - `client_url` string: Present on publish when the space has a connected hostname and the page got a space_path: the page’s address on the client’s own domain (e.g. https://acme.theiragency.com/june-report). Prefer handing this link to the user.
    - `recipient` object, nullable: Present on publish when the request named who the page is for (`recipient`). `link` is their own link (`?to=` on the page URL): hand that over rather than `url`, and every open through it comes back with their name. On GET /sites: the newest person the page was named for, on every plan (whether they opened it is a viewer number and is not here), or null.
      - `name` string
      - `link` string
      - `added_at` integer: GET /sites only. Unix seconds when they were named.
      - `others` integer: GET /sites only. How many other people the page was named for.
    - `claim_token` string, nullable: Anonymous publishes only. Adopts the page into an account (POST /sites/{idOrSlug}/claim) and, until then, is its keyless update credential. As sensitive as the page.
    - `claim_url` string, uri: Anonymous publishes only. The dashboard claim screen for this page: sign in, and the page moves into that account at the same address.
    - `keep_url` string, uri: Anonymous publishes only. Give this to the person you published for: it opens a page that says when this page stops working and offers both ways to keep it (a free account, or the link by email); opening it keeps nothing until they choose one. It also marks their browser so the page itself shows them a Keep button. It carries the claim token, so it is as sensitive as claim_url: share `url`, never this.
    - `deleted_at` integer, nullable: Unix epoch seconds the page stopped serving; null when it is live. Non-null means the URL is dead for everyone holding it.
    - `delete_reason` string, nullable: Why it stopped serving: `expired`, `owner` (someone deleted it), `abuse` (a takedown), or `merged` (folded into another page; see `merged_into`). null while the page is live. `abuse` and `merged` are never restorable: POST /sites/{idOrSlug}/restore refuses them, so do not offer a restore control for either.
    - `restorable_until` integer, nullable: Unix epoch seconds. A real deadline, not a hint: the hourly cleanup destroys the content at that moment and restore returns 404 from then on.
    - `likely_revision_of` object: On POST /sites only, and only when the page looks like a new version of one this account already has: the same title (changed in the last 24 hours, same client or none) or the same client and client_path (last 30 days). The page was still published, as asked, at a new link; this names the earlier page and the call that would have kept its link (`hint`). Next time, PUT /sites/{id} on it instead of publishing again. When `can_merge` is true, POST /sites/{newId}/merge folds the two together now. Ignore it if this is a separate document.
      - `id` string
      - `url` string, uri
      - `title` string, nullable
      - `matched_on` 'title' | 'client_path'
      - `last_changed_at` integer: Unix epoch seconds the earlier page last changed.
      - `last_changed_at_iso` string, nullable
      - `minutes_ago` integer
      - `client` object
        - `slug` string
        - `name` string
      - `can_merge` boolean: Whether POST /sites/{newId}/merge would fold the earlier page into this one now.
      - `hint` string: The same facts in a sentence, safe to repeat.
    - `merged_into` object, nullable: On GET /sites/{idOrSlug} for a page with delete_reason `merged`: the page that holds its content now, and that its link opens.
      - `id` string
      - `url` string, uri
      - `title` string, nullable
  - `has_more` boolean: More pages remain.
  - `next_before` integer, nullable: Cursor for the next page; null on the last page.
  - `next_before_id` string, nullable: Cursor for the next page; null on the last page.

## Other responses

- `401`: Missing or invalid credentials.

---

[API](https://skmtc.dev/stacktr/apis/stacktree-api.md) · [All operations](https://skmtc.dev/stacktr/apis/stacktree-api/llms.txt) · [OpenAPI document](https://skmtc.dev/stacktr/apis/stacktree-api/revisions/5878cce46977?raw)
