No endpoints match your search.

Authentication

The API supports two authentication mechanisms. Most endpoints accept either, but some (like creating API tokens) require session authentication only.

🍪 Session Cookie

Browser-based. Set automatically after POST /auth/login. The cookie pagey_session is HttpOnly and SameSite=Lax.

🔑 Bearer Token

For programmatic access. Pass your API token in the Authorization header: Bearer <token>

API tokens have no expiry — revoke them when no longer needed. An API token cannot create other API tokens; that action requires a session cookie.

Errors & Rate Limits

StatusMeaning
400Bad request — invalid input, missing required fields
401Unauthenticated — missing or invalid credentials
403Forbidden — authenticated but not authorized, or feature not on your tier
404Not found — resource doesn't exist or was deleted
409Conflict — e.g. subdomain already in use, email already registered
410Gone — site has expired or form no longer accepts responses
429Rate limited or tier quota exceeded
502Upstream error — AI provider returned an error
503Service unavailable — subdomain exhaustion, DB unreachable
Rate limits use a sliding window and are returned in response headers: 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 passwords
POST /api/auth/signup Register a new account ▾
No auth required Auth rate limit

Creates 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.

Request body (JSON)
FieldTypeRequiredDescription
emailstringrequiredValid email address. Normalized to lowercase.
passwordstringrequired8–200 characters.
201 Created
Response{ "message": "Registration successful. Please verify your email.", "user": { "userId": "uuid", "email": "user@example.com" } }
409 Conflict
{ "error": "Email already in use" }
POST /api/auth/login Log in and get a session cookie ▾
No auth required Auth rate limit

Authenticates the user and sets an HttpOnly session cookie. Sessions expire after 90 days (configurable). Email must be verified.

Request body (JSON)
FieldTypeRequiredDescription
emailstringrequiredUser's email address.
passwordstringrequiredUser's password.
200 OK
Response — also sets Set-Cookie: pagey_session=…{ "message": "Login successful", "user": { "id": "uuid", "email": "user@example.com", "tier": "free", "isEmailVerified": true } }
401 Unauthorized
{ "error": "Invalid credentials" }
403 Forbidden
{ "error": "Email is not verified" }
POST /api/auth/logout Log out the current session ▾
Auth optional

Invalidates the current session (deletes the DB row) and clears the session cookie. Redirects to / with a 302. Safe to call even when unauthenticated.

302 Redirect

Redirects to /. Cookie is cleared.

GET /api/auth/me Get current user profile ▾
Auth required

Returns the authenticated user's profile. Works with both session cookie and Bearer token.

200 OK
{ "user": { "id": "uuid", "email": "user@example.com", "isEmailVerified": true, "tier": "free", "createdAt": "2024-01-01T00:00:00Z", "updatedAt": "2024-01-01T00:00:00Z" } }
GET /api/auth/verify-email Verify email address ▾
No auth required Auth rate limit

Verifies the user's email using the token from the verification email. Token is valid for 24 hours. On success, redirects to /login.

Query parameters
ParamTypeRequiredDescription
tokenstringrequiredRaw verification token from the email link.
302 Redirect

Redirects to /login on success.

400 Bad Request
{ "error": "Invalid or expired verification token" }
POST /api/auth/forgot-password Request a password reset email ▾
No auth required Auth rate limit

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).

Request body (JSON)
FieldTypeRequiredDescription
emailstringrequiredThe email address to send the reset link to.
200 OK
{ "message": "If the email exists, a password reset link has been sent" }
POST /api/auth/reset-password Complete a password reset ▾
No auth required Auth rate limit

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.

Request body (JSON)
FieldTypeRequiredDescription
tokenstringrequiredRaw reset token from the email link.
newPasswordstringrequiredNew password. 8–200 characters.
200 OK
{ "message": "Password reset successful" }
400 Bad Request
{ "error": "Invalid or expired reset token" }

User Sessions

Manage login sessions across devices
GET /api/auth/sessions List active sessions ▾
Auth required

Returns all non-expired sessions for the authenticated user. The current session is identified with isCurrent: true. Token hashes are never returned.

200 OK
{ "sessions": [{ "id": "uuid", "isCurrent": true, "createdAt": "2024-01-01T00:00:00Z", "lastSeenAt": "2024-01-02T10:00:00Z", "expiresAt": "2024-04-01T00:00:00Z" }] }
DELETE /api/auth/sessions/:sessionId Revoke a specific session ▾
Auth required

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.

Path parameters
ParamTypeDescription
sessionIdUUIDThe session ID to revoke.
200 OK
{ "message": "Session revoked" }
POST /api/auth/sessions/revoke-others Revoke all other sessions ▾
Auth required

Revokes all sessions except the current one ("log out everywhere else"). The current session remains active.

200 OK
{ "message": "All other sessions revoked", "revokedCount": 3 }
POST /api/auth/sessions/revoke-all Revoke all sessions including current ▾
Auth required

Revokes ALL sessions for the user, including the current one. The session cookie is cleared. The user must log in again.

200 OK
{ "message": "All sessions revoked", "revokedCount": 4 }

API Tokens

Long-lived tokens for programmatic API access
API tokens are account-scoped and grant full API access on behalf of the user. The raw token is shown only once at creation time. Token hashes (stored in the DB) are never returned. Revoked tokens are soft-deleted and retained for audit; they are physically purged after 365 days.
POST /api/auth/api-tokens Create a new API token ▾
Auth required Session cookie only

Creates 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.

Request body (JSON)
FieldTypeRequiredDescription
labelstringoptionalHuman-readable label. Max 100 characters. Useful for identifying the token's purpose.
201 Created
{ "token": "a1b2c3d4...", "message": "Store this token securely — it will not be shown again" }
403 Forbidden
{ "error": "API tokens cannot create other API tokens" }
The raw token is shown exactly once. Store it immediately in a secure location (secrets manager, env variable). It cannot be retrieved later.
GET /api/auth/api-tokens List all API tokens ▾
Auth required

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.

200 OK
{ "tokens": [{ "id": "uuid", "label": "CI pipeline", "last_used_at": "2024-03-01T12:00:00Z", "created_at": "2024-01-15T09:00:00Z", "revoked_at": null // null = active; ISO timestamp = revoked }] }
last_used_at is updated lazily with a 5-minute debounce to avoid write amplification on every request.
DELETE /api/auth/api-tokens/:tokenId Revoke an API token ▾
Auth required

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.

Path parameters
ParamTypeDescription
tokenIdUUIDThe API token's ID (from the list response).
200 OK
{ "message": "Token revoked" }
404 Not Found
{ "error": "Token not found or already revoked" }

Sites

Create, manage, and configure hosted sites
POST /api/sites Create a site ▾
Auth required Upload rate limit 25 new sites/day · No total cap

Creates 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).

Request body (multipart/form-data)
FieldTypeRequiredDescription
filefilerequiredindex.html or a .zip archive containing index.html at root.
titlestringoptionalSite title. Max 255 characters.
passwordstringoptionalPassword-protect the site. Min 6 characters.
expiresAtISO datetimeoptionalWhen the site expires. Must be in the future.
customSubdomainstringoptionalCustom subdomain slug. Free for registered users. Lowercase letters, numbers, hyphens. Max 63 chars.
201 Created
{ "site": { "id": "uuid", "subdomain": "blue-oak-river", "title": "My Site", "status": "active", "isPasswordProtected": false, "maintenanceMode": false, "expiresAt": null, "tags": [], "createdAt": "2024-01-01T00:00:00Z" } }
GET /api/sites List sites ▾
Auth required

Returns a paginated list of sites owned by the authenticated user.

Query parameters
ParamTypeDefaultDescription
pagenumber1Page number.
pageSizenumber20Results per page. Max 100.
statusstring—Filter by status: active, deleted, expired.
searchstring—Filter by subdomain or title (case-insensitive substring).
200 OK
{ "page": 1, "pageSize": 20, "total": 5, "items": [{ /* site objects */ }] }
GET /api/sites/:siteId Get a single site ▾
Auth required

Returns a single site by ID. Returns 404 if not found, 410 if expired. Only the owner can retrieve it.

200 OK
{ "site": { /* full site object */ } }
PATCH /api/sites/:siteId Update site metadata ▾
Auth requiredTier: canUpdateSite

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.

Request body (JSON) — all fields optional
FieldTypeDescription
titlestring | nullSite title. Null clears it.
expiresAtISO datetime | nullNew expiry. Must be in the future. Null removes expiry and resets status to active.
passwordstring | null | ""New site password (min 6 chars). null or empty string removes password protection.
countryRestrictionobjectComplete 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.

200 OK
{ "site": { /* updated site object */ } }
DELETE /api/sites/:siteId Soft delete a site ▾
Auth requiredTier: canDeleteSite

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.

200 OK
{ "message": "Site deleted" }
PUT /api/sites/:siteId/content Replace site content ▾
Auth requiredUpload rate limitTier: canUpdateSite

Replaces the site's content with a new upload. The old active folder is asynchronously deleted after the transaction commits.

Request body (multipart/form-data)
FieldTypeRequiredDescription
filefilerequiredindex.html or a .zip archive.
200 OK
{ "site": { /* updated site object */ } }
GET /api/sites/:siteId/files Browse the site file tree ▾
Auth required

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.

POST /api/sites/:siteId/files/upload Upload or replace files ▾
Auth requiredUpload + mutation limitsTier: canUpdateSite

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.

POST /api/sites/:siteId/directories Create a directory ▾
Auth requiredTier: canUpdateSite

JSON body: { "path": "assets/images" }. The immediate parent must already exist.

PATCH /api/sites/:siteId/files/move Rename or move a path ▾
Auth requiredTier: canUpdateSite

JSON body: { "sourcePath": "assets/old.png", "destinationPath": "images/new.png" }. The destination parent must exist and the destination must not.

DELETE /api/sites/:siteId/files Delete files or directories ▾
Auth requiredTier: canUpdateSite

JSON body: { "paths": ["unused.css", "images/old"] }. Accepts up to 100 paths. Root index.html cannot be deleted, moved, or renamed.

GET /api/sites/:siteId/download Download site as ZIP ▾
Auth requiredTier: canDownloadSite

Returns the site's current active folder as a ZIP archive. Response has Content-Type: application/zip and Content-Disposition: attachment; filename="subdomain.zip".

200 OK

Binary ZIP stream.

PUT /api/sites/:siteId/tags Set site tags ▾
Auth requiredTier: canSetTags (Paid)

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.

Request body (JSON)
FieldTypeRequiredDescription
tagsstring[]requiredArray of tag strings. Pass [] to remove all tags.
200 OK
{ "site": { /* site object with updated tags */ } }
PATCH /api/sites/:siteId/subdomain Set custom subdomain ▾
Auth requiredTier: canSetCustomSubdomain (Paid)

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.

Request body (JSON)
FieldTypeRequiredDescription
subdomainstringrequiredNew subdomain slug.
200 OK
{ "site": { /* site object with new subdomain */ } }
409 Conflict
{ "error": "Subdomain already in use" }
PATCH /api/sites/:siteId/maintenance Toggle maintenance mode ▾
Auth requiredTier: canSetMaintenance (Paid)

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.

Request body (JSON)
FieldTypeRequiredDescription
enabledbooleanrequiredtrue to enable, false to disable.
200 OK
{ "site": { /* site object */ } }

Site Access Tokens

Shareable tokens for granting access to password-protected or maintenance-mode sites
Site tokens are per-site (not account-scoped). They grant visitors access to password-protected sites and optionally bypass maintenance mode. Free for all registered users. Max 10 active tokens per site. Max validity: 90 days.
POST /api/sites/:siteId/tokens Create a site access token ▾
Auth requiredTier: canManageTokens (Paid)

Creates 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.

Request body (JSON)
FieldTypeRequiredDescription
expiresAtISO datetimerequiredToken expiry. Must be in the future. Max 90 days from now.
labelstringoptionalHuman-readable label. Max 50 characters.
canBypassMaintenancebooleanoptionalWhether the token allows viewing the site while in maintenance mode. Default: true.
201 Created
{ "id": "uuid", "label": "QA team", "canBypassMaintenance": true, "revoked": false, "expiresAt": "2024-04-01T00:00:00Z", "token": "a1b2c3...", // raw token — shown once only "url": "https://blue-oak.pagey.site/?token=a1b2c3..." }
GET /api/sites/:siteId/tokens List site tokens ▾
Auth requiredTier: canManageTokens (Paid)

Returns all tokens for the site. Raw token values are never returned — only metadata.

Query parameters
ParamTypeDefaultDescription
statusstringallactive, revoked, expired, or all.
200 OK
{ "tokens": [{ /* token objects without raw token */ }] }
PATCH /api/sites/:siteId/tokens/:tokenId Update a site token ▾
Auth requiredTier: canManageTokens (Paid)

Updates a non-revoked token's label or maintenance-bypass flag. At least one field must be provided.

Request body (JSON) — at least one required
FieldTypeDescription
labelstring | nullNew label (max 50 chars) or null to clear.
canBypassMaintenancebooleanUpdate maintenance bypass permission.
200 OK
{ "token": { /* updated token metadata */ } }
DELETE /api/sites/:siteId/tokens/:tokenId Revoke a site token ▾
Auth requiredTier: canManageTokens (Paid)

Marks the token as revoked. The token cache is invalidated immediately — the token stops working within milliseconds.

200 OK
{ "message": "Token revoked" }
POST /api/sites/:siteId/tokens/revoke-all Revoke all active tokens ▾
Auth requiredTier: canManageTokens (Paid)

Revokes all non-expired, non-revoked tokens for the site. The full token cache for the site is invalidated immediately.

200 OK
{ "message": "All active tokens revoked", "revokedCount": 3 }

Visitor Sessions

Manage password-gated visitor sessions for protected sites
GET /api/sites/:siteId/access-sessions List visitor sessions ▾
Auth requiredTier: canManageAccessSessions

Returns 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).

Query parameters
ParamTypeDefaultDescription
statusstringallactive or expired or all. (revoked not supported.)
200 OK
{ "sessions": [{ "id": "uuid", "expires_at": "2024-01-02T10:00:00Z", "created_at": "2024-01-01T10:00:00Z" }] }
DELETE /api/sites/:siteId/access-sessions/:sessionId Revoke a visitor session ▾
Auth requiredTier: canManageAccessSessions

Hard-deletes a visitor session. The cache is invalidated immediately — the visitor is kicked out within milliseconds.

200 OK
{ "message": "Access session revoked" }
POST /api/sites/:siteId/access-sessions/revoke-all Revoke all visitor sessions ▾
Auth requiredTier: canManageAccessSessions

Hard-deletes all non-expired visitor sessions. All visitors will need to re-enter the site password. Cache is invalidated immediately.

200 OK
{ "message": "All active access sessions revoked", "revokedCount": 12 }

Analytics

Page view and visitor analytics for your sites
GET /api/sites/:siteId/analytics Get site analytics ▾
Auth requiredAnalytics: free with an account, 90-day retention

Returns 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.

Query parameters
ParamTypeDefaultDescription
fromISO date7 days agoStart of date range (inclusive).
toISO datenowEnd of date range (exclusive).
200 OK
{ "analytics": { "from": "2024-03-01T00:00:00Z", "to": "2024-03-07T00:00:00Z", "pageViews": 1420, "uniqueVisitors": 384, "sessions": 421, "daily": [{ "day": "2024-03-01T00:00:00Z", "pageViews": 230 }], "referrers": [{ "referrer": "https://news.ycombinator.com", "count": 85 }], "countries": [{ "countryCode": "US", "count": 310 }], "topPages": [{ "path": "/", "pageViews": 900, "uniqueVisitors": 300 }], "entryPages": [{ "path": "/", "entries": 390 }], "exitPages": [{ "path": "/", "exits": 370 }], "devices": [{ "deviceType": "desktop", "count": 250 }], "browsers": [{ "browser": "Chrome", "count": 200 }], "operatingSystems": [{ "os": "macOS", "count": 180 }], "trafficSources": [{ "source": "direct", "count": 150 }], "trafficSourceDetails": [{ "source": "search", "detail": "Google", "count": 90 }], "utmSources": [], "utmMediums": [], "utmCampaigns": [] } }

Forms

Build forms, collect submissions, and export responses
Management endpoints require a session cookie or account API token and the canUseForms 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.
POST /api/forms Create a form ▾
Auth requiredTier: canUseForms

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.

Request body (JSON)
FieldTypeRequiredDescription
titlestringrequiredNon-empty title, at most 255 characters.
descriptionstringoptionalAt most 1,000 characters. Default: empty string.
fieldsarrayrequired1–25 field objects, in display order. See field properties below.
settingsobjectoptionalacceptingResponses 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.).
stylesobjectoptionalbackground, 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 object properties
FieldTypeRequiredDescription
namestringrequiredUnique name, at most 64 characters, matching ^[a-z][a-z0-9_]*$. constructor and prototype are reserved.
labelstringrequiredNon-empty label, at most 150 characters.
typestringrequiredtext, email, tel, textarea, number, date, url, select, radio, or checkbox.
requiredbooleanoptionalDefault: false.
placeholderstringoptionalAt most 200 characters. Default: empty string.
optionsstring[]for select/radio1–50 unique, non-empty options, at most 150 characters each.
To set allowResubmit: false, include a required field with name: "email" and type: "email". Duplicate-email checks are case-insensitive.
Example request
{ "title": "Contact us", "fields": [ { "name": "email", "label": "Email", "type": "email", "required": true } ] }
201 Created
{ "form": { "id": "7d481dc0-5814-4419-a9ee-4769071e75ea", "ownerUserId": "6c1875b6-b7a2-4863-9206-75af5240c235", "siteId": null, "aiSessionId": null, "title": "Contact us", "status": "active", "definition": { "description": "", "fields": [{ "name": "email", "label": "Email", "type": "email", "required": true, "placeholder": "" }], "settings": { "allowResubmit": true, "acceptingResponses": true, "submitLabel": "Send message", "successMessage": "Thank you! Your response has been received." }, "styles": { "background": "#ffffff", "text": "#202020", "accent": "#25634e", "buttonText": "#ffffff", "radius": 10, "font": "sans", "width": 640 } }, "createdAt": "2026-10-28T10:00:00.000Z", "updatedAt": "2026-10-28T10:00:00.000Z" } }

400 for an invalid definition; 429 when the active-form allowance is exhausted.

GET /api/forms List your forms ▾
Auth requiredTier: canUseForms

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.

Query parameters
ParamTypeDefaultDescription
siteIdUUID—Filter to forms linked to this site.
pageinteger1Page number, clamped to 1–1,000,000.
pageSizeinteger20Items per page, clamped to 1–100.
200 OK
{ "page": 1, "pageSize": 20, "total": 1, "items": [{ /* form properties, plus: */ "responseCount": 12, "siteSubdomain": null, "siteTitle": null }] }
GET /api/forms/usage Get form usage and limits ▾
Auth requiredTier: canUseForms

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.

200 OK
{ "forms": 2, "submissionsThisMonth": 12, "limits": { "maxForms": 25, "maxFormSubmissionsPerMonth": 5000, "maxFormSubmissionsPerSite": 1000 } }
GET /api/forms/:formId Get a form ▾
Auth requiredTier: canUseForms

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.

200 OK
{ "form": { /* form object as shown in Create a form */ } }
PUT /api/forms/:formId Replace a builder form definition ▾
Auth requiredTier: canUseForms

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.

This endpoint only updates builder forms. AI-generated forms return 400; use PATCH to rename them. Missing, deleted, or other users' forms return 404.
200 OK
{ "form": { /* updated form object */ } }
PATCH /api/forms/:formId Rename a form ▾
Auth requiredTier: canUseForms

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.

Request body (JSON)
{ "title": "Customer enquiries" }
200 OK
{ "form": { /* updated form object */ } }
DELETE /api/forms/:formId Delete a form ▾
Auth requiredTier: canUseForms

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.

204 No Content

Empty response body.

GET /api/forms/:formId/responses List form responses ▾
Auth requiredTier: canUseForms

Returns responses newest first, with the form metadata. Returns 404 for a missing form and 403 if you do not own it.

Query parameters
ParamTypeDefaultDescription
pageinteger1Page number, clamped to 1–1,000,000.
pageSizeinteger50Items per page, clamped to 1–100.
searchstring—Case-insensitive search across email, name fields, phone, input text, and extra fields.
200 OK
{ "form": { /* form object */ }, "page": 1, "pageSize": 50, "total": 1, "items": [{ "id": "79a9a02f-d1f5-4ffc-bf98-ab8ad27f1523", "formId": "7d481dc0-5814-4419-a9ee-4769071e75ea", "email": "visitor@example.com", "firstName": null, "lastName": null, "fullName": null, "phone": null, "inputText": null, "extraFields": {}, "ipAddress": "203.0.113.10", "submittedAt": "2026-10-28T10:05:00.000Z" }] }
GET /api/forms/:formId/responses/csv Export responses as CSV ▾
Auth requiredTier: canUseForms

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.

200 OK
Content-Type: text/csv; charset=utf-8 Content-Disposition: attachment; filename="form-submissions-7d481dc0-5814-4419-a9ee-4769071e75ea.csv"

CSV response body; empty when no responses match.

GET /api/public/forms/:formId/embed Embed a builder form ▾
No auth required

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.

Example embed
<iframe src="https://pagey.site/api/public/forms/7d481dc0-5814-4419-a9ee-4769071e75ea/embed" title="Contact us" width="100%" height="600"> </iframe>
200 OK

Content-Type: text/html; charset=utf-8, Cache-Control: no-store. The form submits to the public submission endpoint.

POST /api/public/forms/:formId/submit Submit a form response ▾
No auth requiredForm submission rate limit

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.

Request fields

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.

Example request
curl -X POST "https://pagey.site/api/public/forms/7d481dc0-5814-4419-a9ee-4769071e75ea/submit" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "email": "visitor@example.com" }'
201 Created (JSON)
{ "message": "Form submitted successfully", "id": "79a9a02f-d1f5-4ffc-bf98-ab8ad27f1523" }

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).

StatusMeaning
400Invalid or empty fields, validation failure, honeypot filled, oversized multipart data, or unsupported files.
403Owner account unavailable or forms feature disabled.
404Invalid form UUID or form not found.
409Email already submitted when repeat submissions are disabled, or form ownership changed during submission.
410Form deleted, inactive, or paused.
429Account 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 access
POST /api/public/sites Create an anonymous site ▾
No auth requiredUpload rate limit

Creates 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).

Request body (multipart/form-data)
FieldTypeRequiredDescription
filefilerequiredindex.html or a .zip archive.
titlestringoptionalSite title.
passwordstringoptionalSite password. Required if you want to be able to update it later.
expiresAtISO datetimeoptionalExpiry date.
201 Created
{ "site": { /* site object */ } }
GET /api/public/sites/:subdomain Get public site info ▾
No auth required

Returns basic public metadata for a site by subdomain. Does not include sensitive fields. Returns 404 for deleted/expired sites.

200 OK
{ "site": { "id": "uuid", "subdomain": "blue-oak", "title": "My Site", "status": "active", "isPasswordProtected": true, "maintenanceMode": false, "expiresAt": null, "tags": ["portfolio"] } }
PUT /api/public/sites/:subdomain/content Update anonymous site content ▾
No account neededUpload + Mutation rate limit

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.

Request body (multipart/form-data)
FieldTypeRequiredDescription
filefilerequiredNew site file (index.html or .zip).
passwordstringrequiredThe site's current password.
200 OK
{ "site": { /* updated site object */ } }
POST /api/public/sites/:subdomain/access Unlock a password-protected site ▾
No auth requiredMutation rate limit

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.

Request body (JSON or form-encoded)
FieldTypeRequiredDescription
passwordstringrequiredThe site's password.
returnPathstringoptionalPath to redirect to after form-based unlock. Defaults to /. Must be a relative path.
200 OK (JSON) / 302 Redirect (form)
{ "message": "Access granted", "expiresAt": "2024-01-02T10:00:00Z" }
The visitor session is stored as an HttpOnly cookie named _sa_<siteId>, scoped to the site's subdomain.

AI Builder

Conversational HTML generation powered by LLM
AI Builder is session-based. Each session holds a multi-turn conversation with the LLM for generating a single-page website. Monthly session limits and per-session prompt limits are enforced per tier. Sessions count toward the monthly limit even if deleted (since LLM costs have been incurred). Rate limit: 20 generation requests per 15-minute window per user.
GET /api/ai/usage Get AI Builder usage ▾
Auth required

Returns usage stats for the current calendar month. Use this to check remaining capacity before creating a session.

200 OK
{ "usage": { "available": true, "sessionsThisMonth": 2, "maxSessionsPerMonth": 5, "maxPromptsPerSession": 5, "remainingSessions": 3 } }
POST /api/ai/sessions Create an AI Builder session ▾
Auth requiredTier: canUseAiBuilder + monthly limit

Creates a new AI Builder session. Enforces the monthly session limit (counting both active and deleted sessions created this month).

Request body (JSON)
FieldTypeRequiredDescription
titlestringoptionalSession title. Defaults to "Untitled". Max 255 chars.
201 Created
{ "session": { "id": "uuid", "title": "My Portfolio", "status": "active", "userPromptCount": 0, "hasGeneratedHtml": false, "deployedSiteId": null, "createdAt": "2024-03-01T10:00:00Z" } }
GET /api/ai/sessions List AI Builder sessions ▾
Auth required
Query parameters
ParamTypeDefaultDescription
statusstringactiveactive, deleted, or all.
pagenumber1Page number.
pageSizenumber20Results per page. Max 50.
200 OK
{ "page": 1, "pageSize": 20, "total": 3, "items": [{ /* session objects */ }] }
GET /api/ai/sessions/:sessionId Get session with message history ▾
Auth required

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).

200 OK
{ "session": { /* session object */ }, "messages": [{ "id": "uuid", "role": "user", "content": "Build me a portfolio site", "contentType": "text", "createdAt": "2024-03-01T10:01:00Z" }] }
PATCH /api/ai/sessions/:sessionId Update session title ▾
Auth required
Request body (JSON)
FieldTypeRequiredDescription
titlestringrequiredNew session title. Max 255 chars. Empty → "Untitled".
200 OK
{ "session": { /* updated session */ } }
DELETE /api/ai/sessions/:sessionId Soft-delete a session ▾
Auth required

Soft-deletes the session (sets status: deleted). The session still counts toward the monthly limit since LLM costs have already been incurred.

200 OK
{ "message": "Session deleted" }
POST /api/ai/sessions/:sessionId/generate Generate or refine HTML ▾
Auth requiredAI rate limit: 20/15min per userTier: canUseAiBuilder + per-session prompt limit

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.

Request body (JSON)
FieldTypeRequiredDescription
messagestringrequiredUser's message to the LLM.
modelstringoptionalModel override. Defaults to configured model.
200 OK — HTML response
{ "type": "html", "html": "<!DOCTYPE html>…", "message": "Built a dark portfolio with hero section.", "model": "claude-sonnet-4-20250514", "usage": { "inputTokens": 1240, "outputTokens": 3500 } }
200 OK — Question response
{ "type": "question", "question": "What's the primary purpose of the site?", "options": ["Portfolio", "Blog", "Landing page"], "model": "claude-sonnet-4-20250514", "usage": { "inputTokens": 500, "outputTokens": 80 } }
429 Too Many Requests
{ "error": "Too many AI generation requests. Please try again shortly." }
POST /api/ai/sessions/:sessionId/deploy Deploy generated HTML as a site ▾
Auth required25 new sites/day · No total cap

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).

Request body (JSON)
FieldTypeRequiredDescription
titlestringoptionalSite title. Defaults to session title.
customSubdomainstringoptionalCustom subdomain (free for registered users).
passwordstringoptionalPassword-protect the deployed site.
201 Created
{ "site": { /* newly created site object */ } }
400 Bad Request
{ "error": "No generated HTML to deploy. Generate a site first." }