---
url: 'https://dxpr.com/hc/documentation/learn-dxpr-builder/reference/api-endpoints'
title: 'API endpoints | DXPR'
author:
  name: docs-sync
  url: 'https://dxpr.com/users/docs-sync'
date: '2026-10-23T14:31:17+00:00'
copyright: 'Copyright DXPR. All rights reserved.'
type: article
summary: 'Every route the module registers, with methods, access and parameters'
tags:
  - 'Configuration and API'
image: 'https://dxpr.com/themes/custom/dxpr_com/images/dxpr-share-card.jpg'
published: true
og:
  site_name: DXPR
  updated_time: '2026-10-23T14:31:17+0000'
  'image:alt': 'DXPR, powered by Drupal: AI-powered Drupal for marketers.'
schema:
  '@context': 'https://schema.org'
  '@graph':
    -
      mainEntity: {  }
    -
      '@type': TechArticle
      headline: 'API endpoints'
      description: 'Every route the module registers, with methods, access and parameters'
      image:
        '@type': ImageObject
        url: 'https://dxpr.com/themes/custom/dxpr_com/images/dxpr-share-card.jpg'
        width: '1200'
        height: '630'
      datePublished: '2026-10-23T14:31:17+0000'
      dateModified: '2026-10-23T14:31:17+0000'
      author:
        '@type': Organization
        '@id': 'https://dxpr.com/#organization'
        name: DXPR
        url: 'https://dxpr.com/'
        logo:
          '@type': ImageObject
          url: 'https://dxpr.com/themes/custom/dxpr_com/dxpr-logo-dark.svg'
          width: '1348'
          height: '501'
      publisher:
        '@type': Organization
        '@id': 'https://dxpr.com/#organization'
        name: DXPR
        url: 'https://dxpr.com/'
        logo:
          '@type': ImageObject
          url: 'https://dxpr.com/themes/custom/dxpr_com/dxpr-logo-dark.svg'
          width: '1348'
          height: '501'
      mainEntityOfPage: 'https://dxpr.com/hc/documentation/learn-dxpr-builder/reference/api-endpoints'
      articleSection: 'Configuration and API'
    -
      '@type': WebPage
      '@id': 'https://dxpr.com/hc/documentation/learn-dxpr-builder/reference/api-endpoints'
      description: 'Every route the module registers, with methods, access and parameters'
      breadcrumb:
        '@type': BreadcrumbList
        itemListElement:
          -
            '@type': ListItem
            position: 1
            name: Home
            item: 'https://dxpr.com/'
      publisher:
        '@type': Organization
        '@id': 'https://dxpr.com/#organization'
        name: DXPR
      inLanguage: en
---
[Help Center](/hc)/[Documentation](/hc/documentation) /[Learn DXPR Builder](/hc/documentation/learn-dxpr-builder) /[Reference](/hc/documentation/learn-dxpr-builder/reference) /[Configuration and API](/hc/documentation/learn-dxpr-builder/reference/config-and-api) # API endpoints.

Every route the module registers, with methods, access and parameters

 Learn DXPR BuilderReferenceConfiguration and API

   ## In this guide

 Related guides

- [Configuration reference](/hc/documentation/learn-dxpr-builder/reference/config-reference)
- [API reference](/hc/documentation/learn-dxpr-builder/development/api)
- [Hooks API](/hc/documentation/learn-dxpr-builder/development/hooks-api)

This page lists every route that DXPR Builder registers in `dxpr_builder.routing.yml`, with the HTTP methods, access requirements, request parameters and response shapes as implemented in the controllers. The editor's JavaScript is the intended client for the AJAX endpoints; nothing on this page is a stable public API, and parameter names can change between releases.

## Access requirements

Routes use three kinds of requirement. A route can combine them.

| Requirement | Meaning |
|---|---|
| `_permission` | The user needs the named Drupal permission. DXPR Builder defines `administer dxpr builder configuration`, `edit with dxpr builder` and `administer dxpr_builder_profile` in `dxpr_builder.permissions.yml`; `view media` and `administer site configuration` come from core. |
| `_csrf_token` | The request must carry a valid CSRF token in the `token` query parameter. For the main AJAX handler, the editor fetches a URL with the token from `/dxpr_builder/csrf` (or from the `dxpr_builder_csrf` operation). For the other routes, the field formatter puts URLs with a token in `drupalSettings.dxprBuilder`, for example `fileUploadUrl`, `entityLinkSuggestionsUrl`, `userSettingsUrl` and `aiAgent.endpointUrl`. |
| `_dxpr_builder_billable_user` | The user must be a billable user, as defined below. |

### What "billable user" means

`DxprBuilderBillableUserAccessCheck` allows access when `DxprBuilderLicenseService::isBillableUser()` returns true for the current account. That method returns false for blocked accounts and for accounts whose `dxpr_user_is_disavowed` field is set (an administrator excludes a user from editing on the User Licenses page). Otherwise it returns true for user 1 and for any account that holds the `edit with dxpr builder` permission. In short: a billable user is an active, non-excluded account that is allowed to edit with DXPR Builder, and the licence service counts these accounts against your product key.

### HTTP methods

Where a route declares `methods`, only those methods are routed. Where it does not, the route accepts any method (Drupal's `MethodFilter` only filters routes that declare methods). The tables below state the method the editor's JavaScript actually uses when that is visible in the source.

## Editor AJAX endpoints

### CSRF token URL

|  |  |
|---|---|
| Path | `/dxpr_builder/csrf` |
| Route | `dxpr_builder.csrf_refresh` |
| Methods | Not declared (any method). The editor sends GET (`backend-connect.js`, `fetchCsrfTokenUrl()`). |
| Requirements | Billable user |
| Controller | `AjaxController::ajaxRefresh()` |

The request must carry the header `X-Requested-With: XMLHttpRequest`; without it the controller returns HTTP 403 with the JSON body `{"error": "Invalid request"}`.

| Parameter | Source | Required | Notes |
|---|---|---|---|
| `enterprise` | Query string | Optional | When set to `true` and the `dxpr_builder_e` module is installed, the token is generated for the `dxpr_builder_e.ajax_callback` route instead. When set to `true` without that module the controller throws an exception. |

Response: a JSON string with the URL of the main AJAX handler and a `token` query parameter. The URL is a path from the site root (base path included, no host). The JavaScript posts every builder operation to this URL.

### Main AJAX handler

|  |  |
|---|---|
| Path | `/dxpr_builder/ajax` |
| Route | `dxpr_builder.ajax_callback` |
| Methods | Not declared (any method). The controller reads every parameter from the POST body, and the editor sends POST. |
| Requirements | Billable user, CSRF token |
| Controller | `AjaxController::ajaxCallback()` |

The POST body field `action` selects the operation. Every operation and its parameters are listed under [Main AJAX handler operations](#main-ajax-handler-operations). When `action` is missing or unknown, the controller returns an empty Drupal `AjaxResponse` with no commands. Most operations that re-check the billable user status return the same empty response when that check fails; the tables note the two exceptions.

### AI chat

|  |  |
|---|---|
| Path | `/dxpr_builder/ajax/ai/chat` |
| Route | `dxpr_builder.ai_chat` |
| Methods | POST |
| Requirements | `edit with dxpr builder`, billable user, CSRF token |
| Controller | `AiChatController::chat()` |

The request body is JSON. The controller proxies the request to the `dxpr` provider of the Drupal `ai` module, so no API key reaches the browser. Before the call it merges the domains from the `ai_output_allowed_domains` setting into the `ai` module's hostname filter, when that service exists. A value of `*` sets the filter to full trust instead. The filter is restored afterwards.

| Parameter | Source | Required | Notes |
|---|---|---|---|
| `messages` | JSON body | Required | Array of objects with `role` and `content`. |
| `stream` | JSON body | Optional | Boolean, default `true`. |
| `model` | JSON body | Optional | Default `kavya-m1`. |
| `prediction` | JSON body | Optional | Passed to the provider configuration as an array. |
| `providers` | JSON body | Optional | Passed through to the provider configuration. |
| `allowed_html_tags` | JSON body | Optional | Passed through to the provider configuration. |
| `allowed_html_classes` | JSON body | Optional | Passed through to the provider configuration. |
| `web_search` | JSON body | Optional | Only the value `false` is forwarded; any other value is ignored. |
| `response_format` | JSON body | Optional | Passed to the provider configuration as an array. |

Responses:

| Case | Status | Body |
|---|---|---|
| Streamed (`stream` true and the provider streams) | 200, `Content-Type: text/event-stream` | Server-sent events. Each event is `data: <JSON>` where the JSON has `choices[0].delta.content` and, when the provider supplies them, `id`, `object`, `created`, `model`, `original_model` and `usage`. The stream ends with `data: [DONE]`. |
| Non-streamed | 200, `application/json` | `{"choices": [{"message": {"role": "assistant", "content": "<text>"}}]}` |
| Body is not JSON, or `messages` is missing or empty | 400, plain text | `Invalid request: messages required` |
| `model` not in the `allowed_models` list of `ai_provider_dxpr.settings` (only checked when the `ai_provider_dxpr` module is installed and the list is set) | 403, `application/json` | `{"error": {"code": "model_not_allowed", "message": "The selected model is not allowed. Please refresh the page to update your model options."}}` |
| Provider exception | 500, `application/json` | `{"error": {"message": "The request could not be completed."}}` |

### AI image generation

|  |  |
|---|---|
| Path | `/dxpr_builder/ajax/ai/image` |
| Route | `dxpr_builder.ai_image` |
| Methods | POST |
| Requirements | `edit with dxpr builder`, billable user, CSRF token |
| Controller | `AiImageController::generate()` |

| Parameter | Source | Required | Notes |
|---|---|---|---|
| `prompt` | JSON body | Required | Text prompt. |
| `model` | JSON body | Optional | Default `kavya-image`. |
| `size` | JSON body | Optional | Forwarded to the provider configuration when non-empty. |
| `quality` | JSON body | Optional | Forwarded to the provider configuration when non-empty. |
| `background` | JSON body | Optional | Forwarded to the provider configuration when non-empty. |

Responses:

| Case | Status | Body |
|---|---|---|
| Success | 200 | `{"data": [{"b64_json": "<base64 image>"}]}` (only the first generated image is returned) |
| Body is not JSON, or `prompt` is missing | 400 | `{"error": {"message": "Invalid request: prompt required"}}` |
| Provider returned no image | 500 | `{"error": {"message": "No image was generated"}}` |
| Provider exception | 500 | `{"error": {"message": "Image generation failed. Please try again."}}` |

### AI image editing

|  |  |
|---|---|
| Path | `/dxpr_builder/ajax/ai/image/edit` |
| Route | `dxpr_builder.ai_image_edit` |
| Methods | POST |
| Requirements | `edit with dxpr builder`, billable user, CSRF token |
| Controller | `AiImageController::edit()` |

| Parameter | Source | Required | Notes |
|---|---|---|---|
| `prompt` | JSON body | Required | Editing instruction. |
| `image` | JSON body | Required | Base64-encoded source image (strict decoding). |
| `mimeType` | JSON body | Optional | Default `image/png`; the subtype becomes the file extension passed to the provider. |
| `model` | JSON body | Optional | Default `kavya-image`. |
| `size`, `quality`, `background` | JSON body | Optional | Forwarded to the provider configuration when non-empty. |

Responses:

| Case | Status | Body |
|---|---|---|
| Success | 200 | `{"data": [{"b64_json": "<base64 image>"}]}` |
| Body is not JSON, or `prompt` or `image` is missing | 400 | `{"error": {"message": "Invalid request: prompt and image required"}}` |
| `image` is not valid base64 | 400 | `{"error": {"message": "Invalid image data: base64 decode failed"}}` |
| Provider returned no image | 500 | `{"error": {"message": "No image was generated"}}` |
| Provider exception | 500 | `{"error": {"message": "Image editing failed. Please try again."}}` |

### File upload

|  |  |
|---|---|
| Path | `/dxpr_builder/ajax/file_upload` |
| Route | `dxpr_builder.ajax_file_upload_callback` |
| Methods | POST, DELETE (declared in routing; the controller has a single upload handler and no separate delete branch) |
| Requirements | Billable user, CSRF token |
| Controller | `UploadFileController::fileUpload()` |

| Parameter | Source | Required | Notes |
|---|---|---|---|
| `upload` | Multipart form files, sent as `upload[]` | Required | One or more uploaded files. |

The controller sorts each file by the first part of its client MIME type into `<default scheme>://dxpr_builder_images`, `dxpr_builder_videos` or `dxpr_builder_files`. An image needs an extension of the MIME types `image/gif`, `jpg`, `jpeg`, `png`, `webp`, `avif` or `svg`. A video needs one of `video/webm`, `ogv`, `ogg`, `mp4` or `quicktime` (for example `.mov`). The size limit is PHP's upload maximum. A file with the same name and identical SHA-256 content as an existing managed file that the user can view is reused instead of saved again. When the `media` module is installed and a media type named `image`, `media_image`, `video` or `media_video` exists, a published media entity is created (or reused) for the file. File usage is recorded under the `dxpr_builder` module.

Responses:

| Case | Status | Body |
|---|---|---|
| Success | 201 | JSON array, one object per file: `url` (absolute file URL), `uuid` and `id` (of the media entity when one exists, otherwise of the file), `fid` (file ID), `entity_type` (`media` or `file`). |
| No `upload` files | 400 | `{"message": "No files were uploaded."}` |
| Invalid upload, unwritable destination or save failure | 500 | `{"message": "<reason>"}` |
| Validation failure (extension or size) | 422 | `{"message": "<violation messages>"}` |
| Filename already locked by another request | 503 | `{"message": "File \"<name>\" is already locked for writing."}` |

### Media library dialog

|  |  |
|---|---|
| Path | `/dxpr_builder/media-library` |
| Route | `dxpr_builder.media_library` |
| Methods | Not declared (any method). Parameters are read from the query string. |
| Requirements | `view media`; admin route |
| Controller | `AjaxController::openImageMediaLibrary()` |

| Parameter | Source | Required | Notes |
|---|---|---|---|
| `media_type` | Query string | Optional | `image` (default) or `video`. |
| `remaining_slots` | Query string | Optional | Number of items the opener accepts; defaults to 1. |
| `uuid` | Query string | Optional | Passed to the opener as `opener_parameters.uuid`. |

The media type offered depends on the `media_browser` setting: `media_library` uses the `image` or `video` media type directly; `media_library_acquia_dam` uses `acquia_dam_image_asset` or `acquia_dam_video_asset` when that media type exists.

Responses:

| Case | Result |
|---|---|
| Success | A render array built by `MediaLibraryUiBuilder::buildUi()` with the opener `media_library.opener.dxpr_builder`. Drupal renders it as an HTML page intended for an iframe. |
| `media_library` module not installed | 403, plain text `Media Library is not enabled. Please enable it to access this feature.` |
| Unsupported `media_type` | 400, plain text `Unsupported media type "<value>".` |
| `media_browser` setting has no usable media type | 400, plain text `The "<setting>" settings is not supported.` |

### Help link validator

|  |  |
|---|---|
| Path | `/dxpr_builder/ajax/help_link_validator` |
| Route | `dxpr_builder.ajax_help_link_validator` |
| Methods | Not declared (any method). The dev script `dxpr_builder_dev.js` sends POST with a form-encoded body. |
| Requirements | Billable user |
| Controller | `AjaxController::validateHelpLink()` |

| Parameter | Source | Required | Notes |
|---|---|---|---|
| `help_link` | POST body | Required for a meaningful answer | Only `http` or `https` URLs on host `dxpr.com` with a path starting `/documentation/` are checked. |

Response: a JSON array. `[true]` when a HEAD request to the link (without following redirects) returns 200; `[false]` when the HTTP client throws; `[]` in every other case, including a missing parameter, a non-matching URL or a non-200 status.

### Entity link suggestions

|  |  |
|---|---|
| Path | `/dxpr_builder/entity-link-suggestions` |
| Route | `dxpr_builder.entity_link_suggestions` |
| Methods | GET |
| Requirements | `edit with dxpr builder`, CSRF token; `no_cache` route option |
| Controller | `EntityLinkSuggestionsController::suggestions()` |

| Parameter | Source | Required | Notes |
|---|---|---|---|
| `q` | Query string | Required for results | Search text, lower-cased. A value like `/node/123` loads that entity directly. |
| `langcode` | Query string | Optional | Language of the host entity; defaults to the current language. Only entities available in this language are returned. |

Bundles are those with `ckeditor5_link_suggestions` enabled in their bundle info; when none are configured, all node bundles are searched. Up to 100 entities per entity type are matched with a `CONTAINS` search through core's entity reference selection plugins.

Response: `{"suggestions": [...]}` with private cacheability and a max age of 300 seconds. Each suggestion has `description`, `entity_type_id`, `entity_uuid`, `group`, `label` and `path`. When `q` matches nothing, the array holds a single manual-link suggestion with `description`, `label` (the lower-cased input) and `path` (the input when it is a valid URL, otherwise an empty string). When `q` is empty, `suggestions` is an empty array.

### User editor settings

|  |  |
|---|---|
| Path | `/dxpr_builder/user-settings` |
| Route | `dxpr_builder.user_settings` |
| Methods | POST |
| Requirements | `edit with dxpr builder`, CSRF token; `no_cache` route option |
| Controller | `UserSettingsController::save()` |

The editor sends this request when a user changes their own editor settings, for example under **Editor settings**. The values are stored with the user account in `user.data`, module `dxpr_builder`.

| Parameter | Source | Required | Notes |
|---|---|---|---|
| `controls_layout` | JSON body | Required (the only setting) | `element`, `breadcrumb` or `default`. `default` removes the user's choice, so the profile or site setting applies. |

Responses:

| Case | Status | Body |
|---|---|---|
| Saved | 200 | `{"settings": {"controls_layout": "<value>"}}`: every setting after saving, `default` where the user has no choice stored. |
| Body is not a non-empty JSON object, or a name or value is invalid | 400 | `{"error": "Invalid setting or value."}`. Nothing is stored. |

## Main AJAX handler operations

All operations go to `/dxpr_builder/ajax` with the token query parameter obtained from `/dxpr_builder/csrf`. The operation name is sent in the POST body field `action`; all other parameters are POST body fields unless stated otherwise. The "Re-checks billable" column marks operations that call `isBillableUser()` again inside the switch. When that check fails they return the empty `AjaxResponse` described above, unless the row says otherwise.

### Session and access

| Operation | Parameters | Re-checks billable | Returns |
|---|---|---|---|
| `dxpr_login` | None | No | JSON boolean: whether the current user is billable. |
| `dxpr_builder_csrf` | None | Yes | JSON string: the handler URL with a `token` query parameter (same as `/dxpr_builder/csrf` without the `enterprise` option). |

### Containers (fields rendered with the DXPR Builder formatter)

| Operation | Parameters | Re-checks billable | Returns |
|---|---|---|---|
| `dxpr_get_container_types` | None | Yes | JSON object keyed `entity_type\|bundle` with value `entity_type - bundle`, one entry per bundle whose default view display uses the `dxpr_builder_text` formatter on at least one field. |
| `dxpr_get_container_names` | `container_type` (required, `entity_type\|bundle`) | Yes (returns an empty JSON string instead of the empty response) | JSON object keyed `entity_id\|field_name` (or `entity_id\|field_name\|langcode` for translatable entities) with value `entity label\|field label`. When `container_type` is missing, or the billable check fails, the response is an empty JSON string. |
| `dxpr_save_container` | `type` (required, `entity_type\|bundle`); `name` (required, `entity_id\|field_name` or `entity_id\|revision_id\|field_name`); `htmlContent` (required, encoded markup); `lang` (optional, defaults to the site default language) | Yes | JSON empty string on success. HTTP 400 with a JSON message when `htmlContent` is not a string or the language does not apply to a non-translatable entity; 404 when the requested translation does not exist; 500 when the entity cannot be loaded. |

`dxpr_save_container` does the following:

1. Decodes `htmlContent`: XOR every byte with 7, then URL-decode.
2. Writes the result to the first delta of the named field.
3. Creates a new default revision with the log message `Saved with DXPR builder` when the entity type is revisionable.
4. Removes any content lock for the entity.
5. Updates `block_revision_id` in Layout Builder sections that reference the saved inline block.
6. Increments the `dxpr_builder.saves_count` state value.

The save only happens when the user has `update` access to the entity. Without it, the operation still returns the empty-string success response.

### CMS elements (blocks and views)

| Operation | Parameters | Re-checks billable | Returns |
|---|---|---|---|
| `dxpr_builder_get_cms_element_names` | None | Yes | JSON object from `DxprBuilderService::getCmsElementNames()` with keys `list`, `disallowed` and `blockCategories`. |
| `dxpr_get_cms_element_settings` | `name` (required): `view-<view_id>-<display_id>` or `block-<plugin_id>`; `ajax_page_state[libraries]` (optional, comma-separated libraries already loaded) | Yes | JSON object with `css`, `js`, `settings` and `data`. For a view, `data` is the rendered exposed filter widgets; for a block, `data` is the block's configuration form wrapped in `<form>` tags, with the `provider`, `admin_label`, `label`, `label_display` and `context_mapping` elements removed and all details elements opened. |
| `dxpr_builder_load_cms_element` | `name` (required, parsed by `parseStringForCmsElementInfo()`); `settings` (required); `data` (required, array); `ajax_page_state[libraries]` (optional) | No | JSON object with `css`, `js`, `settings` and `data`, where `data` is the rendered block or view display. |

In both `settings`-returning operations, `css` and `js` are rendered asset tags not already present in the page, and `settings` is the decoded `drupalSettings` object, or an empty string when there are none.

### Page templates

| Operation | Parameters | Re-checks billable | Returns |
|---|---|---|---|
| `dxpr_get_page_templates` | None | Yes | JSON array of enabled page templates, each with `title`, `uuid`, `module` (always `null`: the entity does not store it), `category`, `date`, `weight` (integer) and `image` (a `data:` URL, or the module's `not-found.png` URL). |
| `dxpr_load_page_template` | `uuid` (required) | Yes | HTML response containing the template markup with base tokens replaced; empty when no enabled template matches. |

### User templates

| Operation | Parameters | Re-checks billable | Returns |
|---|---|---|---|
| `dxpr_get_templates` | None | Yes | JSON array of templates owned by the current user or marked global; templates whose author account no longer exists are left out. Each entry has `id`, `uuid`, `name`, `global`, `current_user_is_author`, `author_id`, `author_name`, `type` and, when stored, `image` (a `data:` URL). |
| `dxpr_load_template` | `uuid` (required) | Yes | HTML response containing the template markup with base tokens replaced; empty when no enabled template matches. |
| `dxpr_save_template` | `name` (required); `template` (required, markup); `type` (optional, defaults to `custom`); `global` (optional, cast to boolean); `image` (optional: a base64 string or `data:image` URL in the POST body, or a multipart file field named `image`) | Yes | JSON object with `message` and `code`. `code` 200 on success; `code` 409 (HTTP status 200) when a template with the same machine name exists; `code` 400 with HTTP 400 when the image is not valid image data. |
| `dxpr_delete_template` | `name` (required) | Yes | JSON empty string. |

The machine name is derived from `name` by lower-casing it and replacing every run of characters outside `a-z0-9_` with an underscore. `type` falls back to the `data-azb` attribute in the markup when a stored template has no type.

### Images

| Operation | Parameters | Re-checks billable | Returns |
|---|---|---|---|
| `dxpr_builder_get_image_urls` | `entityIDs` (required, array); `imageStyle` (required; `original` for the unstyled file); `entityType` (required; `media` resolves media items, any other value is used as the entity type of the IDs) | Yes | Text body: comma-separated image URLs. Files give a root-relative URL with a `fid` query parameter. Media items of the `image` or `video` type resolve to their source file. `acquia_dam_image_asset` items give the DAM URL with `mid`, `acquiaDamAsset` and `imageStyle` added. Other media items are skipped. HTTP 204 with no body when `entityIDs` is empty, or when no media item can be resolved. |
| `dxpr_builder_get_image_style_url` | `imageStyle` (required); `entityId` (required); `entityTypeId` (required, `file` or `media`) | Yes | Text body. For a file: its root-relative URL through the image style (SVG files and `original` skip the style), with a `fid` query parameter. For an `acquia_dam_image_asset` media item: the DAM URL. HTTP 204 with no body for any other entity, or when the entity is not found. |
| `dxpr_builder_get_image_metadata` | `entityId` (required); `entityType` (required, `file` or `media`) | Yes (returns 403 JSON instead of the empty response) | `{"data": {"alt": "...", "title": "..."}}` from the media item's image source field (an Acquia DAM asset gives its DAM alt text and an empty title). For a file, the values come from the first media item whose `field_media_image` references it. `data` is an empty array when the entity is neither a file nor a media item, or when no media references the file. Errors: 400 `{"error": "Missing required parameters"}`, 404 `{"error": "Entity not found"}`, 500 `{"error": "Failed to load entity"}`, 403 `{"error": "Access denied"}`. |

### Content locks

| Operation | Parameters | Re-checks billable | Returns |
|---|---|---|---|
| `dxpr_content_lock_status` | `entity_id`, `revision_id`, `entity_type`, `langcode` (all required) | No | JSON object with `status` (boolean) and `label` (entity label); when locked, also `entity_lock_author_id` and `entity_lock_author_name`. |
| `dxpr_toggle_content_lock` | `entity_id`, `revision_id`, `entity_type`, `langcode`, `toggle_action` (all required; `lock` creates the lock, any other value removes it) | No | JSON object with `label` and `status` (`true` after locking, `false` after unlocking). An empty JSON array (`[]`) when the entity cannot be loaded or the user lacks `update` access. |

## Admin pages

All of these routes accept any method (no `methods` key). Paths containing `{...}` take the config entity ID, or the migration ID for `{migration_id}`.

| Path | Route | Permission | Purpose |
|---|---|---|---|
| `/admin/dxpr_studio` | `dxpr_builder.admin.studio` | `administer dxpr builder configuration` | DXPR Studio overview; core's admin menu block page listing child pages. |
| `/admin/dxpr_studio/dxpr_builder` | `dxpr_builder.admin.studio.builder` | `administer dxpr builder configuration` | DXPR Builder overview; core's admin menu block page. |
| `/admin/dxpr_studio/dxpr_builder/settings` | `dxpr_builder.settings` | `administer dxpr builder configuration` | `DxprBuilderSettingsForm`: the module settings form. |
| `/admin/dxpr_studio/dxpr_builder/ai_settings` | `dxpr_builder.ai_settings` | `administer dxpr builder configuration` | `DxprBuilderAiSettingsForm`: the AI settings form. |
| `/admin/dxpr_studio/dxpr_builder/user_templates` | `entity.dxpr_builder_user_template.collection` | `administer site configuration` | List of `dxpr_builder_user_template` config entities. |
| `/admin/dxpr_studio/dxpr_builder/user_templates/add` | `entity.dxpr_builder_user_template.add_form` | `administer site configuration` | Add a user template. |
| `/admin/dxpr_studio/dxpr_builder/user_templates/{dxpr_builder_user_template}` | `entity.dxpr_builder_user_template.edit_form` | `administer site configuration` | Edit a user template. |
| `/admin/dxpr_studio/dxpr_builder/user_templates/{dxpr_builder_user_template}/delete` | `entity.dxpr_builder_user_template.delete_form` | `administer site configuration` | Delete a user template (confirm form). |
| `/admin/dxpr_studio/dxpr_builder/page_template` | `entity.dxpr_builder_page_template.collection` | `administer site configuration` | List of `dxpr_builder_page_template` config entities. |
| `/admin/dxpr_studio/dxpr_builder/page_template/add` | `entity.dxpr_builder_page_template.add_form` | `administer site configuration` | Add a page template. |
| `/admin/dxpr_studio/dxpr_builder/page_template/create` | `dxpr_builder.create_page_template` | `administer dxpr builder configuration` | `PageController::createPageTemplate()`: copies the enabled user template identified by the `uuid` query parameter into a new page template with the same ID and category `DXPR`. It sets a status or error message and redirects to the user template list. |
| `/admin/dxpr_studio/dxpr_builder/page_template/{dxpr_builder_page_template}` | `entity.dxpr_builder_page_template.edit_form` | `administer site configuration` | Edit a page template. |
| `/admin/dxpr_studio/dxpr_builder/page_template/{dxpr_builder_page_template}/delete` | `entity.dxpr_builder_page_template.delete_form` | `administer site configuration` | Delete a page template (confirm form). |
| `/admin/dxpr_studio/dxpr_builder/profile` | `entity.dxpr_builder_profile.collection` | `administer dxpr_builder_profile` | List of `dxpr_builder_profile` config entities. |
| `/admin/dxpr_studio/dxpr_builder/profile/add` | `entity.dxpr_builder_profile.add_form` | `administer dxpr_builder_profile` | Add a profile. |
| `/admin/dxpr_studio/dxpr_builder/profile/{dxpr_builder_profile}` | `entity.dxpr_builder_profile.edit_form` | `administer dxpr_builder_profile` | Edit a profile. |
| `/admin/dxpr_studio/dxpr_builder/profile/{dxpr_builder_profile}/delete` | `entity.dxpr_builder_profile.delete_form` | `administer dxpr_builder_profile` | Delete a profile (confirm form). |
| `/admin/dxpr_studio/dxpr_builder/user_licenses` | `dxpr_builder.user_licenses` | `administer dxpr builder configuration` | `PageController::userLicensesPage()`: uncached table of licensed users (columns User, Roles, Sites, Assigned and Operations). Above it, the page renders its own licence summary card (`dxpr-license-info`) when licence information is available, and, while a freed licence waits out its 30 days, a "Next license available on" notice. On this route the **License info** block renders nothing, so the summary never shows twice. |
| `/admin/dxpr_studio/dxpr_builder/user_licenses/sites` | `dxpr_builder.user_licenses.sites` | `administer dxpr builder configuration` | `PageController::userLicensesSitesPage()`: item list of the domains recorded for the user given in the `mail` request parameter; opened in a modal from the licences table. |
| `/admin/dxpr_studio/dxpr_builder/content` | `dxpr_builder.licensed_content` | `administer dxpr builder configuration` | `PageController::licensedContentPage()`: paged table (20 per page) of content items edited with DXPR Builder, with the content items licence summary. The query parameter `needs_migration=1` shows only items that need a content migration. Empty when no licence information is available. |
| `/admin/dxpr_studio/dxpr_builder/migration-status` | `dxpr_builder.migration_status` | `administer dxpr builder configuration` | `MigrationStatusController::statusPage()`: uncached table of registered content migrations with their pending items. Each pending migration has a **Run now** link, and a **Run all pending migrations** button appears when any are pending. |
| `/admin/dxpr_studio/dxpr_builder/migration-run/{migration_id}` | `dxpr_builder.migration_run_confirm` | `administer dxpr builder configuration` | `MigrationRunConfirmForm`: confirms, then runs one migration as a batch and returns to the status page. |
| `/admin/dxpr_studio/dxpr_builder/migration-run-all` | `dxpr_builder.migration_run_all_confirm` | `administer dxpr builder configuration` | `MigrationRunAllConfirmForm`: confirms, then runs every registered migration as one batch and returns to the status page. |
| `/dxpr-builder/disavow-user` | `dxpr_builder.disavow_user_confirm` | `administer dxpr builder configuration` | `DisavowUserConfirmForm`: confirms excluding the user given in the `uid` query parameter from DXPR Builder editing. |
| `/dxpr-builder/delete-stale-user` | `dxpr_builder.delete_stale_user_confirm` | `administer dxpr builder configuration` | `DeleteStaleUserConfirmForm`: confirms removing licence data for the email address given in the `email` query parameter. |

## What's next?

- [Configuration reference](/hc/documentation/learn-dxpr-builder/reference/config-reference) for the settings that control the media browser and AI behaviour used by these endpoints.
- [PHP API](/hc/documentation/learn-dxpr-builder/development/api) for the services the controllers call.
- [Hooks API](/hc/documentation/learn-dxpr-builder/development/hooks-api) for the hooks that let other modules alter builder behaviour.