# Hosted page and template API reference

Manage hosted-page content, publication, SEO settings, and media, and read the public template catalog.

Available on the Scale and Pro plans.

These endpoints manage hosted pages and their supporting assets. They are dashboard API routes under `/api`, not public REST v1 resources. Page publication is separate from publishing the project's snippet configuration.

## Authentication, paths, and rate limits

Project routes use `https://api.rightmessage.com/api/{teamPid}`. They require an authenticated session or API token, project membership or administrator access, and account and project suspension checks. Browser-session writes require CSRF protection. Bearer tokens are accepted, but these routes do not enforce v1 `read`, `write`, or `publish` abilities.

Template catalog routes use `https://api.rightmessage.com/api/template-catalog` and do not require authentication. Neither the page/media route group nor the catalog declares a numeric per-route throttle. Do not apply v1's 120-per-minute declaration to them. Global infrastructure limits can still apply.

Send `Accept: application/json` and JSON bodies with `Content-Type: application/json`. The illustrative requests below use bearer placeholders for clarity and were not executed.

## Page operations

A `pid` is the encoded numeric-string page identifier returned by the API, not its slug or database ID. Item routes require digits. The project scopes every lookup.

| Method and path               | Body or query                                         | Success                           |
| ----------------------------- | ----------------------------------------------------- | --------------------------------- |
| GET `/pages`                  | Optional `status`, `type`, and `search` query strings | 200, bare array of page resources |
| POST `/pages`                 | Create fields below                                   | 201, bare page resource           |
| GET `/pages/{pid}`            | None                                                  | 200, bare page resource           |
| PUT `/pages/{pid}`            | Update fields below                                   | 200, bare page resource           |
| DELETE `/pages/{pid}`         | None                                                  | 204, empty body                   |
| POST `/pages/{pid}/publish`   | None                                                  | 200, bare page resource           |
| POST `/pages/{pid}/unpublish` | None                                                  | 200, bare page resource           |
| POST `/pages/{pid}/duplicate` | Optional `name`                                       | 201, bare new page resource       |
| POST `/pages/{pid}/archive`   | None                                                  | 200, bare page resource           |

Page resources are not wrapped in `data`. This differs from public v1 resource serializers. There is no PATCH page route.

The list orders newest first and has no pagination. Status and type are exact filters without request validation. Search uses SQL LIKE matching on name or slug. Duplicate accepts an unvalidated `name`, defaulting to the original name plus ` (Copy)`. It copies content, settings, flow, and metadata, and creates a draft with a unique slug. A slug collision on duplication falls back to a random slug.

## Page create and update fields

| Field      | Create                                               | Update                                    |
| ---------- | ---------------------------------------------------- | ----------------------------------------- |
| `name`     | Required string, maximum 255                         | Optional string, maximum 255              |
| `type`     | Required: `lead_gen` or `quiz_funnel`                | Optional, same values                     |
| `slug`     | Nullable string, maximum 255; generated when empty   | Optional non-null string, maximum 255     |
| `status`   | Nullable: draft, published, archived; defaults draft | Optional non-null value from the same set |
| `content`  | Nullable object                                      | Optional nullable object                  |
| `settings` | Nullable object                                      | Optional nullable object                  |

Slugs must match `^[a-z0-9]+(?:-[a-z0-9]+)*$` and be unique within the project. Duplicate slugs return 422. A new page also creates the project's hosted-page slug if one is missing.

Both create and update declare these nullable `content` fields:

| Keys                                  | Type and limit                                          |
| ------------------------------------- | ------------------------------------------------------- |
| `flowId`                              | String, maximum 255. No existence check in the request. |
| `template`                            | centered or split                                       |
| `logo`, `headline`, `authorSignature` | Strings, maximum 500                                    |
| `description`                         | String, maximum 2,000                                   |
| `backgroundColor`, `accentColor`      | Strings, maximum 50                                     |

Update additionally declares these nullable content fields:

| Keys                                                                                                                                                              | Type and limit         |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| `containerBackgroundColor`, `borderColor`, `textColor`                                                                                                            | Strings, maximum 50    |
| `borderRadius`, `borderWidth`, `buttonBorderRadius`, `answerButtonSpacing`, `formFieldSpacing`                                                                    | Strings, maximum 20    |
| `flowHeadlineFontSize`, `flowDescriptionFontSize`, `answerButtonFontSize`, `formFieldFontSize`, `submitButtonFontSize`, `headlineFontSize`, `descriptionFontSize` | Strings, maximum 20    |
| `containerShadow`                                                                                                                                                 | Boolean                |
| `font`                                                                                                                                                            | String, maximum 100    |
| `lineHeight`                                                                                                                                                      | Number, 1 through 3    |
| `customCss`                                                                                                                                                       | String, maximum 10,000 |

The parent objects are not closed key allowlists. The tables describe fields with explicit validation, not a guarantee that unknown nested keys are rejected. Supplied `content` and `settings` replace their stored objects rather than merging nested values.

## SEO settings available through the API

Both requests accept nullable `settings.metaTitle`, string up to 255; `settings.metaDescription`, string up to 1,000; and `settings.ogImage` and `settings.favicon`, strings up to 500. These fields are API-accessible even when a dashboard editor does not expose a control.

Use those nested settings for SEO. The response also includes legacy top-level `meta_title`, `meta_description`, `og_image`, `favicon`, and `flow_id` columns. The request validators do not declare those top-level write fields. The page renderer uses `content.flowId` for the flow, and the page's meta-title helper uses `settings.metaTitle` with the page name as fallback.

## Page response and immediate effects

Each page resource contains `pid`, `slug`, `name`, `type`, `status`, `content`, `settings`, the legacy columns above, `views_count`, `conversions_count`, percentage `conversion_rate`, `full_url`, nullable `published_at`, nullable `last_published_at`, `created_at`, `updated_at`, and booleans `is_published`, `is_draft`, `is_archived`, `is_lead_gen`, and `is_quiz_funnel`.

Item and lifecycle responses also include `team` with `pid`, `name`, `slug`, and `pages_base_url`. Other responses include it when project details are loaded. The URL uses the project slug, configured pages domain, and page slug. Conversion rate is conversions divided by views times 100, rounded to two decimals, or zero with no views.

Publish changes status to published, initializes `published_at` if absent, and updates `last_published_at`. Unpublish changes status to draft; archive changes it to archived. Both retain publication timestamps. Only published, non-deleted pages on nonsuspended projects render publicly.

There is no second page-content draft hidden behind a published page. Updating a published page changes its stored live content directly. Supplying `status: "published"` on create or update changes visibility without calling the publish method, so it does not have that method's timestamp behavior. The embedded flow is extracted from stable project configuration, not the current flow draft.

Page publishing does not validate that `content.flowId` exists or has usable dependencies. Hosted pages include questions referenced through segment-group IDs. Direct `raq_` question references, including input questions with no group, are not included in that question list. A successful page publish is not proof that such a flow will render correctly. Check the public page before sharing it.

## Illustrative page requests and shared response

These operations use the fictional WorkForge project. The same page resource contract covers list, create, read, update, publish, unpublish, duplicate, and archive. Status fields reflect the requested lifecycle operation. The duplicate response has a new pid and draft status.

```http
GET /api/123456/pages?status=draft&type=lead_gen&search=WorkForge HTTP/1.1
Host: api.rightmessage.com
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json

POST /api/123456/pages HTTP/1.1
Host: api.rightmessage.com
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json
Content-Type: application/json

{"name":"WorkForge demo","type":"lead_gen","slug":"workforge-demo","content":{"template":"centered","headline":"See WorkForge","logo":"https://example.com/logo.png"}}

GET /api/123456/pages/987654 HTTP/1.1
Host: api.rightmessage.com
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json

PUT /api/123456/pages/987654 HTTP/1.1
Host: api.rightmessage.com
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json
Content-Type: application/json

{"settings":{"metaTitle":"WorkForge demo","metaDescription":"See how WorkForge helps your team."}}

POST /api/123456/pages/987654/publish HTTP/1.1
Host: api.rightmessage.com
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json

POST /api/123456/pages/987654/unpublish HTTP/1.1
Host: api.rightmessage.com
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json

POST /api/123456/pages/987654/duplicate HTTP/1.1
Host: api.rightmessage.com
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json
Content-Type: application/json

{"name":"WorkForge demo copy"}

POST /api/123456/pages/987654/archive HTTP/1.1
Host: api.rightmessage.com
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json

DELETE /api/123456/pages/987654 HTTP/1.1
Host: api.rightmessage.com
Authorization: Bearer YOUR_API_TOKEN
Accept: application/json
```

Illustrative page-resource excerpt. The list returns an array of these objects; DELETE returns no body. The field list above defines the full resource.

```json
{"pid":"987654","slug":"workforge-demo","name":"WorkForge demo","type":"lead_gen","status":"draft","content":{"template":"centered","headline":"See WorkForge","logo":"https://example.com/logo.png"},"settings":{"metaTitle":"WorkForge demo"},"views_count":0,"conversions_count":0,"conversion_rate":0,"published_at":null,"last_published_at":null,"is_published":false,"is_draft":true,"is_archived":false}
```

## Project media operations

These routes share the page authentication boundary and affect storage immediately, without publishing.

| Method and path        | Request                                                                         | Success                                            |
| ---------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------- |
| GET `/files`           | No parameters                                                                   | 200, bare array of filenames                       |
| POST `/files`          | Multipart `file`, required file, maximum 2,048 KiB; jpeg, jpg, png, gif, or svg | 200, filename string, not a JSON resource envelope |
| DELETE `/files/{hash}` | Filename from storage, no body                                                  | 200, `{"message":"File deleted successfully"}`     |
| POST `/files/from-url` | JSON `url`, required valid URL, maximum 2,048 characters                        | 200, `{"hashName":"..."}`                          |

URL import downloads the image with a 30-second timeout. Allowed response media types are image/jpeg, image/png, image/gif, image/svg+xml, and image/webp. Content must not exceed 2,097,152 bytes. WebP is accepted by URL import but not by the multipart validator.

Imported filenames use the content MD5 plus a normalized extension. Existing identical content at that project path returns `existing: true` with `hashName`. New uploads omit `existing`. Import stores the file, not a page reference. A hosted page logo can use `usf://FILENAME`, which the page renderer resolves to the project's media URL.

Deduplication is by content hash plus extension, not content alone. Identical bytes imported with different accepted extensions can create separate files. Deleting media does not inspect or rewrite page references, and a missing filename still receives the same success message. Remove references before deleting an image that a published page uses.

Illustrative calls, not executed:

```bash
curl 'https://api.rightmessage.com/api/123456/files' -H 'Authorization: Bearer YOUR_API_TOKEN' -H 'Accept: application/json'
curl 'https://api.rightmessage.com/api/123456/files' -H 'Authorization: Bearer YOUR_API_TOKEN' -H 'Accept: application/json' -F 'file=@workforge-logo.png'
curl -X POST 'https://api.rightmessage.com/api/123456/files/from-url' -H 'Authorization: Bearer YOUR_API_TOKEN' -H 'Accept: application/json' -H 'Content-Type: application/json' --data '{"url":"https://example.com/logo.png"}'
curl -X DELETE 'https://api.rightmessage.com/api/123456/files/0123456789abcdef0123456789abcdef.png' -H 'Authorization: Bearer YOUR_API_TOKEN' -H 'Accept: application/json'
```

Corresponding illustrative bodies, in request order:

```text
["0123456789abcdef0123456789abcdef.png"]
workforge-upload-hash.png
{"hashName":"0123456789abcdef0123456789abcdef.png","existing":true}
{"message":"File deleted successfully"}
```

URL import returns 422 with `error` for `Invalid image type. Allowed: jpeg, png, gif, svg, webp`, `Image too large (max 2MB)`, or `Failed to download image from URL`. Request validation instead returns the normal `message` and `errors` envelope. Other storage or network failures can return 500.

## Public template catalog

The catalog describes available templates without installing them or changing a project.

GET `/api/template-catalog` returns 200 with `data`, an array of entries, and `meta: {last_page:1}`. It returns the entire catalog, ordered by configured order then title. There are no supported query filters or pagination inputs.

GET `/api/template-catalog/{idOrSlug}` accepts an exact template UUID or filename-derived slug. It returns 200 with `data`, one entry, or 404 when absent. Neither endpoint installs a template or changes a project.

Each entry contains `id`, `slug`, `title`, nullable `description`, nullable `preview_image` with a `url`, `template_type: {value:string}`, boolean `hide_preview`, nullable `impact_statement`, nullable `best_for`, array `template_categories`, nullable `structural_archetype`, boolean `is_skeleton`, and `payload_json: {value:string}`. The payload value is serialized JSON text, not an already-decoded object. Template type defaults flow; boolean flags default false.

Illustrative requests and response excerpts, not executed. The slug and entry are fictional:

```http
GET /api/template-catalog HTTP/1.1
Host: api.rightmessage.com
Accept: application/json

GET /api/template-catalog/workforge-welcome HTTP/1.1
Host: api.rightmessage.com
Accept: application/json
```

```json
{"data":[{"id":"11111111-1111-4111-8111-111111111111","slug":"workforge-welcome","title":"WorkForge welcome","template_type":{"value":"flow"},"payload_json":{"value":"{}"}}],"meta":{"last_page":1}}
```

The item response uses `{"data":ENTRY}` with the same full entry schema. The abbreviated example payload is not an installable template.

## Common failures

Authenticated routes return 401 for missing authentication and 403 for denied access or suspension. Invalid page or media inputs return 422. Missing, deleted, wrong-project, or nondigit page IDs return 404. Browser-session writes can fail CSRF protection. Maintenance returns 503 for these API routes, including the otherwise-public catalog. These operations do not create v1 configuration drafts or snapshots, or send v1 draft-update notifications.
