API Reference
Complete reference for the Pagey Site REST API. All endpoints are prefixed with /api unless noted otherwise.
https://pagey.site/api
application/json for most requests. multipart/form-data for file uploads.
Most responses are JSON. Downloads, form embeds, and browser submissions return other formats as documented. JSON errors return { "error": "..." }.
Authentication
The API supports two authentication mechanisms. Most endpoints accept either, but some (like creating API tokens) require session authentication only.
Browser-based. Set automatically after POST /auth/login. The cookie pagey_session is HttpOnly and SameSite=Lax.
For programmatic access. Pass your API token in the Authorization header: Bearer <token>
Errors & Rate Limits
| Status | Meaning |
|---|---|
| 400 | Bad request — invalid input, missing required fields |
| 401 | Unauthenticated — missing or invalid credentials |
| 403 | Forbidden — authenticated but not authorized, or feature not on your tier |
| 404 | Not found — resource doesn't exist or was deleted |
| 409 | Conflict — e.g. subdomain already in use, email already registered |
| 410 | Gone — site has expired or form no longer accepts responses |
| 429 | Rate limited or tier quota exceeded |
| 502 | Upstream error — AI provider returned an error |
| 503 | Service unavailable — subdomain exhaustion, DB unreachable |
RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset. All /api/ routes share a global limiter. Auth, upload, site-mutation, AI-generation, and public form-submission routes have additional tighter limiters.
Authentication
Register, log in, verify email, and manage passwordsCreates a new user account with the free tier. Sends a verification email with a 24-hour token. The account cannot log in until email is verified.
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | Valid email address. Normalized to lowercase. | |
| password | string | required | 8–200 characters. |
Authenticates the user and sets an HttpOnly session cookie. Sessions expire after 90 days (configurable). Email must be verified.
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | User's email address. | |
| password | string | required | User's password. |
Invalidates the current session (deletes the DB row) and clears the session cookie. Redirects to / with a 302. Safe to call even when unauthenticated.
Redirects to /. Cookie is cleared.
Returns the authenticated user's profile. Works with both session cookie and Bearer token.
Verifies the user's email using the token from the verification email. Token is valid for 24 hours. On success, redirects to /login.
| Param | Type | Required | Description |
|---|---|---|---|
| token | string | required | Raw verification token from the email link. |
Redirects to /login on success.
Sends a password reset link to the given email if an account exists. Always returns 200 to prevent email enumeration. A random delay is added when the email is not found. Max 3 active reset tokens per user; oldest are pruned if exceeded. Reset links expire after 15 minutes (configurable).
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | The email address to send the reset link to. |
Resets the user's password using the token from the reset email. On success, the token is consumed, all active sessions are deleted, and the session cookie is cleared.
| Field | Type | Required | Description |
|---|---|---|---|
| token | string | required | Raw reset token from the email link. |
| newPassword | string | required | New password. 8–200 characters. |
User Sessions
Manage login sessions across devicesReturns all non-expired sessions for the authenticated user. The current session is identified with isCurrent: true. Token hashes are never returned.
Revokes a session by ID. If the revoked session is the current one, the session cookie is also cleared. Only the owner can revoke their own sessions.
| Param | Type | Description |
|---|---|---|
| sessionId | UUID | The session ID to revoke. |
Revokes all sessions except the current one ("log out everywhere else"). The current session remains active.
Revokes ALL sessions for the user, including the current one. The session cookie is cleared. The user must log in again.
API Tokens
Long-lived tokens for programmatic API accessCreates a new API token for the authenticated user. Requires session-cookie authentication — an existing API token cannot create another one (prevents token escalation). The raw token is returned once; it cannot be retrieved again.
| Field | Type | Required | Description |
|---|---|---|---|
| label | string | optional | Human-readable label. Max 100 characters. Useful for identifying the token's purpose. |
Returns all API tokens for the user — both active and revoked. Token hashes are never included. Use revoked_at to distinguish active from revoked tokens. Ordered by created_at DESC.
last_used_at is updated lazily with a 5-minute debounce to avoid write amplification on every request.Soft-revokes the token by setting revoked_at. The token immediately stops working for authentication. The row is retained for audit purposes and is physically purged after 365 days. Only the token owner can revoke it.
| Param | Type | Description |
|---|---|---|
| tokenId | UUID | The API token's ID (from the list response). |
Sites
Create, manage, and configure hosted sitesCreates a new site from an uploaded file. Accepts multipart/form-data. The file must be either index.html (single file) or a .zip archive containing index.html at its root. ZIP uploads undergo bomb-protection checks (compression ratio, file count, total size).
| Field | Type | Required | Description |
|---|---|---|---|
| file | file | required | index.html or a .zip archive containing index.html at root. |
| title | string | optional | Site title. Max 255 characters. |
| password | string | optional | Password-protect the site. Min 6 characters. |
| expiresAt | ISO datetime | optional | When the site expires. Must be in the future. |
| customSubdomain | string | optional | Custom subdomain slug. Free for registered users. Lowercase letters, numbers, hyphens. Max 63 chars. |
Returns a paginated list of sites owned by the authenticated user.
| Param | Type | Default | Description |
|---|---|---|---|
| page | number | 1 | Page number. |
| pageSize | number | 20 | Results per page. Max 100. |
| status | string | — | Filter by status: active, deleted, expired. |
| search | string | — | Filter by subdomain or title (case-insensitive substring). |
Returns a single site by ID. Returns 404 if not found, 410 if expired. Only the owner can retrieve it.
Updates one or more metadata fields. Changing or removing the site password immediately revokes all active visitor access sessions. Sending only the fields you want to change — absent fields are not touched.
| Field | Type | Description |
|---|---|---|
| title | string | null | Site title. Null clears it. |
| expiresAt | ISO datetime | null | New expiry. Must be in the future. Null removes expiry and resets status to active. |
| password | string | null | "" | New site password (min 6 chars). null or empty string removes password protection. |
| countryRestriction | object | Complete country rule: {"mode":"allow","countries":["IN","US"]}. Mode is off (default), allow (only selected countries), or block (all except selected countries). Countries are two-letter ISO country/territory codes; Kosovo uses XK. Codes are normalized to uppercase and deduplicated. An enabled rule requires at least one country. Send {"mode":"off","countries":[]} to disable. |
Country restrictions apply to every hosted page and asset, linked form embeds and submissions, and visitor password authentication. Unknown locations are denied while a rule is enabled. The signed-in site owner can preview from any country; passwords, share tokens, and temporary password overrides do not bypass country rules. Restricted responses use Cache-Control: private, no-store. Location is based on IP address, so VPNs and proxies can change the detected country. The returned site object includes countryRestriction.
Soft-deletes the site by setting status: deleted. This is one-way for users: no restore endpoint exists. Physical file deletion happens after the configured retention period.
Replaces the site's content with a new upload. The old active folder is asynchronously deleted after the transaction commits.
| Field | Type | Required | Description |
|---|---|---|---|
| file | file | required | index.html or a .zip archive. |
Recursively returns files and directories from the active site folder. Optional query parameter path selects a subtree. The response includes byte/file totals, configured limits, MIME types, timestamps, and contentRevision.
Accepts multipart files, optional JSON-array paths, optional destination, and optional boolean overwrite. Matching paths preserve a dragged folder hierarchy. The mutation publishes directly to the active folder and advances its content revision.
JSON body: { "path": "assets/images" }. The immediate parent must already exist.
JSON body: { "sourcePath": "assets/old.png", "destinationPath": "images/new.png" }. The destination parent must exist and the destination must not.
JSON body: { "paths": ["unused.css", "images/old"] }. Accepts up to 100 paths. Root index.html cannot be deleted, moved, or renamed.
Returns the site's current active folder as a ZIP archive. Response has Content-Type: application/zip and Content-Disposition: attachment; filename="subdomain.zip".
Binary ZIP stream.
Replaces all tags on the site. This is a full replacement (not an append). Max 5 tags, each max 30 characters. Duplicates are deduplicated. Empty strings are ignored.
| Field | Type | Required | Description |
|---|---|---|---|
| tags | string[] | required | Array of tag strings. Pass [] to remove all tags. |
Changes the site's subdomain. The new subdomain must be unique. Old subdomain ceases to work immediately. Valid format: lowercase letters, numbers, and hyphens; max 63 chars; not reserved.
| Field | Type | Required | Description |
|---|---|---|---|
| subdomain | string | required | New subdomain slug. |
Enables or disables maintenance mode. When enabled, visitors see a maintenance page unless they have a site token with canBypassMaintenance: true or are the site owner.
| Field | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | required | true to enable, false to disable. |
Site Access Tokens
Shareable tokens for granting access to password-protected or maintenance-mode sitesCreates a new site access token. Returns the raw token and a ready-to-share URL (appends ?token=… to the site URL). The raw token is returned only at creation time.
| Field | Type | Required | Description |
|---|---|---|---|
| expiresAt | ISO datetime | required | Token expiry. Must be in the future. Max 90 days from now. |
| label | string | optional | Human-readable label. Max 50 characters. |
| canBypassMaintenance | boolean | optional | Whether the token allows viewing the site while in maintenance mode. Default: true. |
Returns all tokens for the site. Raw token values are never returned — only metadata.
| Param | Type | Default | Description |
|---|---|---|---|
| status | string | all | active, revoked, expired, or all. |
Updates a non-revoked token's label or maintenance-bypass flag. At least one field must be provided.
| Field | Type | Description |
|---|---|---|
| label | string | null | New label (max 50 chars) or null to clear. |
| canBypassMaintenance | boolean | Update maintenance bypass permission. |
Marks the token as revoked. The token cache is invalidated immediately — the token stops working within milliseconds.
Revokes all non-expired, non-revoked tokens for the site. The full token cache for the site is invalidated immediately.
Visitor Sessions
Manage password-gated visitor sessions for protected sitesReturns visitor sessions for the site. These are created when a visitor successfully enters the site password. Note: revoked is not a valid status filter here (sessions are hard-deleted on revoke).
| Param | Type | Default | Description |
|---|---|---|---|
| status | string | all | active or expired or all. (revoked not supported.) |
Hard-deletes a visitor session. The cache is invalidated immediately — the visitor is kicked out within milliseconds.
Hard-deletes all non-expired visitor sessions. All visitors will need to re-enter the site password. Cache is invalidated immediately.
Analytics
Page view and visitor analytics for your sitesReturns comprehensive analytics for the site within a date range. All 15 analytics dimensions are returned in a single response. The from date is clamped to the user's analytics retention window (90 days by default for registered users). Soft-deleted analytics rows are excluded.
| Param | Type | Default | Description |
|---|---|---|---|
| from | ISO date | 7 days ago | Start of date range (inclusive). |
| to | ISO date | now | End of date range (exclusive). |
Forms
Build forms, collect submissions, and export responsescanUseForms feature. Public embed and submission endpoints require no authentication. All formId values are UUIDs. Default account allowances are 25 active forms, 5,000 submissions per calendar month (UTC), and 1,000 stored submissions per standalone form or linked site. Forms linked to the same site share its stored-submission allowance. Use GET /api/forms/usage for your effective limits.
Creates a standalone builder form. Send the definition properties at the top level of the request. The response nests them under form.definition, with title on the form itself.
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | required | Non-empty title, at most 255 characters. |
| description | string | optional | At most 1,000 characters. Default: empty string. |
| fields | array | required | 1–25 field objects, in display order. See field properties below. |
| settings | object | optional | acceptingResponses and allowResubmit are booleans, both defaulting to true. submitLabel: non-empty text up to 60 characters (default Send message). successMessage: non-empty text up to 1,000 characters (default Thank you! Your response has been received.). |
| styles | object | optional | background, text, accent, buttonText: six-digit hex colors. font: sans, serif, or mono. radius: integer 0–24. width: integer 280–900. Defaults: #ffffff, #202020, #25634e, #ffffff, sans, 10, 640, respectively. |
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Unique name, at most 64 characters, matching ^[a-z][a-z0-9_]*$. constructor and prototype are reserved. |
| label | string | required | Non-empty label, at most 150 characters. |
| type | string | required | text, email, tel, textarea, number, date, url, select, radio, or checkbox. |
| required | boolean | optional | Default: false. |
| placeholder | string | optional | At most 200 characters. Default: empty string. |
| options | string[] | for select/radio | 1–50 unique, non-empty options, at most 150 characters each. |
allowResubmit: false, include a required field with name: "email" and type: "email". Duplicate-email checks are case-insensitive.400 for an invalid definition; 429 when the active-form allowance is exhausted.
Lists your active builder and AI-generated forms, newest first. Each item includes the form properties shown in Create a form, plus responseCount, siteSubdomain, and siteTitle. AI-generated forms have definition: null; standalone forms have no linked site.
| Param | Type | Default | Description |
|---|---|---|---|
| siteId | UUID | — | Filter to forms linked to this site. |
| page | integer | 1 | Page number, clamped to 1–1,000,000. |
| pageSize | integer | 20 | Items per page, clamped to 1–100. |
Returns your active-form count, account-wide submissions this calendar month (UTC), and effective limits. Submissions retained for deleted forms still count toward monthly usage. Paid-account numeric overrides may change the example limits below.
Returns an active form you own, including its definition. AI-generated forms have definition: null. Returns 404 for a missing, deleted, or other user's form. The response does not include a response count; use the list endpoint for counts.
Accepts the same top-level JSON body as Create a form. Send title and the full fields array; omitted optional properties reset to their defaults. Array order sets field order. To pause submissions, send settings.acceptingResponses: false with the full definition. Existing responses are retained.
400; use PATCH to rename them. Missing, deleted, or other users' forms return 404.Updates only the title of a builder or AI-generated form you own. title must be a string; it is trimmed and truncated to 255 characters. An empty string clears the title to null. Other properties are ignored.
Soft-deletes an active form you own. It stops accepting responses and no longer counts toward the active-form limit. Stored responses remain, including their contribution to monthly usage. Returns 404 for a missing, already deleted, or other user's form.
Empty response body.
Returns responses newest first, with the form metadata. Returns 404 for a missing form and 403 if you do not own it.
| Param | Type | Default | Description |
|---|---|---|---|
| page | integer | 1 | Page number, clamped to 1–1,000,000. |
| pageSize | integer | 50 | Items per page, clamped to 1–100. |
| search | string | — | Case-insensitive search across email, name fields, phone, input text, and extra fields. |
Downloads all matching responses, newest first. Optional search uses the same filtering as List form responses; pagination parameters do not apply. Columns include Submitted At, Email, First Name, Last Name, Full Name, Phone, Input Text, sorted extra-field names, and IP Address. Spreadsheet formula prefixes are escaped. Returns 404 for a missing form or 403 for another user's form.
CSV response body; empty when no responses match.
Returns the rendered builder form with its configured fields, styles, and settings. It can be embedded on any website, including sites hosted outside Pagey Site. Requires an active builder form and the owner's canUseForms feature. Missing, deleted, or AI-generated forms return 404.
Content-Type: text/html; charset=utf-8, Cache-Control: no-store. The form submits to the public submission endpoint.
Accepts application/json, application/x-www-form-urlencoded, or field-only multipart/form-data. File uploads are rejected. Cross-origin requests are supported without credentials; preflight OPTIONS requests return 204. Send Accept: application/json to receive JSON for both success and validation errors.
Send a flat object with at most 30 fields. Builder form values must be strings keyed by the configured field names; required fields, field lengths, and types are validated, and undeclared fields are not stored. Send numbers as strings, dates as YYYY-MM-DD, URLs with http: or https:, and a checked checkbox as "yes" (unchecked: empty or omitted). Select/radio values must match a configured option. Keep the honeypot _pagey_website empty or omit it.
For AI-generated forms, string, number, and boolean values are accepted. The names email, first_name, last_name, full_name, phone, and input_text map to dedicated response properties; other ordinary names become extraFields. Fields beginning with _ are not stored. Dedicated field limits are 320 characters for email, 255 for each name, 50 for phone, and 2,000 for input text; custom values are limited to 2,000 characters.
Without Accept: application/json, builder submissions return the rendered form with a success message (201), or inline validation errors. AI-generated forms redirect with 303 to an absolute HTTP(S) URL supplied in _redirect or _next, or return an HTML thank-you page (201).
| Status | Meaning |
|---|---|
| 400 | Invalid or empty fields, validation failure, honeypot filled, oversized multipart data, or unsupported files. |
| 403 | Owner account unavailable or forms feature disabled. |
| 404 | Invalid form UUID or form not found. |
| 409 | Email already submitted when repeat submissions are disabled, or form ownership changed during submission. |
| 410 | Form deleted, inactive, or paused. |
| 429 | Account monthly quota, stored-submission allowance, or rate limit reached. The public submission limiter defaults to 30 requests per 15 minutes per IP and is configurable. |
Public Endpoints
No authentication required — anonymous site creation and visitor password accessCreates a site without a user account. Anonymous creation is limited to 10 new sites per UTC day per browser or network, with no total site cap and no custom subdomains. Registered accounts can create 25 new sites per UTC day. To update the site later, the site must be password-protected (password is used as the auth credential).
| Field | Type | Required | Description |
|---|---|---|---|
| file | file | required | index.html or a .zip archive. |
| title | string | optional | Site title. |
| password | string | optional | Site password. Required if you want to be able to update it later. |
| expiresAt | ISO datetime | optional | Expiry date. |
Returns basic public metadata for a site by subdomain. Does not include sensitive fields. Returns 404 for deleted/expired sites.
Replaces content for a password-protected anonymous site. The site's password is used as the authentication credential. Only works for sites that have a password set.
| Field | Type | Required | Description |
|---|---|---|---|
| file | file | required | New site file (index.html or .zip). |
| password | string | required | The site's current password. |
Verifies the site password and creates a visitor access session (24-hour cookie). Accepts both JSON and HTML form submissions (application/x-www-form-urlencoded). For form submissions, redirects to returnPath on success or back with an auth_error query param on failure. Visitor session limits are enforced against the site owner's tier.
| Field | Type | Required | Description |
|---|---|---|---|
| password | string | required | The site's password. |
| returnPath | string | optional | Path to redirect to after form-based unlock. Defaults to /. Must be a relative path. |
_sa_<siteId>, scoped to the site's subdomain.AI Builder
Conversational HTML generation powered by LLMReturns usage stats for the current calendar month. Use this to check remaining capacity before creating a session.
Creates a new AI Builder session. Enforces the monthly session limit (counting both active and deleted sessions created this month).
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | optional | Session title. Defaults to "Untitled". Max 255 chars. |
| Param | Type | Default | Description |
|---|---|---|---|
| status | string | active | active, deleted, or all. |
| page | number | 1 | Page number. |
| pageSize | number | 20 | Results per page. Max 50. |
Returns the session object plus all messages in chronological order. Message contentType is one of text (plain chat), html (generated HTML), or question (LLM clarifying question JSON).
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | required | New session title. Max 255 chars. Empty → "Untitled". |
Soft-deletes the session (sets status: deleted). The session still counts toward the monthly limit since LLM costs have already been incurred.
Sends a message to the LLM and gets back generated HTML, a clarifying question, or a text response. Message history is automatically maintained in the session. If the last assistant message was a question, the user's reply does not count toward the per-session prompt limit.
| Field | Type | Required | Description |
|---|---|---|---|
| message | string | required | User's message to the LLM. |
| model | string | optional | Model override. Defaults to configured model. |
Takes the session's lastGeneratedHtml and creates a new hosted site from it using the normal site creation pipeline. The site is linked back to the session via deployedSiteId. Requires lastGeneratedHtml to be non-null (i.e. at least one successful generate call).
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | optional | Site title. Defaults to session title. |
| customSubdomain | string | optional | Custom subdomain (free for registered users). |
| password | string | optional | Password-protect the deployed site. |