Visuals API Overview
Endpoints for discovering the available visual types and for creating, reading, updating, and deleting saved Visuals (a chart, gauge, KPI, pivot, map, or any other registered visual-builder type).
The CRUD routes mount on the api deployer at /api/v and /api/v/{id}. The
component projection mounts on the viz deployer (root /v) at
/v/component/{id}.{ext}. A saved Visual stores its definition as a single-key
component object whose key is the visual type and whose value is that type's
configuration.
Authentication
Reading the visual-type list (GET /api/v) and a Visual's component projection
(GET /v/component/{id}.{ext}) require only an authenticated session, with no
permission or license gate.
Creating a Visual requires permission.visuals.create, which resolves to the
visuals license feature plus Designer access level or above; the Visual
model's create hook independently asserts the visuals feature. Updating and
deleting a Visual require permission.visual.write, which resolves to admin (or
the Visual's owner acting as admin on the entity).
The component object
A Visual's definition lives in a single-key component object. The key is the
visual type and the value is that type's stored configuration. For example, a
pivot is { "pivot": { "layout": "tabular" } } and a chart is
{ "chart": { ... } }.
On read, the getter re-emits the same single-key shape with the resolved
configuration merged in, including a dataset reference (which is null when
the Visual has no associated Dataset):
{
"pivot": {
"dataset": null,
"layout": "tabular"
}
}
This is not the { type, chartType, data, options } shape used by some
client-side charting libraries.
GET /api/v
List the registered visual types.
Authentication: Required (no permission or license gate)
Response:
A HAL collection (inf:visuals rel) whose rows are embedded under
inf:visual. Each row is a visual-builder driver descriptor, not a saved
Visual. A descriptor carries an id (the visual type), an expandable href
template, the params it accepts, the output views (content types and their
ext), and presentation flags such as directive, filterDisabled,
allowChildren, defaultLayout, and preventPreview.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/v"
}
},
"start": 0,
"count": 25,
"total": 25,
"_embedded": {
"inf:visual": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/v/layoutPanel"
}
},
"id": "layoutPanel",
"href": "http://127.0.0.1:52838undefined",
"directive": true,
"params": {},
"views": [],
"filterDisabled": true,
"allowVisualDownload": true,
"allowChildren": true,
"defaultLayout": {
"widthPercent": 100,
"heightPixels": 500
},
"preventPreview": true
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/v/dateRange"
}
},
"id": "dateRange",
"href": "http://127.0.0.1:52838undefined",
"directive": true,
"params": {},
"views": [],
"filterDisabled": true,
"allowVisualDownload": true,
"allowChildren": false,
"defaultLayout": {
"widthPixels": 345,
"heightPixels": 120
},
"preventPreview": false,
"editor": "dateRangeInputEditor",
"iconSvg": "/images/icons/dynamic_form.svg"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/v/numericRange"
}
},
"id": "numericRange",
"href": "http://127.0.0.1:52838undefined",
"directive": true,
"params": {},
"views": [],
"filterDisabled": true,
"allowVisualDownload": true,
"allowChildren": false,
"defaultLayout": {
"widthPixels": 345,
"heightPixels": 120
},
"preventPreview": false,
"iconSvg": "/images/icons/dynamic_form.svg"
},
"… 22 more items (25 total) — omitted from docs"
]
}
}
GET /api/v returns the available visual types, not the Visuals you have saved.
To read a saved Visual, use GET /api/v/{id} with its id.
POST /api/v
Create a saved Visual.
Authentication: permission.visuals.create (Designer or above, with the
visuals license feature)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
component | object | Yes | Single-key { type: config } object (e.g. { "pivot": { "layout": "tabular" } }) |
name | string | No | Visual name |
description | string | No | Visual description |
embedded | boolean | No | Whether the Visual is embedded in another entity (default false) |
The payload is stripUnknown, so unrecognized fields are dropped. ownerId is
set to the calling principal.
Example Request:
{
"name": "Quarterly Pivot",
"description": "Revenue by quarter.",
"component": {
"pivot": {
"layout": "tabular"
}
},
"embedded": false
}
Response:
Responds 201 Created with the saved Visual and a Location header.
{
"name": "Quarterly Pivot",
"description": "Revenue by quarter.",
"component": {
"pivot": {
"layout": "tabular"
}
},
"embedded": false
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/datasets//visuals/00000000-0000-4000-8000-000000000001"
}
},
"component": {
"pivot": {
"dataset": null,
"layout": "tabular"
}
},
"permissions": {
"write": true,
"deletePinned": false
},
"id": "00000000-0000-4000-8000-000000000001",
"pinned": false,
"layoutData": {},
"tenant": "acme",
"name": "Quarterly Pivot",
"description": "Revenue by quarter.",
"datasetId": null,
"embedded": false,
"ownerId": "admin",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"source": null,
"defaultName": null,
"defaultDescription": null,
"datasourceId": null
}
GET /api/v/{id}
Retrieve a single saved Visual.
Authentication: Required (resolved through the Visual read_access scope)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Visual id |
Response:
The Visual resolves through its read_access scope (an owner principal match),
so a requester without read access gets 404, not 403. The HAL response runs
the type driver's loadConfig, re-emits the single-key component, and adds a
view link plus an inf:dataset link when the Visual has a datasetId.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/v/00000000-0000-4000-8000-000000000001"
}
},
"component": {
"pivot": {
"dataset": null,
"layout": "tabular"
}
},
"permissions": {
"write": true,
"deletePinned": false
},
"id": "00000000-0000-4000-8000-000000000001",
"datasourceId": null,
"datasetId": null,
"source": null,
"name": "Quarterly Pivot",
"description": "Revenue by quarter.",
"defaultName": null,
"defaultDescription": null,
"embedded": false,
"pinned": false,
"layoutData": {},
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"ownerId": "admin"
}
PUT /api/v/{id}
Update a saved Visual.
Authentication: permission.visual.write (admin or the Visual's owner)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Visual id |
Request Body:
| Field | Type | Description |
|---|---|---|
name | string | Visual name |
description | string | Visual description |
component | object | Single-key { type: config } object; changes the visual type and configuration |
embedded | boolean | Embedded status |
The update runs in a DB transaction.
Example Request:
{
"name": "Updated Revenue Pivot",
"description": "Revenue rollup, refreshed."
}
Response:
Responds 200 with the updated Visual.
{
"name": "Updated Revenue Pivot",
"description": "Revenue rollup, refreshed."
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/v/00000000-0000-4000-8000-000000000001"
}
},
"component": {
"pivot": {
"dataset": null,
"layout": "tabular"
}
},
"permissions": {
"write": true,
"deletePinned": false
},
"id": "00000000-0000-4000-8000-000000000001",
"datasourceId": null,
"datasetId": null,
"source": null,
"name": "Updated Revenue Pivot",
"description": "Revenue rollup, refreshed.",
"defaultName": null,
"defaultDescription": null,
"embedded": false,
"pinned": false,
"layoutData": {},
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"ownerId": "admin",
"datasets": []
}
DELETE /api/v/{id}
Delete a saved Visual.
Authentication: permission.visual.write (admin or the Visual's owner)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Visual id |
Response:
Responds 200 with an empty body (Informer's db.remove convention), not
204. The delete runs in a DB transaction.
GET /v/component/{id}.{ext}
Project a saved Visual's component in a requested format.
Authentication: Required (plain lookup, no read_access scope)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string | Visual id |
ext | string | Output format: json (default) or html |
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
download | boolean | Force a download with a filename |
Response:
With ext=json (the default), the route returns the Visual's component
getter: the single-key { type: { dataset, ...config } } object.
{
"_links": {
"self": {
"href": "https://informer.example.com/v/component/00000000-0000-4000-8000-000000000001.json"
}
},
"pivot": {
"dataset": null,
"layout": "tabular"
}
}
This route resolves the Visual with a plain lookup and does not apply the
read_access scope, so any authenticated user in the Tenant can read any
Visual's component JSON, a wider read surface than GET /api/v/{id}.
Render and proxy endpoints (not shown)
The following routes return rendered or non-JSON output and are documented here without captured examples.
GET /v/component/{id}.html
The same component-projection route with ext=html renders the Visual through
a Swig template and responds with text/html rather than JSON.
GET /w/{id} and GET /w/{id}.{ext}
The saved-Visual proxy on the savedViz deployer (root /w) renders a saved
Visual for embedding or download. It serves rendered output (HTML and image
formats) rather than a JSON envelope.
Descriptor script (.ngv1)
The visual-builder descriptor is also served as a client-side script asset
(.ngv1) rather than JSON, so it is not captured here.