> ## Documentation Index
> Fetch the complete documentation index at: https://developers.octaviatech.app/llms.txt
> Use this file to discover all available pages before exploring further.

# PHP methods

> Every method on the PHP SDK, with its route, parameters and types.

131 methods across 11 resources. Every one returns the same envelope — `success`, `statusCode`, `message` and `data` — so a caller reads `ok` first and touches `data` only when it is set.

<Info>
  Method names follow the route segments, which is why some read as a name and a verb at once — `captchaConfigGET`, `socialLinkedinPublish`. That is the generator being literal, not a mistake.
</Info>

## How to read this page

Each resource opens with a table of its methods and the route each one calls, then a section per method with the full parameter list. Every parameter says where it goes (`path`, `query` or `body`), its type, and whether it is required.

## Resources

* [`ai`](#ai) — AI automation
* [`aiConversation`](#aiconversation) — AI conversation
* [`article`](#article) — Articles
* [`author`](#author) — Authors
* [`category`](#category) — Categories
* [`formSubmission`](#formsubmission) — Form submissions
* [`form`](#form) — Forms
* [`language`](#language) — Languages
* [`report`](#report) — Reports
* [`subcategory`](#subcategory) — Subcategories
* [`tag`](#tag) — Tags

***

## `ai`

Summarize, translate, SEO, repurpose, generate and social publishing.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`generateContent`](#ai-generatecontent) | `POST /ai/generateContent` | Generate full content (title, slug, summary, body) |
| [`generateImage`](#ai-generateimage) | `POST /ai/generateImage` | Generate a featured image |
| [`generateTitle`](#ai-generatetitle) | `POST /ai/generateTitle` | Generate a title from text |
| [`optimizeArticle`](#ai-optimizearticle) | `POST /ai/optimizeArticle` | Optimize an article for SEO and save the result |
| [`optimizeArticleStream`](#ai-optimizearticlestream) | `POST /ai/optimizeArticle/stream` | Optimize article SEO with streaming progress (Server-Sent Events) |
| [`repurpose`](#ai-repurpose) | `POST /ai/repurpose` | Repurpose article/text for social platforms |
| [`repurposeStream`](#ai-repurposestream) | `POST /ai/repurpose/stream` | Repurpose article/text for social platforms with SSE |
| [`repurposeTemplateGET`](#ai-repurposetemplateget) | `GET /ai/repurpose/template` | Get the tenant repurpose template |
| [`repurposeTemplatePUT`](#ai-repurposetemplateput) | `PUT /ai/repurpose/template` | Update the tenant repurpose template |
| [`seoOptimize`](#ai-seooptimize) | `POST /ai/seoOptimize` | SEO optimize text (content + metaDescription) |
| [`seoOptimizeStream`](#ai-seooptimizestream) | `POST /ai/seoOptimize/stream` | SEO optimize text with streaming response (Server-Sent Events) |
| [`socialConnections`](#ai-socialconnections) | `GET /ai/social/connections` | List the tenant's social publishing connections |
| [`socialLinkedinConnect`](#ai-sociallinkedinconnect) | `PUT /ai/social/linkedin/connect` | Connect or replace the tenant's LinkedIn account |
| [`socialLinkedinPublish`](#ai-sociallinkedinpublish) | `POST /ai/social/linkedin/publish` | Publish a text post to the connected LinkedIn account |
| [`socialTelegramConnect`](#ai-socialtelegramconnect) | `PUT /ai/social/telegram/connect` | Connect or replace the tenant's Telegram bot |
| [`socialTelegramPublish`](#ai-socialtelegrampublish) | `POST /ai/social/telegram/publish` | Send a message to the connected Telegram chat |
| [`socialTwitterConnect`](#ai-socialtwitterconnect) | `PUT /ai/social/twitter/connect` | Connect or replace the tenant's Twitter/X bearer token |
| [`socialTwitterPublish`](#ai-socialtwitterpublish) | `POST /ai/social/twitter/publish` | Post a tweet to the connected Twitter/X account |
| [`summarize`](#ai-summarize) | `POST /ai/summarize` | Summarize text |
| [`summarizeArticleStream`](#ai-summarizearticlestream) | `POST /ai/summarizeArticle/stream` | Summarize an article by ID with streaming response |
| [`summarizeStream`](#ai-summarizestream) | `POST /ai/summarize/stream` | Summarize text with streaming response |
| [`translate`](#ai-translate) | `POST /ai/translate` | Translate text |
| [`translateArticle`](#ai-translatearticle) | `POST /ai/translateArticle` | Translate an article into one or more languages |
| [`translateArticleStream`](#ai-translatearticlestream) | `POST /ai/translateArticle/stream` | Translate article with streaming progress (Server-Sent Events) |
| [`translateForm`](#ai-translateform) | `POST /ai/translateForm` | Translate a form into one or more languages |
| [`translateStream`](#ai-translatestream) | `POST /ai/translate/stream` | Translate text with streaming response (Server-Sent Events) |

### `generateContent`

`POST /ai/generateContent`

Generate full content (title, slug, summary, body).

Multi-step pipeline with timeouts & fallbacks. `body` is returned as a stringified HTML-wrapper like `<div>{"body":"<div>…</div>"}</div>`.

```php theme={null}
$cms->ai->generateContent(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | High-level topic or brief; can include structure hints |
| body | `wordCount` | `integer` | no | Target words for body (default \~800) |

### `generateImage`

`POST /ai/generateImage`

Generate a featured image.

Either `prompt` or `articleId` must be provided. When `articleId` is given and `prompt` is omitted, the prompt is derived from the article's title, summary, tags and a content excerpt. `setAsThumbnail` stores the generated image as the article thumbnail.

```php theme={null}
$cms->ai->generateImage(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `prompt` | `string` | no | Text description of the desired image |
| body | `articleId` | `string` | no | Article whose content is used to build the prompt |
| body | `size` | `1024x1024 / 1024x1536 / 1536x1024 / auto` | no | Output dimensions |
| body | `quality` | `low / medium / high / auto` | no | Rendering quality |
| body | `style` | `vivid / natural` | no | Art direction |
| body | `setAsThumbnail` | `boolean` | no | If true, sets the article thumbnail to the generated image (requires articleId) |

### `generateTitle`

`POST /ai/generateTitle`

Generate a title from text.

```php theme={null}
$cms->ai->generateTitle(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | Base text/article |
| body | `maxChars` | `integer` | no | Hard cap on title length |

### `optimizeArticle`

`POST /ai/optimizeArticle`

Optimize an article for SEO and save the result.

Optimizes each target language of the article and persists the patched fields plus the recomputed `seo.perLanguage` metrics. When `languages` is omitted, every language present on the article is optimized. `title`, `summary` and `content` default to true; `tags` defaults to false.

```php theme={null}
$cms->ai->optimizeArticle(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `articleId` | `string` | yes | |
| body | `primaryKeyword` | `string` | no | Keyword to target; falls back to the article's stored per-language primary keyword |
| body | `languages` | `string[]` | no | Language codes to optimize. Ignored codes that the article does not have. Defaults to all available languages. |
| body | `fields` | `object` | no | Per-field opt-in. Unspecified fields keep their defaults. |

### `optimizeArticleStream`

`POST /ai/optimizeArticle/stream`

Optimize article SEO with streaming progress (Server-Sent Events).

```php theme={null}
$cms->ai->optimizeArticleStream(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `articleId` | `string` | yes | |
| body | `primaryKeyword` | `string` | no | |
| body | `languages` | `string[]` | no | |
| body | `fields` | `object` | no | |

### `repurpose`

`POST /ai/repurpose`

Repurpose article/text for social platforms.

```php theme={null}
$cms->ai->repurpose(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `articleId` | `string` | no | Existing article id |
| body | `text` | `string` | no | Raw content (use instead of articleId) |
| body | `platforms` | `linkedin / twitter / telegram / instagram[]` | no | |
| body | `language` | `string` | no | |
| body | `tone` | `professional / friendly / casual / technical` | no | |
| body | `length` | `short / medium / long` | no | |

### `repurposeStream`

`POST /ai/repurpose/stream`

Repurpose article/text for social platforms with SSE.

```php theme={null}
$cms->ai->repurposeStream(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `articleId` | `string` | no | |
| body | `text` | `string` | no | |
| body | `platforms` | `linkedin / twitter / telegram / instagram[]` | no | |
| body | `language` | `string` | no | |
| body | `tone` | `professional / friendly / casual / technical` | no | |
| body | `length` | `short / medium / long` | no | |

### `repurposeTemplateGET`

`GET /ai/repurpose/template`

Get the tenant repurpose template.

Returns the stored template for the tenant, creating it with the service defaults (`en` / `professional` / `medium` and per-platform tone, length, CTA, hashtag style and max hashtag counts) on first read.

```php theme={null}
$cms->ai->repurposeTemplateGET()
```

### `repurposeTemplatePUT`

`PUT /ai/repurpose/template`

Update the tenant repurpose template.

Deep-merges the supplied values into the stored template. Omitted keys keep their current values, and unknown keys are stripped before validation. The per-platform sub-objects all have identical fields.

```php theme={null}
$cms->ai->repurposeTemplatePUT(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `defaults` | `object` | no | Tenant-wide defaults applied to every platform |
| body | `platforms` | `object` | no | Per-platform overrides. Every platform sub-object has the same fields. |

### `seoOptimize`

`POST /ai/seoOptimize`

SEO optimize text (content + metaDescription).

```php theme={null}
$cms->ai->seoOptimize(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | Raw text/HTML to optimize |
| body | `keyword` | `string` | no | Primary keyword to target |

### `seoOptimizeStream`

`POST /ai/seoOptimize/stream`

SEO optimize text with streaming response (Server-Sent Events).

```php theme={null}
$cms->ai->seoOptimizeStream(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | |
| body | `keyword` | `string` | no | |

### `socialConnections`

`GET /ai/social/connections`

List the tenant's social publishing connections.

Returns every stored connection for the tenant. Only linkedin, telegram and twitter are supported. Secrets are stored encrypted and are always masked in the response (`abcd...wxyz`).

```php theme={null}
$cms->ai->socialConnections()
```

### `socialLinkedinConnect`

`PUT /ai/social/linkedin/connect`

Connect or replace the tenant's LinkedIn account.

Upserts the LinkedIn connection. The access token is encrypted at rest and masked in the response. Re-calling this endpoint replaces the stored credentials and marks the connection active.

```php theme={null}
$cms->ai->socialLinkedinConnect(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `accessToken` | `string` | yes | LinkedIn OAuth access token |
| body | `authorUrn` | `string` | yes | Author URN the posts are published as |

### `socialLinkedinPublish`

`POST /ai/social/linkedin/publish`

Publish a text post to the connected LinkedIn account.

Requires an active LinkedIn connection for the tenant. Posts a UGC share with `lifecycleState: PUBLISHED` and no media. `visibility` defaults to PUBLIC. Upstream LinkedIn failures are surfaced as 502.

```php theme={null}
$cms->ai->socialLinkedinPublish(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | Post body |
| body | `visibility` | `PUBLIC / CONNECTIONS` | no | Who can see the post |

### `socialTelegramConnect`

`PUT /ai/social/telegram/connect`

Connect or replace the tenant's Telegram bot.

Upserts the Telegram connection. The bot token is encrypted at rest and masked in the response. Re-calling this endpoint replaces the stored credentials and marks the connection active.

```php theme={null}
$cms->ai->socialTelegramConnect(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `botToken` | `string` | yes | Telegram bot token |
| body | `chatId` | `string` | yes | Target chat id the bot posts to |

### `socialTelegramPublish`

`POST /ai/social/telegram/publish`

Send a message to the connected Telegram chat.

Requires an active Telegram connection for the tenant. Sends via the bot's `sendMessage` to the stored `chatId`. `parseMode` defaults to HTML. Upstream Telegram failures are surfaced as 502.

```php theme={null}
$cms->ai->socialTelegramPublish(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | Message body |
| body | `parseMode` | `Markdown / MarkdownV2 / HTML` | no | Telegram formatting mode for `text` |
| body | `disableWebPagePreview` | `boolean` | no | If true, suppresses link previews |

### `socialTwitterConnect`

`PUT /ai/social/twitter/connect`

Connect or replace the tenant's Twitter/X bearer token.

Upserts the Twitter connection. The bearer token is encrypted at rest and masked in the response. Re-calling this endpoint replaces the stored credential and marks the connection active.

```php theme={null}
$cms->ai->socialTwitterConnect(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `bearerToken` | `string` | yes | Twitter API v2 bearer token used to authorize tweet posts |

### `socialTwitterPublish`

`POST /ai/social/twitter/publish`

Post a tweet to the connected Twitter/X account.

Requires an active Twitter connection for the tenant. Posts to the Twitter v2 `tweets` endpoint using the stored bearer token. Upstream Twitter failures are surfaced as 502.

```php theme={null}
$cms->ai->socialTwitterPublish(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | Tweet body |

### `summarize`

`POST /ai/summarize`

Summarize text.

```php theme={null}
$cms->ai->summarize(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | Raw text or HTML |
| body | `maxWords` | `integer` | no | Target max words |

### `summarizeArticleStream`

`POST /ai/summarizeArticle/stream`

Summarize an article by ID with streaming response.

```php theme={null}
$cms->ai->summarizeArticleStream(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `articleId` | `string` | yes | |
| body | `maxWords` | `integer` | no | |
| body | `language` | `string` | no | Specific language to summarize (e.g. 'fa', 'en') |
| body | `fields` | `string[]` | no | Fields to summarize (e.g. \['content', 'title']) |

### `summarizeStream`

`POST /ai/summarize/stream`

Summarize text with streaming response.

```php theme={null}
$cms->ai->summarizeStream(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | |
| body | `maxWords` | `integer` | no | |

### `translate`

`POST /ai/translate`

Translate text.

```php theme={null}
$cms->ai->translate(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | |
| body | `targetLanguage` | `string` | yes | IETF code (e.g., en, es, fa…) |

### `translateArticle`

`POST /ai/translateArticle`

Translate an article into one or more languages.

Requires `articleId`, a non-empty `targetLanguages` array and at least one selected field (all fields default to false, so a request without `fields` is rejected). The source language is detected from the article unless `sourceLanguage` is given. If a target language already has a translation for a selected multilingual field and `sourceLanguage` is omitted, the request fails with 409 instead of overwriting.

```php theme={null}
$cms->ai->translateArticle(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `articleId` | `string` | yes | |
| body | `targetLanguages` | `string[]` | yes | Target language codes; at least one non-empty entry is required |
| body | `sourceLanguage` | `string` | no | Source language; auto-detected from the article when omitted |
| body | `fields` | `object` | yes | Which article fields to translate. At least one must be true; every flag defaults to false. |

### `translateArticleStream`

`POST /ai/translateArticle/stream`

Translate article with streaming progress (Server-Sent Events).

```php theme={null}
$cms->ai->translateArticleStream(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `articleId` | `string` | yes | |
| body | `sourceLanguage` | `string` | no | |
| body | `targetLanguages` | `string[]` | yes | |
| body | `fields` | `object` | no | |

### `translateForm`

`POST /ai/translateForm`

Translate a form into one or more languages.

Translates a form's multilingual text, section titles/descriptions, field labels/placeholders and option labels. The source language is detected from the form unless `sourceLanguage` is provided. Every flag in `fields` defaults to true, so an empty `fields` object translates the whole form. A target language that already holds a translation and no explicit `sourceLanguage` fails with 400 rather than overwriting.

```php theme={null}
$cms->ai->translateForm(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `formId` | `string` | yes | |
| body | `targetLanguages` | `string[]` | yes | Target language codes; at least one is required |
| body | `sourceLanguage` | `string` | no | Source language; auto-detected from the form when omitted |
| body | `fields` | `object` | no | Which parts of the form to translate. All flags default to true. |

### `translateStream`

`POST /ai/translate/stream`

Translate text with streaming response (Server-Sent Events).

```php theme={null}
$cms->ai->translateStream(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `text` | `string` | yes | |
| body | `targetLanguage` | `string` | yes | |

***

## `aiConversation`

Multi-turn article drafting that keeps context between calls.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`conversationContinue`](#aiconversation-conversationcontinue) | `POST /ai/conversation/continue` | Continue conversation with answers |
| [`conversationConversationId`](#aiconversation-conversationconversationid) | `GET /ai/conversation/{conversationId}` | Get conversation state |
| [`conversationGenerate`](#aiconversation-conversationgenerate) | `POST /ai/conversation/generate` | Continue conversation and generate article (combined) |
| [`conversationGenerateStream`](#aiconversation-conversationgeneratestream) | `POST /ai/conversation/generate/stream` | Continue and generate article with streaming (SSE) |
| [`conversationRegenerate`](#aiconversation-conversationregenerate) | `POST /ai/conversation/regenerate` | Regenerate article with SEO improvements |
| [`conversationStart`](#aiconversation-conversationstart) | `POST /ai/conversation/start` | Start a new article generation conversation |

### `conversationContinue`

`POST /ai/conversation/continue`

Continue conversation with answers.

```php theme={null}
$cms->aiConversation->conversationContinue(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `conversationId` | `string` | yes | |
| body | `userMessage` | `string` | yes | |

### `conversationConversationId`

`GET /ai/conversation/{conversationId}`

Get conversation state.

```php theme={null}
$cms->aiConversation->conversationConversationId(conversationId)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `conversationId` | `string` | yes | Taken from the route. |

### `conversationGenerate`

`POST /ai/conversation/generate`

Continue conversation and generate article (combined).

```php theme={null}
$cms->aiConversation->conversationGenerate(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `conversationId` | `string` | yes | |
| body | `userMessage` | `string` | yes | User's answer to clarification questions |

### `conversationGenerateStream`

`POST /ai/conversation/generate/stream`

Continue and generate article with streaming (SSE).

```php theme={null}
$cms->aiConversation->conversationGenerateStream(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `conversationId` | `string` | yes | |
| body | `userMessage` | `string` | yes | |

### `conversationRegenerate`

`POST /ai/conversation/regenerate`

Regenerate article with SEO improvements.

```php theme={null}
$cms->aiConversation->conversationRegenerate(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `conversationId` | `string` | yes | |

### `conversationStart`

`POST /ai/conversation/start`

Start a new article generation conversation.

```php theme={null}
$cms->aiConversation->conversationStart(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `prompt` | `string` | yes | |

***

## `article`

Create, read, publish, archive and delete articles, plus comments, reactions and engagement settings.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`advanceSearch`](#article-advancesearch) | `GET /articles/advanceSearch` | Advanced search articles |
| [`archive`](#article-archive) | `PUT /articles/archive` | Archive article |
| [`commentsCommentIdDELETE`](#article-commentscommentiddelete) | `DELETE /articles/comments/{commentId}` | Delete comment |
| [`commentsCommentIdPATCH`](#article-commentscommentidpatch) | `PATCH /articles/comments/{commentId}` | Update comment |
| [`commentsCommentIdReactionDELETE`](#article-commentscommentidreactiondelete) | `DELETE /articles/comments/{commentId}/reaction` | Remove reaction from comment |
| [`commentsCommentIdReactionPOST`](#article-commentscommentidreactionpost) | `POST /articles/comments/{commentId}/reaction` | Set reaction on comment |
| [`commentsCommentIdReactionSummary`](#article-commentscommentidreactionsummary) | `GET /articles/comments/{commentId}/reactionSummary` | Get comment reaction summary |
| [`commentsGetAll`](#article-commentsgetall) | `GET /articles/comments/getAll` | Get all comments for tenant (moderation) |
| [`create`](#article-create) | `POST /articles/create` | Create article |
| [`deleteId`](#article-deleteid) | `DELETE /articles/delete/{id}` | Delete article |
| [`engagementSettingsGET`](#article-engagementsettingsget) | `GET /articles/engagementSettings` | Get tenant engagement settings (public) |
| [`engagementSettingsPUT`](#article-engagementsettingsput) | `PUT /articles/engagementSettings` | Update tenant engagement settings |
| [`getAll`](#article-getall) | `GET /articles/getAll` | Get all articles (public) |
| [`getByAuthorId`](#article-getbyauthorid) | `GET /articles/getByAuthorId/{authorId}` | Get articles by author id (public) |
| [`getByCategoryId`](#article-getbycategoryid) | `GET /articles/getByCategoryId/{categoryId}` | Get articles by category id (public) |
| [`getByCategorySlug`](#article-getbycategoryslug) | `GET /articles/getByCategorySlug/{slug}` | Get articles by category slug (public) |
| [`getById`](#article-getbyid) | `GET /articles/getById/{id}` | Get article by id (public) |
| [`getBySlug`](#article-getbyslug) | `GET /articles/getBySlug/{slug}` | Get article by slug (public) |
| [`getBySubCategoryId`](#article-getbysubcategoryid) | `GET /articles/getBySubCategoryId/{subCategoryId}` | Get articles by subcategory id (public) |
| [`getBySubCategorySlug`](#article-getbysubcategoryslug) | `GET /articles/getBySubCategorySlug/{slug}` | Get articles by subcategory slug (public) |
| [`getByTag`](#article-getbytag) | `GET /articles/getByTag/{tag}` | Get articles by tag (public) |
| [`idCommentsGET`](#article-idcommentsget) | `GET /articles/{id}/comments` | Get comments for article (public) |
| [`idCommentsPOST`](#article-idcommentspost) | `POST /articles/{id}/comments` | Create comment on article |
| [`idReactionDELETE`](#article-idreactiondelete) | `DELETE /articles/{id}/reaction` | Remove reaction from article |
| [`idReactionPOST`](#article-idreactionpost) | `POST /articles/{id}/reaction` | Set reaction on article |
| [`idReactionSummary`](#article-idreactionsummary) | `GET /articles/{id}/reactionSummary` | Get article reaction summary |
| [`search`](#article-search) | `GET /articles/search` | Search articles |
| [`seoAnalysisId`](#article-seoanalysisid) | `GET /articles/seoAnalysis/{id}` | Get SEO analysis for article |
| [`update`](#article-update) | `PUT /articles/update` | Update article |

### `advanceSearch`

`GET /articles/advanceSearch`

Advanced search articles.

Combine filters (category, subCategory, author, tags, keyword) with pagination & sorting. Always restricted to published, non-private, non-deleted articles.

```php theme={null}
$cms->article->advanceSearch(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `keyword` | `string` | no | Free-text term matched against titles, summaries and body e.g. `droopy` |
| query | `category` | `string` | no | e.g. `{{objectId}}` |
| query | `subCategory` | `string` | no | e.g. `{{objectId}}` |
| query | `author` | `string` | no | e.g. `{{objectId}}` |
| query | `tags` | `string` | no | Single tag, or repeat the param for several (`?tags=ai&tags=news`). Not comma-separated e.g. `ai` |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `20` |
| query | `sortBy` | `createdAt / publishDate` | no | e.g. `publishDate` |
| query | `sortOrder` | `asc / desc` | no | e.g. `desc` |

### `archive`

`PUT /articles/archive`

Archive article.

Archives an article by soft-deleting it (sets `isDeleted=true`). The record is retained and the publish state is left unchanged.

<Note>A soft delete — the row is hidden but not removed. `deleteId` is the real remove.</Note>

```php theme={null}
$cms->article->archive(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `id` | `string` | yes | |

### `commentsCommentIdDELETE`

`DELETE /articles/comments/{commentId}`

Delete comment.

Soft-deletes the comment (sets `isDeleted`), removes its reactions, and detaches it from the article. A caller may only delete their own comment; a privileged role (SUPER\_USER, MANAGER, CHIEF\_EDITOR, PUBLISHER, EDITOR) may delete any comment in the tenant.

```php theme={null}
$cms->article->commentsCommentIdDELETE(commentId)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `commentId` | `string` | yes | Taken from the route. |

### `commentsCommentIdPATCH`

`PATCH /articles/comments/{commentId}`

Update comment.

Partial update — send `content`, `status`, or both (at least one is required). A caller may only edit their own comment; changing `status` additionally requires a privileged role (SUPER\_USER, MANAGER, CHIEF\_EDITOR, PUBLISHER, EDITOR) and is rejected with 403 otherwise.

```php theme={null}
$cms->article->commentsCommentIdPATCH(commentId, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `commentId` | `string` | yes | Taken from the route. |
| body | `content` | `string` | no | New comment body, trimmed before storing. Max 4000 characters. |
| body | `status` | `pending / approved / rejected` | no | New moderation status. Requires a privileged moderator role. |
| body | `captchaToken` | `string` | no | Captcha token. Used when the caller is editing a guest comment and the tenant has `requireCaptchaForGuestEngagement` enabled. |

### `commentsCommentIdReactionDELETE`

`DELETE /articles/comments/{commentId}/reaction`

Remove reaction from comment.

Deletes the caller's own reaction for an approved comment. Removing a reaction that does not exist still succeeds, and the returned totals reflect the comment after refresh.

```php theme={null}
$cms->article->commentsCommentIdReactionDELETE(commentId)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `commentId` | `string` | yes | Taken from the route. |

### `commentsCommentIdReactionPOST`

`POST /articles/comments/{commentId}/reaction`

Set reaction on comment.

Creates or updates the caller's reaction for an approved comment. One reaction per actor (authenticated user, or guest fingerprint) is kept, so submitting `like` after `dislike` switches it. Rejected with 403 when the parent article, its category, or its sub-category disables reactions.

```php theme={null}
$cms->article->commentsCommentIdReactionPOST(commentId, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `commentId` | `string` | yes | Taken from the route. |
| body | `type` | `like / dislike` | yes | Reaction to record |
| body | `captchaToken` | `string` | no | Captcha token. Required when the caller is treated as a guest and the tenant has `requireCaptchaForGuestEngagement` enabled. |
| body | `guestId` | `string` | no | Stable client-generated guest identifier. Used to build the guest fingerprint together with IP and user agent when the caller is unauthenticated. |

### `commentsCommentIdReactionSummary`

`GET /articles/comments/{commentId}/reactionSummary`

Get comment reaction summary.

Returns the stored like/dislike totals for an approved comment. `userReaction` is the caller's own reaction (`like`, `dislike`, or `null` when the caller has none or cannot be identified as a user or guest).

```php theme={null}
$cms->article->commentsCommentIdReactionSummary(commentId)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `commentId` | `string` | yes | Taken from the route. |

### `commentsGetAll`

`GET /articles/comments/getAll`

Get all comments for tenant (moderation).

Paginated moderation feed of every non-deleted comment in the tenant, newest first, including pending and rejected ones. Each row is joined with its article, the article's categories, and the comment author. All filters are optional and combine together.

```php theme={null}
$cms->article->commentsGetAll(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | Defaults to 20, capped at 100 e.g. `20` |
| query | `status` | `pending / approved / rejected` | no | Filter by moderation status. Values outside the enum are ignored. e.g. `pending` |
| query | `from` | `string` | no | Only comments created on or after this date e.g. `2025-05-01` |
| query | `to` | `string` | no | Only comments created on or before this date (end of day) e.g. `2025-05-31` |
| query | `articleId` | `string` | no | e.g. `{{objectId}}` |
| query | `categoryId` | `string` | no | e.g. `{{objectId}}` |
| query | `articleName` | `string` | no | Case-insensitive match against the article slug (or full-text search) e.g. `droopy-nose` |

### `create`

`POST /articles/create`

Create article.

```php theme={null}
$cms->article->create(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `mainTitle` | `object` | yes | Main title in multiple languages |
| body | `title2` | `object` | no | Optional secondary title in multiple languages |
| body | `title3` | `object` | no | Optional tertiary title in multiple languages |
| body | `slug` | `string` | no | URL-friendly unique identifier |
| body | `summary` | `object` | no | Short summary in multiple languages |
| body | `content` | `object` | yes | Main article content (HTML allowed) |
| body | `thumbnail` | `string` | no | Thumbnail image URL |
| body | `tags` | `string[]` | no | List of tags |
| body | `category` | `string[]` | yes | Array of category IDs |
| body | `subCategory` | `string[]` | no | Array of sub-category IDs |
| body | `author` | `string` | no | Author ID. Defaults to the authenticated user when omitted |
| body | `gallery` | `string[]` | no | Optional gallery image URLs |
| body | `publishDate` | `string` | no | Publish date in ISO 8601 |
| body | `isPublished` | `boolean` | no | Publish state |
| body | `isPrivate` | `boolean` | no | If true, restricts visibility |
| body | `language` | `en / es` | no | Preferred language for immediate rendering/preview |
| body | `autoSummarize` | `boolean` | no | If true, backend may auto-create a summary |

### `deleteId`

`DELETE /articles/delete/{id}`

Delete article.

```php theme={null}
$cms->article->deleteId(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `engagementSettingsGET`

`GET /articles/engagementSettings`

Get tenant engagement settings (public).

Returns the effective engagement configuration for your tenant. When the tenant has no stored record, the built-in defaults are returned. Article and category/sub-category overrides are applied separately and are not reflected here.

```php theme={null}
$cms->article->engagementSettingsGET()
```

### `engagementSettingsPUT`

`PUT /articles/engagementSettings`

Update tenant engagement settings.

Upserts the engagement configuration for your tenant. Every field is optional — omitted fields keep their current value, and a tenant with no record yet starts from the built-in defaults. Booleans and numbers are validated server-side; out-of-range values are rejected with 400. The authenticated user id is stored as `updatedBy`.

```php theme={null}
$cms->article->engagementSettingsPUT(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `commentsEnabled` | `boolean` | no | Master switch for comments. Default true. |
| body | `reactionsEnabled` | `boolean` | no | Master switch for reactions. Default true. |
| body | `allowGuestComments` | `boolean` | no | Allow unauthenticated visitors to comment. Default false. |
| body | `allowGuestReactions` | `boolean` | no | Allow unauthenticated visitors to react. Default false. |
| body | `autoApproveComments` | `boolean` | no | When false, new comments are stored as `pending` for moderation. Default true. |
| body | `requireCaptchaForGuestEngagement` | `boolean` | no | Require a captcha token for guest comments and reactions. Default true. |
| body | `guestEngagementCaptchaMinScore` | `number` | no | Minimum captcha score accepted for guest engagement, between 0 and 1. Default 0.5. |
| body | `guestEngagementRateLimitPerMin` | `number` | no | Guest engagement actions allowed per minute per guest fingerprint, between 1 and 500 (floored to an integer). Default 20. |

### `getAll`

`GET /articles/getAll`

Get all articles (public).

Returns a paginated list. Public view defaults to published & active & not private/deleted. Use filters to refine results.

<Note>Paginated. `page` is 1-based and `limit` caps the page size; the totals come back in the response metadata.</Note>

```php theme={null}
$cms->article->getAll(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `20` |
| query | `keyword` | `string` | no | e.g. `nose` |
| query | `includeDeleted` | `boolean` | no | e.g. `False` |
| query | `includeUnpublished` | `boolean` | no | e.g. `False` |
| query | `includePrivate` | `boolean` | no | e.g. `False` |
| query | `category` | `string` | no | e.g. `{{objectId}}` |
| query | `subCategory` | `string` | no | e.g. `{{objectId}}` |
| query | `author` | `string` | no | e.g. `{{objectId}}` |
| query | `tags` | `string` | no | Single tag, or repeat the param for several (`?tags=ai&tags=news`). Not comma-separated e.g. `ai` |
| query | `sortBy` | `createdAt / publishDate` | no | e.g. `publishDate` |
| query | `sortOrder` | `asc / desc` | no | e.g. `desc` |

### `getByAuthorId`

`GET /articles/getByAuthorId/{authorId}`

Get articles by author id (public).

```php theme={null}
$cms->article->getByAuthorId(authorId, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `authorId` | `string` | yes | Taken from the route. |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `20` |

### `getByCategoryId`

`GET /articles/getByCategoryId/{categoryId}`

Get articles by category id (public).

```php theme={null}
$cms->article->getByCategoryId(categoryId, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `categoryId` | `string` | yes | Taken from the route. |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `20` |

### `getByCategorySlug`

`GET /articles/getByCategorySlug/{slug}`

Get articles by category slug (public).

```php theme={null}
$cms->article->getByCategorySlug(slug, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `slug` | `string` | yes | Taken from the route. |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `20` |
| query | `keyword` | `string` | no | Free-text term matched against titles, summaries and body e.g. `droopy` |
| query | `includeDeleted` | `boolean` | no | e.g. `False` |
| query | `includeUnpublished` | `boolean` | no | e.g. `False` |
| query | `includePrivate` | `boolean` | no | e.g. `False` |
| query | `subCategory` | `string` | no | e.g. `{{objectId}}` |
| query | `author` | `string` | no | e.g. `{{objectId}}` |
| query | `tags` | `string` | no | Single tag, or repeat the param for several. Not comma-separated e.g. `ai` |
| query | `sortBy` | `createdAt / publishDate` | no | e.g. `publishDate` |
| query | `sortOrder` | `asc / desc` | no | e.g. `desc` |

### `getById`

`GET /articles/getById/{id}`

Get article by id (public).

Multilingual fields are localized from the `lang` or `accept-language` request header. Omit the headers to receive every configured language.

```php theme={null}
$cms->article->getById(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `getBySlug`

`GET /articles/getBySlug/{slug}`

Get article by slug (public).

Multilingual fields are localized from the `lang` request header. Omit the header to receive every configured language.

```php theme={null}
$cms->article->getBySlug(slug)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `slug` | `string` | yes | Taken from the route. |

### `getBySubCategoryId`

`GET /articles/getBySubCategoryId/{subCategoryId}`

Get articles by subcategory id (public).

```php theme={null}
$cms->article->getBySubCategoryId(subCategoryId, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `subCategoryId` | `string` | yes | Taken from the route. |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `20` |

### `getBySubCategorySlug`

`GET /articles/getBySubCategorySlug/{slug}`

Get articles by subcategory slug (public).

```php theme={null}
$cms->article->getBySubCategorySlug(slug, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `slug` | `string` | yes | Taken from the route. |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `20` |

### `getByTag`

`GET /articles/getByTag/{tag}`

Get articles by tag (public).

```php theme={null}
$cms->article->getByTag(tag, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `tag` | `string` | yes | Taken from the route. |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `20` |

### `idCommentsGET`

`GET /articles/{id}/comments`

Get comments for article (public).

Paginated list of approved, non-deleted comments for the article, each with the author (`firstName`, `lastName`, `avatar`) and the caller's own reaction. When comments are disabled for the article, category, or sub-category, an empty list is returned with 200 instead of an error.

```php theme={null}
$cms->article->idCommentsGET(id, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | Defaults to 20, capped at 100 e.g. `20` |
| query | `sortOrder` | `asc / desc` | no | Sort by creation time. Any value other than `asc` means newest first. e.g. `desc` |
| query | `parentId` | `string` | no | Return only replies to this comment. Takes precedence over `rootOnly`. e.g. `{{objectId}}` |
| query | `rootOnly` | `string` | no | Set to `true` to return only top-level comments. Ignored when `parentId` is provided. e.g. `true` |

### `idCommentsPOST`

`POST /articles/{id}/comments`

Create comment on article.

Adds a comment to the article. The stored status is `approved` or `pending` depending on the tenant/article `autoApproveComments` setting. Pass `parentId` to reply to an existing comment on the same article; the parent must exist and not be deleted. Rejected with 403 when the article, its category, or its sub-category disables comments.

```php theme={null}
$cms->article->idCommentsPOST(id, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |
| body | `content` | `string` | yes | Comment body, trimmed before storing. Max 4000 characters. |
| body | `parentId` | `string` | no | Optional id of the comment being replied to |
| body | `captchaToken` | `string` | no | Captcha token. Required when the caller is treated as a guest and the tenant has `requireCaptchaForGuestEngagement` enabled. |
| body | `guestId` | `string` | no | Stable client-generated guest identifier. Used to build the guest fingerprint together with IP and user agent when the caller is unauthenticated. |

### `idReactionDELETE`

`DELETE /articles/{id}/reaction`

Remove reaction from article.

Deletes the caller's own reaction for the article. Removing a reaction that does not exist still succeeds, and the returned totals reflect the article after refresh. Rejected with 403 when the article, its category, or its sub-category disables reactions.

```php theme={null}
$cms->article->idReactionDELETE(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `idReactionPOST`

`POST /articles/{id}/reaction`

Set reaction on article.

Creates or updates the caller's reaction for the article. One reaction per actor (authenticated user, or guest fingerprint) is kept, so submitting `like` after `dislike` switches it. Reactions are rejected with 403 when the article, its category, or its sub-category disables reactions.

```php theme={null}
$cms->article->idReactionPOST(id, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |
| body | `type` | `like / dislike` | yes | Reaction to record |
| body | `captchaToken` | `string` | no | Captcha token. Required when the caller is treated as a guest and the tenant has `requireCaptchaForGuestEngagement` enabled. |
| body | `guestId` | `string` | no | Stable client-generated guest identifier. Used to build the guest fingerprint together with IP and user agent when the caller is unauthenticated. |

### `idReactionSummary`

`GET /articles/{id}/reactionSummary`

Get article reaction summary.

Returns the stored like/dislike totals for the article. `userReaction` is the caller's own reaction (`like`, `dislike`, or `null` when the caller has none or cannot be identified as a user or guest).

```php theme={null}
$cms->article->idReactionSummary(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `search`

`GET /articles/search`

Search articles.

```php theme={null}
$cms->article->search(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `keyword` | `string` | no | Free-text term matched against titles, summaries and body. Omit to list all published articles e.g. `droopy` |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `20` |

### `seoAnalysisId`

`GET /articles/seoAnalysis/{id}`

Get SEO analysis for article.

Returns comprehensive SEO analysis including keyword analysis, link/image metrics, readability scores, and technical SEO checks. Supports filtering by language via query parameter.

```php theme={null}
$cms->article->seoAnalysisId(id, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Article ID |
| query | `lang` | `string` | no | Optional language code to filter results for specific language. If not provided, returns analysis for all languages. e.g. `en` |

### `update`

`PUT /articles/update`

Update article.

Partial update. Provide only fields you want to change, plus the `id`.

<Note>Takes the whole document in its body, so `id` is a field of that body rather than a separate argument.</Note>

```php theme={null}
$cms->article->update(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `id` | `string` | yes | |
| body | `mainTitle` | `object` | no | |
| body | `title2` | `object` | no | |
| body | `title3` | `object` | no | |
| body | `slug` | `string` | no | |
| body | `summary` | `object` | no | |
| body | `content` | `object` | no | |
| body | `thumbnail` | `string` | no | |
| body | `tags` | `string[]` | no | |
| body | `category` | `string[]` | no | |
| body | `subCategory` | `string[]` | no | |
| body | `author` | `string` | no | |
| body | `gallery` | `string[]` | no | |
| body | `publishDate` | `string` | no | |
| body | `isPublished` | `boolean` | no | |
| body | `isPrivate` | `boolean` | no | |

***

## `author`

The people articles are attributed to.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`create`](#author-create) | `POST /authors/create` | Create author |
| [`deleteId`](#author-deleteid) | `DELETE /authors/delete/{id}` | Delete author |
| [`getAll`](#author-getall) | `GET /authors/getAll` | Get all authors (public) |
| [`getById`](#author-getbyid) | `GET /authors/getById/{id}` | Get author by id (public) |
| [`getBySlug`](#author-getbyslug) | `GET /authors/getBySlug/{slug}` | Get author by slug (public) |
| [`update`](#author-update) | `PUT /authors/update` | Update author |

### `create`

`POST /authors/create`

Create author.

```php theme={null}
$cms->author->create(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `name` | `object` | yes | Author name keyed by language code. Keys must be language codes configured for the service. |
| body | `slug` | `string` | yes | URL-friendly unique identifier (unique per tenant). |
| body | `bio` | `object` | no | Author bio keyed by language code (HTML allowed). |
| body | `avatar` | `string` | no | Avatar image URL. |
| body | `email` | `string` | yes | Public contact email (unique per tenant). |
| body | `isPrivate` | `boolean` | no | Whether the author is hidden from public listings. Defaults to false. |
| body | `isActive` | `boolean` | no | Whether author is active. Defaults to true. |

### `deleteId`

`DELETE /authors/delete/{id}`

Delete author.

```php theme={null}
$cms->author->deleteId(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `getAll`

`GET /authors/getAll`

Get all authors (public).

<Note>Paginated. `page` is 1-based and `limit` caps the page size; the totals come back in the response metadata.</Note>

```php theme={null}
$cms->author->getAll(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | e.g. `1` default `1` |
| query | `limit` | `integer` | no | e.g. `10` default `10` |

### `getById`

`GET /authors/getById/{id}`

Get author by id (public).

```php theme={null}
$cms->author->getById(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `getBySlug`

`GET /authors/getBySlug/{slug}`

Get author by slug (public).

```php theme={null}
$cms->author->getBySlug(slug)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `slug` | `string` | yes | Taken from the route. |

### `update`

`PUT /authors/update`

Update author.

Partial update. Provide only fields to change, plus `id`.

<Note>Takes the whole document in its body, so `id` is a field of that body rather than a separate argument.</Note>

```php theme={null}
$cms->author->update(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `id` | `string` | yes | |
| body | `name` | `object` | no | Author name keyed by language code. Keys must be language codes configured for the service. |
| body | `slug` | `string` | no | URL-friendly unique identifier (unique per tenant). |
| body | `bio` | `object` | no | Author bio keyed by language code (HTML allowed). |
| body | `avatar` | `string` | no | Avatar image URL. |
| body | `email` | `string` | no | Public contact email (unique per tenant). |
| body | `isPrivate` | `boolean` | no | Whether the author is hidden from public listings. |
| body | `isActive` | `boolean` | no | Whether author is active. |

***

## `category`

The top level of the content tree.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`create`](#category-create) | `POST /categories/create` | Create category |
| [`deleteId`](#category-deleteid) | `DELETE /categories/delete/{id}` | Delete category |
| [`getAll`](#category-getall) | `GET /categories/getAll` | Get all categories (public) |
| [`getById`](#category-getbyid) | `GET /categories/getById/{id}` | Get category by id (public) |
| [`getBySlug`](#category-getbyslug) | `GET /categories/getBySlug/{slug}` | Get category by slug (public) |
| [`update`](#category-update) | `PUT /categories/update` | Update category |

### `create`

`POST /categories/create`

Create category.

```php theme={null}
$cms->category->create(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `name` | `object` | yes | Category name in multiple languages |
| body | `slug` | `string` | yes | URL-friendly unique identifier |
| body | `thumbnail` | `string` | no | Optional thumbnail URL |
| body | `description` | `object` | no | Optional description in multiple languages |
| body | `isPrivate` | `boolean` | no | If true, hides the category from public listings |
| body | `isActive` | `boolean` | no | Whether category is active |

### `deleteId`

`DELETE /categories/delete/{id}`

Delete category.

Soft-deletes the category and cascades `isDeleted` onto its sub-categories.

```php theme={null}
$cms->category->deleteId(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `getAll`

`GET /categories/getAll`

Get all categories (public).

<Note>Paginated. `page` is 1-based and `limit` caps the page size; the totals come back in the response metadata.</Note>

```php theme={null}
$cms->category->getAll(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `50` |
| query | `privacy` | `public / private` | no | "public" (default) returns only non-private categories; "private" returns only private ones; any other value returns both e.g. `public` |

### `getById`

`GET /categories/getById/{id}`

Get category by id (public).

```php theme={null}
$cms->category->getById(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `getBySlug`

`GET /categories/getBySlug/{slug}`

Get category by slug (public).

```php theme={null}
$cms->category->getBySlug(slug)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `slug` | `string` | yes | Taken from the route. |

### `update`

`PUT /categories/update`

Update category.

Partial update. Provide only fields to change, plus `id`.

<Note>Takes the whole document in its body, so `id` is a field of that body rather than a separate argument.</Note>

```php theme={null}
$cms->category->update(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `id` | `string` | yes | |
| body | `name` | `object` | no | |
| body | `slug` | `string` | no | |
| body | `description` | `object` | no | |
| body | `thumbnail` | `string` | no | |
| body | `isPrivate` | `boolean` | no | |
| body | `isActive` | `boolean` | no | |

***

## `formSubmission`

Submitted answers and the relations between them and other records.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`getSubmissionById`](#formsubmission-getsubmissionbyid) | `GET /forms/getSubmissionById/{id}` | Get submission by id |
| [`idGetAllSubmissions`](#formsubmission-idgetallsubmissions) | `GET /forms/{id}/getAllSubmissions` | Get submissions by form id |
| [`idInternalSubmit`](#formsubmission-idinternalsubmit) | `POST /forms/{id}/internal-submit` | Submit a form from the backend (internal) |
| [`idSubmit`](#formsubmission-idsubmit) | `POST /forms/{id}/submit` | Submit a form (public) |
| [`submissionDeleteId`](#formsubmission-submissiondeleteid) | `DELETE /forms/submission/delete/{id}` | Delete submission |
| [`submissionIdRelations`](#formsubmission-submissionidrelations) | `GET /forms/submission/{id}/relations` | Get submission relations |
| [`submissionIdRelationsFieldNameConnect`](#formsubmission-submissionidrelationsfieldnameconnect) | `POST /forms/submission/{id}/relations/{fieldName}/connect` | Connect relation |
| [`submissionIdRelationsFieldNameDisconnect`](#formsubmission-submissionidrelationsfieldnamedisconnect) | `POST /forms/submission/{id}/relations/{fieldName}/disconnect` | Disconnect relation |
| [`submissionIdRelationsFieldNameReorder`](#formsubmission-submissionidrelationsfieldnamereorder) | `POST /forms/submission/{id}/relations/{fieldName}/reorder` | Reorder multi relation |
| [`submissionUpdateId`](#formsubmission-submissionupdateid) | `PUT /forms/submission/update/{id}` | Update submission (partial) |
| [`submissionsGetAll`](#formsubmission-submissionsgetall) | `GET /forms/submissions/getAll` | Get all submissions for the tenant |
| [`submissionsRelationsBackfill`](#formsubmission-submissionsrelationsbackfill) | `POST /forms/submissions/relations/backfill` | Backfill normalized submission relations |

### `getSubmissionById`

`GET /forms/getSubmissionById/{id}`

Get submission by id.

Requires auth.

```php theme={null}
$cms->formSubmission->getSubmissionById(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the submission. |

### `idGetAllSubmissions`

`GET /forms/{id}/getAllSubmissions`

Get submissions by form id.

Returns submissions for the given form with optional filters and pagination. Requires auth. Callers whose role is `WRITER` are restricted to forms they created.

```php theme={null}
$cms->formSubmission->idGetAllSubmissions(id, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the form. |
| query | `page` | `integer` | no | Page index (1-based). e.g. `1` |
| query | `limit` | `integer` | no | Items per page. e.g. `10` |
| query | `status` | `string` | no | e.g. `pending,approved` |
| query | `lang` | `string` | no | e.g. `en` |

### `idInternalSubmit`

`POST /forms/{id}/internal-submit`

Submit a form from the backend (internal).

Server-to-server submission endpoint. Shares its implementation with the public `/forms/{id}/submit` route but runs on the `internal` channel, which changes three things: - the form is not restricted to `formType: public` — internal forms are reachable; - CAPTCHA is not verified, even when the form has `captcha.enabled=true`; - the stored `status` is taken from the request body's `status` instead of being forced to `pending`. Required-field, disabled-field and type validation still run, as do field `relation` checks. The caller must present the `form.read` permission, and the tenant's seat limit for form submissions is enforced; a tenant at its limit is rejected with 426. "Internal" describes the calling party (a trusted backend), not a separate authentication scheme.

<Note>Server-to-server. Skips captcha, so call it only from a trusted backend.</Note>

```php theme={null}
$cms->formSubmission->idInternalSubmit(id, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the target form. |
| body | `language` | `string` | yes | Language code this submission is stored under (e.g. "en", "fa"). Defaults to "fa" when omitted. |
| body | `values` | `object` | yes | Key-value map where keys are the form's `sections[].fields[].name`. Field rules are enforced server-side (406 on violation): - fields with `required: true` must be present and non-empty - fields with `disabled: true` must be absent - type is checked per field type (email, tel, number, checkbox/switch boolean, multi-checkbox/multi-select array, select must match an `options[].value`) - a field with a `relation` must contain existing submission ids of the target form; `one-to-one` and `many-to-one` take a single id string, `one-to-many` and `many-to-many` take a non-empty array of id strings |
| body | `status` | `0 / 1 / 2` | no | Initial status. Honoured on this route only (ignored on the public submit route). Defaults to 0 (pending) when omitted. |
| body | `captchaToken` | `string` | no | Accepted but not verified on this route — CAPTCHA checks are skipped for the internal channel. |

### `idSubmit`

`POST /forms/{id}/submit`

Submit a form (public).

Public submission endpoint. Backend enforces required fields, disabled fields, and type-based validation. For "select", "multi-select", and "multi-checkbox", values must be taken from form options\[].value. If a field has `relation`, submitted values must be related submission ids from the target form. `one-to-one` and `many-to-one` accept a single id string. `one-to-many` and `many-to-many` accept an array of id strings. Requires the gateway (`publicRoutes`) and a valid `the service status` header (`enforceTenantStatus`, otherwise 400/423/403). `checkLimits("formSubmissions")` applies the tenant's seat limit and rejects with 426 once the limit is reached. Status is always stored as pending on this route; only `/forms/{id}/internal-submit` honours a body `status`.

<Note>The public path. Validates captcha when the form requires it.</Note>

```php theme={null}
$cms->formSubmission->idSubmit(id, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the target form. |
| body | `language` | `string` | yes | Language code for storing this submission (e.g., "en"). Not validated against the form’s title keys by the service; use a configured code. |
| body | `captchaToken` | `string` | no | CAPTCHA token received on the client from provider widget. Required only if the form has captcha.enabled=true. |
| body | `values` | `object` | yes | Key-value map where keys are form fields\[].name. Validation rules per field type: - email: valid email - tel: international phone pattern - number: numeric value - checkbox/switch: boolean - multi-checkbox/multi-select: array - select: one of options\[].value |

### `submissionDeleteId`

`DELETE /forms/submission/delete/{id}`

Delete submission.

Soft-deletes a submission by its ID — the record is retained with `isDeleted: true` and excluded from all list queries.

```php theme={null}
$cms->formSubmission->submissionDeleteId(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the submission. |

### `submissionIdRelations`

`GET /forms/submission/{id}/relations`

Get submission relations.

Returns resolved forward relations and reverse relations for a submission. Optional `fieldName` query narrows forward relations to one field.

```php theme={null}
$cms->formSubmission->submissionIdRelations(id, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |
| query | `fieldName` | `string` | no | |

### `submissionIdRelationsFieldNameConnect`

`POST /forms/submission/{id}/relations/{fieldName}/connect`

Connect relation.

Connects one related submission to a relation field. For single-value relations, the existing value is replaced.

```php theme={null}
$cms->formSubmission->submissionIdRelationsFieldNameConnect(id, fieldName, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the source submission. |
| path | `fieldName` | `string` | yes | Name of the relation field on the form. Must exist and carry a `relation` definition. |
| body | `relatedSubmissionId` | `string` | yes | |

### `submissionIdRelationsFieldNameDisconnect`

`POST /forms/submission/{id}/relations/{fieldName}/disconnect`

Disconnect relation.

Removes one related submission from a relation field. For single-value relations, the field is cleared. For multi-value relations, omit `relatedSubmissionId` to clear all.

```php theme={null}
$cms->formSubmission->submissionIdRelationsFieldNameDisconnect(id, fieldName, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the source submission. |
| path | `fieldName` | `string` | yes | Name of the relation field on the form. Must exist and carry a `relation` definition. |
| body | `relatedSubmissionId` | `string` | no | |

### `submissionIdRelationsFieldNameReorder`

`POST /forms/submission/{id}/relations/{fieldName}/reorder`

Reorder multi relation.

Reorders the ids of a multi-value relation field. The provided array must contain exactly the same ids currently connected. Only `one-to-many` and `many-to-many` fields support reorder.

```php theme={null}
$cms->formSubmission->submissionIdRelationsFieldNameReorder(id, fieldName, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the source submission. |
| path | `fieldName` | `string` | yes | Name of the relation field on the form. Must exist and carry a `relation` definition. |
| body | `relatedSubmissionIds` | `string[]` | yes | |

### `submissionUpdateId`

`PUT /forms/submission/update/{id}`

Update submission (partial).

Update submission values and/or status. Values are validated against the form definition (same rules as on submission). At least one of `values` or `status` must be present. `status` is the stored numeric enum, not a name: 0 = pending, 1 = approved, 2 = rejected. Changing `status` is restricted to SUPER\_USER, MANAGER, CHIEF\_EDITOR, PUBLISHER and EDITOR; any other role receives 403.

```php theme={null}
$cms->formSubmission->submissionUpdateId(id, body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the submission. |
| body | `status` | `0 / 1 / 2` | no | New status to set. Allowed values: 0 (pending), 1 (approved), 2 (rejected). |
| body | `values` | `object` | no | Partial values to merge; keys must exist as `fields[].name` on the form and pass type validation. |

### `submissionsGetAll`

`GET /forms/submissions/getAll`

Get all submissions for the tenant.

Lists submissions across every form in the calling tenant (not scoped to a single form), newest first, with the owning form's title, slug and type joined onto each row. Requires the `form.submission.read` permission. Callers whose role is `WRITER` (5) are automatically restricted to submissions on forms they created; a WRITER whose identity cannot be resolved is rejected with 401. Soft-deleted submissions are always excluded.

```php theme={null}
$cms->formSubmission->submissionsGetAll(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | Page index (1-based). Defaults to 1. e.g. `1` |
| query | `limit` | `integer` | no | Items per page. Defaults to 10. e.g. `10` |
| query | `status` | `string` | no | Comma-separated list of statuses to include. Unrecognised names are dropped; omit for all. e.g. `pending,approved` |
| query | `lang` | `string` | no | Comma-separated list of language codes, matched against the submission's stored `language`. e.g. `en,fa` |
| query | `from` | `string` | no | Lower bound on `createdAt`. Unparsable values are ignored. e.g. `2025-09-01T00:00:00.000Z` |
| query | `to` | `string` | no | Upper bound on `createdAt`; a date-only value is extended to 23:59:59.999 of that day. Unparsable values are ignored. e.g. `2025-09-30T23:59:59.999Z` |
| query | `formType` | `public / internal` | no | Only include submissions whose parent form has this `formType`. Values outside the FormType enum are ignored. e.g. `internal` |
| query | `formName` | `string` | no | Case-insensitive filter matched against the parent form's `slug` or any of its title values. e.g. `contact` |

### `submissionsRelationsBackfill`

`POST /forms/submissions/relations/backfill`

Backfill normalized submission relations.

Recomputes and persists the normalized `relations` array for existing submissions. Optionally scope to one form.

```php theme={null}
$cms->formSubmission->submissionsRelationsBackfill(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `formId` | `string` | no | |

***

## `form`

Form definitions, captcha configuration, and both submit paths.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`captchaConfigGET`](#form-captchaconfigget) | `GET /forms/captcha/config` | Get tenant captcha config |
| [`captchaConfigPUT`](#form-captchaconfigput) | `PUT /forms/captcha/config` | Upsert tenant captcha config |
| [`captchaConfigSecret`](#form-captchaconfigsecret) | `PATCH /forms/captcha/config/secret` | Rotate tenant captcha secret |
| [`create`](#form-create) | `POST /forms/create` | Create form |
| [`delete`](#form-delete) | `DELETE /forms/delete` | Delete form |
| [`getAll`](#form-getall) | `GET /forms/getAll` | Get all forms |
| [`getById`](#form-getbyid) | `GET /forms/getById/{id}` | Get form by id |
| [`getBySlug`](#form-getbyslug) | `GET /forms/getBySlug/{slug}` | Get form by slug |
| [`getNextById`](#form-getnextbyid) | `GET /forms/getNextById/{id}` | Get next form by current form id |
| [`update`](#form-update) | `PUT /forms/update` | Update form |

### `captchaConfigGET`

`GET /forms/captcha/config`

Get tenant captcha config.

Returns tenant captcha config metadata (masked secret), never returns raw secret.

```php theme={null}
$cms->form->captchaConfigGET()
```

### `captchaConfigPUT`

`PUT /forms/captcha/config`

Upsert tenant captcha config.

Stores or updates CAPTCHA provider settings for the current tenant. Secret key is stored encrypted server-side.

```php theme={null}
$cms->form->captchaConfigPUT(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `provider` | `recaptcha / hcaptcha / turnstile` | yes | |
| body | `siteKey` | `string` | no | |
| body | `secretKey` | `string` | yes | |
| body | `isActive` | `boolean` | no | |

### `captchaConfigSecret`

`PATCH /forms/captcha/config/secret`

Rotate tenant captcha secret.

Rotates only the CAPTCHA secret key for the current tenant, keeping other config fields unchanged.

```php theme={null}
$cms->form->captchaConfigSecret(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `secretKey` | `string` | yes | |

### `create`

`POST /forms/create`

Create form.

Creates a new form with multilingual text fields, sections, and fields. Keys inside multilingual objects must be valid language codes configured in your system (e.g., "en", "fa", ...).

```php theme={null}
$cms->form->create(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `title` | `object` | yes | Map of language code → string (at least one entry required). |
| body | `slug` | `string` | yes | URL-friendly, unique per tenant. Must satisfy the service’s slug rules (lowercase, digits, hyphens; no spaces). |
| body | `description` | `object` | no | Multilingual description (language code → string). |
| body | `submitButtonText` | `object` | no | Multilingual submit button label (language code → string). |
| body | `sections` | `object[]` | yes | Ordered list of sections; each section contains fields. |
| body | `isActive` | `boolean` | no | Whether the form is enabled (default true). |
| body | `formType` | `public / internal` | no | Whether the form is reachable on the public submit route. Defaults to "public"; "internal" forms are only submittable through `/forms/{id}/internal-submit`. |
| body | `captcha` | `object` | no | Per-form CAPTCHA gate. `enabled` defaults to false. `provider` defaults to the service-wide `CAPTCHA_PROVIDER_DEFAULT` (falling back to "recaptcha"). `minScore` (0–1) applies to providers that return a score. |
| body | `notification` | `object` | no | Optional submission notifications. `enabled` defaults to false. `email` and `push` each take `admin` and `submitter` sub-objects whose `enabled` flags default to false. Notifications are fire-and-forget and never block or fail the HTTP response. |
| body | `relation` | `object` | no | Optional relation to other forms for multi-step flows. |

### `delete`

`DELETE /forms/delete`

Delete form.

Deletes a form by ID supplied in the request body.

```php theme={null}
$cms->form->delete(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `id` | `string` | yes | 24-char hex identifier of the form to delete. |

### `getAll`

`GET /forms/getAll`

Get all forms.

Returns a paginated list. Requires auth. Soft-deleted forms are always excluded; forms are returned regardless of `isActive` unless the `status` filter is used. Each row carries a `submissionsCount`.

<Note>Paginated. `page` is 1-based and `limit` caps the page size; the totals come back in the response metadata.</Note>

```php theme={null}
$cms->form->getAll(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | Page index (1-based). e.g. `1` |
| query | `limit` | `integer` | no | Items per page. e.g. `10` |
| query | `formType` | `public / internal` | no | Only include forms with this `formType`. Values outside the FormType enum are ignored. e.g. `public` |
| query | `status` | `active / inactive` | no | Filter on the form's `isActive` flag. Omit to return both. e.g. `active` |

### `getById`

`GET /forms/getById/{id}`

Get form by id.

Requires auth.

```php theme={null}
$cms->form->getById(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-char hex identifier of the form. |

### `getBySlug`

`GET /forms/getBySlug/{slug}`

Get form by slug.

Public endpoint to fetch a form definition by its slug.

```php theme={null}
$cms->form->getBySlug(slug)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `slug` | `string` | yes | Form slug. |

### `getNextById`

`GET /forms/getNextById/{id}`

Get next form by current form id.

Public endpoint for multi-step flows. Returns `nextForm=null` if no next form is configured.

```php theme={null}
$cms->form->getNextById(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Current form id. |

### `update`

`PUT /forms/update`

Update form.

Partial update. Provide only the fields to change. ID is required in the body.

<Note>Takes the whole document in its body, so `id` is a field of that body rather than a separate argument.</Note>

```php theme={null}
$cms->form->update(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `id` | `string` | yes | 24-char hex identifier of the form. |
| body | `title` | `object` | no | |
| body | `slug` | `string` | no | Must satisfy slug rules and remain unique per tenant. |
| body | `description` | `object` | no | |
| body | `submitButtonText` | `object` | no | |
| body | `sections` | `object[]` | no | Sections may include field-level relation definitions identical to the create payload. |
| body | `isActive` | `boolean` | no | |
| body | `formType` | `public / internal` | no | Same values as on create. |
| body | `captcha` | `object` | no | Per-form CAPTCHA gate; same shape as on create. |
| body | `notification` | `object` | no | Submission notification settings; same shape as on create. |
| body | `relation` | `object` | no | |

***

## `language`

The locales a multilingual field can be written in.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`create`](#language-create) | `POST /languages/create` | Create a language |
| [`deleteId`](#language-deleteid) | `DELETE /languages/delete/{id}` | Delete a language (soft delete) |
| [`getAll`](#language-getall) | `GET /languages/getAll` | List languages (paginated) |
| [`getById`](#language-getbyid) | `GET /languages/getById` | Get a language by id |
| [`update`](#language-update) | `PUT /languages/update` | Update a language |

### `create`

`POST /languages/create`

Create a language.

Creates a new language record for the current tenant.

Request body (validated): - `code` (string, required): Short language code. Must be exactly 2 characters (e.g., "en", "es", "fa"). - `name` (string, required): Human-readable language name. Must be a non-empty string (e.g., "English").

Uniqueness: - The pair `(code, tenant)` must be unique. If a language with the same `code` already exists for the current tenant, the request fails with 409 Conflict.

Defaults: - Newly created records are active (`isActive=true`) and not deleted (`isDeleted=false`).

```php theme={null}
$cms->language->create(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `code` | `string` | yes | Short language code (unique per tenant). |
| body | `name` | `string` | yes | Human-readable name. |

### `deleteId`

`DELETE /languages/delete/{id}`

Delete a language (soft delete).

Marks a language as deleted for the current tenant.

Path parameters: - `id` (string, required): The language identifier to delete. Must be a 24-character hexadecimal string.

Behavior: - This is a **soft delete** (the record remains but is flagged as deleted). - Subsequent reads (e.g., list, getById) do not return deleted records.

```php theme={null}
$cms->language->deleteId(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | 24-character hex identifier of the language to delete. |

### `getAll`

`GET /languages/getAll`

List languages (paginated).

Returns a paginated list of languages for the current tenant.

Query parameters: - `page` (integer, optional): 1-based page index. Must be >= 1. Defaults to 1. - `limit` (integer, optional): Items per page. Must be >= 1. Defaults to 10.

Notes: - Only non-deleted records are returned. - Results are tenant-scoped. - `page` and `limit` are used only to compute the `pagination` block; the returned `languages` array is not currently sliced by them.

<Note>Paginated. `page` is 1-based and `limit` caps the page size; the totals come back in the response metadata.</Note>

```php theme={null}
$cms->language->getAll(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | 1-based page index. Defaults to 1. e.g. `1` default `1` |
| query | `limit` | `integer` | no | Items per page. Defaults to 10. e.g. `10` default `10` |

### `getById`

`GET /languages/getById`

Get a language by id.

Fetch a single language for the current tenant using its identifier.

Request body: - `id` (string, required): The language identifier. Must be a 24-character hexadecimal string. Note: despite the endpoint using GET, the identifier is read from the request body, not from the query string.

Behavior: - Returns 404 if the identifier does not match a language owned by the current tenant.

```php theme={null}
$cms->language->getById(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `id` | `string` | yes | 24-character hex identifier of the language. |

### `update`

`PUT /languages/update`

Update a language.

Updates an existing language for the current tenant.

The payload is read from the `bodyData` property of the request body, i.e. the validated fields below are nested inside a `bodyData` object.

Request body (validated): - `id` (string, required): The language identifier to update. - `code` (string, required): Updated language code. Must be exactly 2 characters. - `name` (string, required): Updated language name. Must be a non-empty string.

Notes: - If `id` does not belong to an existing language in the current tenant, returns 404. - Validation failures (including a missing field) return 409.

<Note>Takes the whole document in its body, so `id` is a field of that body rather than a separate argument.</Note>

```php theme={null}
$cms->language->update(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `bodyData` | `object` | yes | |

***

## `report`

Statistics, charts and dashboard configuration.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`aiContentImpact`](#report-aicontentimpact) | `GET /reports/aiContentImpact` | Compare AI-generated and human-authored content performance |
| [`aiUsageBreakdown`](#report-aiusagebreakdown) | `GET /reports/aiUsageBreakdown` | Get AI usage breakdown report for the authenticated tenant |
| [`categoryPerformance`](#report-categoryperformance) | `GET /reports/categoryPerformance` | Get category performance report for the authenticated tenant |
| [`chartContentTrend`](#report-chartcontenttrend) | `GET /reports/chart/contentTrend` | Get content trend chart for the authenticated tenant |
| [`chartEngagementTrend`](#report-chartengagementtrend) | `GET /reports/chart/engagementTrend` | Get engagement trend chart for the authenticated tenant |
| [`chartSubmissionFunnel`](#report-chartsubmissionfunnel) | `GET /reports/chart/submissionFunnel` | Get submission funnel chart for the authenticated tenant |
| [`commentOverview`](#report-commentoverview) | `GET /reports/commentOverview` | Get comment and moderation overview for the authenticated tenant |
| [`contentHealth`](#report-contenthealth) | `GET /reports/contentHealth` | Get content health report for the authenticated tenant |
| [`contentOverview`](#report-contentoverview) | `GET /reports/contentOverview` | Get content overview report for the authenticated tenant |
| [`dashboardConfigGET`](#report-dashboardconfigget) | `GET /reports/dashboardConfig` | Get the dashboard layout preference for the authenticated user |
| [`dashboardConfigPUT`](#report-dashboardconfigput) | `PUT /reports/dashboardConfig` | Save the dashboard layout preference for the authenticated user |
| [`formsOverview`](#report-formsoverview) | `GET /reports/formsOverview` | Get forms overview report for the authenticated tenant |
| [`getAllAuthorsStatistics`](#report-getallauthorsstatistics) | `GET /reports/getAllAuthorsStatistics` | Get statistics of all authors (aggregated) |
| [`getAllUsersStatistics`](#report-getallusersstatistics) | `GET /reports/getAllUsersStatistics` | Get statistics of all users (aggregated) |
| [`getAuthorStatisticsAuthorId`](#report-getauthorstatisticsauthorid) | `GET /reports/getAuthorStatistics/{authorId}` | Get statistics for a specific author |
| [`getStatistics`](#report-getstatistics) | `GET /reports/getStatistics` | Get tenant usage statistics for the authenticated tenant |
| [`getUserStatisticsUserId`](#report-getuserstatisticsuserid) | `GET /reports/getUserStatistics/{userId}` | Get statistics for a specific user (secure) |
| [`periodComparison`](#report-periodcomparison) | `GET /reports/periodComparison` | Compare the current report period against the previous equal-length period |
| [`publishingPerformance`](#report-publishingperformance) | `GET /reports/publishingPerformance` | Get publishing performance report for the authenticated tenant |
| [`submissionsOverview`](#report-submissionsoverview) | `GET /reports/submissionsOverview` | Get submissions overview report for the authenticated tenant |
| [`topArticles`](#report-toparticles) | `GET /reports/topArticles` | Get top articles report for the authenticated tenant |

### `aiContentImpact`

`GET /reports/aiContentImpact`

Compare AI-generated and human-authored content performance.

```php theme={null}
$cms->report->aiContentImpact(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `limit` | `integer` | no | |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `aiUsageBreakdown`

`GET /reports/aiUsageBreakdown`

Get AI usage breakdown report for the authenticated tenant.

```php theme={null}
$cms->report->aiUsageBreakdown(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `categoryPerformance`

`GET /reports/categoryPerformance`

Get category performance report for the authenticated tenant.

```php theme={null}
$cms->report->categoryPerformance(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `limit` | `integer` | no | |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `chartContentTrend`

`GET /reports/chart/contentTrend`

Get content trend chart for the authenticated tenant.

```php theme={null}
$cms->report->chartContentTrend(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `groupBy` | `day / week / month` | no | |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `chartEngagementTrend`

`GET /reports/chart/engagementTrend`

Get engagement trend chart for the authenticated tenant.

```php theme={null}
$cms->report->chartEngagementTrend(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `groupBy` | `day / week / month` | no | |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `chartSubmissionFunnel`

`GET /reports/chart/submissionFunnel`

Get submission funnel chart for the authenticated tenant.

```php theme={null}
$cms->report->chartSubmissionFunnel(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `commentOverview`

`GET /reports/commentOverview`

Get comment and moderation overview for the authenticated tenant.

```php theme={null}
$cms->report->commentOverview(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `limit` | `integer` | no | |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `contentHealth`

`GET /reports/contentHealth`

Get content health report for the authenticated tenant.

```php theme={null}
$cms->report->contentHealth(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `limit` | `integer` | no | |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `contentOverview`

`GET /reports/contentOverview`

Get content overview report for the authenticated tenant.

```php theme={null}
$cms->report->contentOverview(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `dashboardConfigGET`

`GET /reports/dashboardConfig`

Get the dashboard layout preference for the authenticated user.

Returns the stored dashboard configuration for the tenant and user resolved from the request context. If the user has never saved a configuration, the built-in default is returned instead of a 404.

```php theme={null}
$cms->report->dashboardConfigGET()
```

### `dashboardConfigPUT`

`PUT /reports/dashboardConfig`

Save the dashboard layout preference for the authenticated user.

Upserts the dashboard configuration for the tenant and user resolved from the request context and returns the normalized result. The whole body is optional — an empty body, a body with no `slots`, or an empty `slots` array all fall back to the built-in default configuration. The response is always the fully normalized config, so unknown widget types, spans and ranges are replaced by their defaults. The authenticated user id is stored as `updatedBy`.

```php theme={null}
$cms->report->dashboardConfigPUT(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `version` | `number` | no | Config schema version. Default 1. |
| body | `templateId` | `string` | no | Layout template identifier. Default "executive\_focus". |
| body | `slots` | `object[]` | no | Widget slots in display order. An empty array is treated as absent and the default slot list is used instead. |

### `formsOverview`

`GET /reports/formsOverview`

Get forms overview report for the authenticated tenant.

```php theme={null}
$cms->report->formsOverview(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `getAllAuthorsStatistics`

`GET /reports/getAllAuthorsStatistics`

Get statistics of all authors (aggregated).

```php theme={null}
$cms->report->getAllAuthorsStatistics(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `50` |
| query | `sortBy` | `count / date` | no | Sort by aggregate metric e.g. `count` |
| query | `order` | `asc / desc` | no | e.g. `desc` |
| query | `from` | `string` | no | e.g. `2025-10-01T00:00:00Z` |
| query | `to` | `string` | no | e.g. `2025-10-31T23:59:59Z` |

### `getAllUsersStatistics`

`GET /reports/getAllUsersStatistics`

Get statistics of all users (aggregated).

```php theme={null}
$cms->report->getAllUsersStatistics(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `50` |
| query | `sortBy` | `count / date` | no | Sort by aggregate metric e.g. `count` |
| query | `order` | `asc / desc` | no | e.g. `desc` |
| query | `from` | `string` | no | e.g. `2025-10-01T00:00:00Z` |
| query | `to` | `string` | no | e.g. `2025-10-31T23:59:59Z` |

### `getAuthorStatisticsAuthorId`

`GET /reports/getAuthorStatistics/{authorId}`

Get statistics for a specific author.

```php theme={null}
$cms->report->getAuthorStatisticsAuthorId(authorId, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `authorId` | `string` | yes | Taken from the route. |
| query | `from` | `string` | no | e.g. `2025-10-01T00:00:00Z` |
| query | `to` | `string` | no | e.g. `2025-10-31T23:59:59Z` |

### `getStatistics`

`GET /reports/getStatistics`

Get tenant usage statistics for the authenticated tenant.

```php theme={null}
$cms->report->getStatistics()
```

### `getUserStatisticsUserId`

`GET /reports/getUserStatistics/{userId}`

Get statistics for a specific user (secure).

```php theme={null}
$cms->report->getUserStatisticsUserId(userId, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `userId` | `string` | yes | Taken from the route. |
| query | `from` | `string` | no | e.g. `2025-10-01T00:00:00Z` |
| query | `to` | `string` | no | e.g. `2025-10-31T23:59:59Z` |

### `periodComparison`

`GET /reports/periodComparison`

Compare the current report period against the previous equal-length period.

```php theme={null}
$cms->report->periodComparison(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `publishingPerformance`

`GET /reports/publishingPerformance`

Get publishing performance report for the authenticated tenant.

```php theme={null}
$cms->report->publishingPerformance(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `limit` | `integer` | no | |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `submissionsOverview`

`GET /reports/submissionsOverview`

Get submissions overview report for the authenticated tenant.

```php theme={null}
$cms->report->submissionsOverview(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

### `topArticles`

`GET /reports/topArticles`

Get top articles report for the authenticated tenant.

```php theme={null}
$cms->report->topArticles(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `metric` | `views / engagement / publishDate` | no | |
| query | `limit` | `integer` | no | |
| query | `from` | `string` | no | |
| query | `to` | `string` | no | |

***

## `subcategory`

The optional second level under a category.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`create`](#subcategory-create) | `POST /subcategories/create` | Create subCategory |
| [`deleteId`](#subcategory-deleteid) | `DELETE /subcategories/delete/{id}` | Delete subCategory |
| [`getAll`](#subcategory-getall) | `GET /subcategories/getAll` | Get all subCategories (public) |
| [`getByCategoryId`](#subcategory-getbycategoryid) | `GET /subcategories/getByCategoryId/{categoryId}` | Get subCategories by parent category id (public) |
| [`getById`](#subcategory-getbyid) | `GET /subcategories/getById/{id}` | Get subCategory by id (public) |
| [`getBySlug`](#subcategory-getbyslug) | `GET /subcategories/getBySlug/{slug}` | Get subCategory by slug (public) |
| [`update`](#subcategory-update) | `PUT /subcategories/update` | Update subCategory |

### `create`

`POST /subcategories/create`

Create subCategory.

```php theme={null}
$cms->subcategory->create(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `name` | `object` | yes | Subcategory name in multiple languages |
| body | `slug` | `string` | no | URL-friendly unique identifier |
| body | `categoryId` | `string` | yes | Parent category ID |
| body | `description` | `object` | no | Optional description in multiple languages |
| body | `icon` | `string` | no | Optional icon URL |
| body | `banner` | `string` | no | Optional banner URL |
| body | `order` | `integer` | no | Optional ordering index |
| body | `isActive` | `boolean` | no | Whether subCategory is active |

### `deleteId`

`DELETE /subcategories/delete/{id}`

Delete subCategory.

```php theme={null}
$cms->subcategory->deleteId(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `getAll`

`GET /subcategories/getAll`

Get all subCategories (public).

<Note>Paginated. `page` is 1-based and `limit` caps the page size; the totals come back in the response metadata.</Note>

```php theme={null}
$cms->subcategory->getAll(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `50` |
| query | `keyword` | `string` | no | e.g. `ai` |
| query | `categoryId` | `string` | no | Filter by parent category e.g. `{{objectId}}` |
| query | `includeInactive` | `boolean` | no | e.g. `False` |
| query | `sortBy` | `order / createdAt` | no | e.g. `order` |
| query | `sortOrder` | `asc / desc` | no | e.g. `asc` |

### `getByCategoryId`

`GET /subcategories/getByCategoryId/{categoryId}`

Get subCategories by parent category id (public).

```php theme={null}
$cms->subcategory->getByCategoryId(categoryId, filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `categoryId` | `string` | yes | Taken from the route. |
| query | `page` | `integer` | no | e.g. `1` |
| query | `limit` | `integer` | no | e.g. `50` |

### `getById`

`GET /subcategories/getById/{id}`

Get subCategory by id (public).

```php theme={null}
$cms->subcategory->getById(id)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `id` | `string` | yes | Taken from the route. |

### `getBySlug`

`GET /subcategories/getBySlug/{slug}`

Get subCategory by slug (public).

```php theme={null}
$cms->subcategory->getBySlug(slug)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| path | `slug` | `string` | yes | Taken from the route. |

### `update`

`PUT /subcategories/update`

Update subCategory.

Partial update. Provide only fields to change, plus `id`.

<Note>Takes the whole document in its body, so `id` is a field of that body rather than a separate argument.</Note>

```php theme={null}
$cms->subcategory->update(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `id` | `string` | yes | |
| body | `name` | `object` | no | |
| body | `slug` | `string` | no | |
| body | `categoryId` | `string` | no | |
| body | `description` | `object` | no | |
| body | `icon` | `string` | no | |
| body | `banner` | `string` | no | |
| body | `order` | `integer` | no | |
| body | `isActive` | `boolean` | no | |

***

## `tag`

Free-form labels attached to articles.

| **Method** | **Route** | **What it does** |
| - | - | - |
| [`create`](#tag-create) | `POST /tags/create` | Create a new tag |
| [`getAll`](#tag-getall) | `GET /tags/getAll` | Get all tags for current tenant |
| [`search`](#tag-search) | `GET /tags/search` | Search tags |

### `create`

`POST /tags/create`

Create a new tag.

Manually create a new tag

```php theme={null}
$cms->tag->create(body)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| body | `name` | `string` | yes | Tag name. Stored trimmed and lowercased; an existing tag with the same normalized name is returned instead of creating a duplicate. |

### `getAll`

`GET /tags/getAll`

Get all tags for current tenant.

Returns a paginated list of all tags for your tenant. No user authentication is required.

<Note>Paginated. `page` is 1-based and `limit` caps the page size; the totals come back in the response metadata.</Note>

```php theme={null}
$cms->tag->getAll(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `page` | `integer` | no | Page number e.g. `1` default `1` |
| query | `limit` | `integer` | no | Items per page e.g. `100` default `100` |

### `search`

`GET /tags/search`

Search tags.

Case-insensitive partial name match against the tags of your tenant. No user authentication is required.

```php theme={null}
$cms->tag->search(filters)
```

| **In** | **Name** | **Type** | **Required** | **Description** |
| - | - | - | - | - |
| query | `q` | `string` | yes | Search term (used as a regular expression) e.g. `ai` |
| query | `limit` | `integer` | no | Maximum number of results e.g. `20` default `20` |
