Skip to main content

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.

List visual typesGET /api/v
GET /api/v (the .md’s `/api/visuals` is wrong) does NOT list saved visuals. It returns the registered visual-builder DRIVER DESCRIPTORS — the available visual types, each with an `id`, an expandable `href` template, `params`, and `views`. HAL collection: `inf:visuals` api rel, rows embedded under `inf:visual`. No permission or license gate.
Response · 200
{
"_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"
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.
This route does not list saved Visuals

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:

FieldTypeRequiredDescription
componentobjectYesSingle-key { type: config } object (e.g. { "pivot": { "layout": "tabular" } })
namestringNoVisual name
descriptionstringNoVisual description
embeddedbooleanNoWhether 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.

Create a visualPOST /api/v
POST /api/v (NOT /api/visuals). Requires permission.visuals.create = hasFeature(visuals) && isDesigner(); the model’s beforeCreate hook also asserts the visuals license feature. Payload is { component (required-ish), name?, description?, embedded? } and is stripUnknown. The `component` is a SINGLE-KEY object whose key is the visual type and whose value is the stored config — e.g. { pivot: { layout: "tabular" } }, NOT { type, chartType, data, options }. `ownerId` is set to the caller. Responds 201 with a Location header.
Request body
{
"name": "Quarterly Pivot",
"description": "Revenue by quarter.",
"component": {
"pivot": {
"layout": "tabular"
}
},
"embedded": false
}
Response · 201
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/v/{id}

Retrieve a single saved Visual.

Authentication: Required (resolved through the Visual read_access scope)

Path Parameters:

ParameterTypeDescription
idstringVisual 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.

Get a visualGET /api/v/00000000-0000-4000-8000-000000000001
GET /api/v/{id} (NOT /api/visuals/{id}) resolves through visual.lookup, which applies the Visual read_access scope (owner principal match). A requester without read access gets 404, not 403. The HAL response runs the driver’s loadConfig, ignores the raw `type`/`config` keys, and adds a `view` link (the savedViz /w/{id} href) plus `inf:dataset` when the visual has a datasetId.
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/v/{id}

Update a saved Visual.

Authentication: permission.visual.write (admin or the Visual's owner)

Path Parameters:

ParameterTypeDescription
idstringVisual id

Request Body:

FieldTypeDescription
namestringVisual name
descriptionstringVisual description
componentobjectSingle-key { type: config } object; changes the visual type and configuration
embeddedbooleanEmbedded 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.

Update a visualPUT /api/v/00000000-0000-4000-8000-000000000001
PUT /api/v/{id} (db.update handler) requires permission.visual.write = isAdmin(visual) (admin or the owner). Runs in a DB transaction and returns the updated visual. Send `component` as the single-key { type: config } object to change the visual type/config.
Request body
{
"name": "Updated Revenue Pivot",
"description": "Revenue rollup, refreshed."
}
Response · 200
{
"_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": []
}
Captured from the API examples test suite; ids and timestamps are normalized.

DELETE /api/v/{id}

Delete a saved Visual.

Authentication: permission.visual.write (admin or the Visual's owner)

Path Parameters:

ParameterTypeDescription
idstringVisual id

Response:

Responds 200 with an empty body (Informer's db.remove convention), not 204. The delete runs in a DB transaction.

Delete a visualDELETE /api/v/00000000-0000-4000-8000-000000000001
DELETE /api/v/{id} (db.remove handler, NOT /api/visuals/{id}) requires permission.visual.write = isAdmin(visual). Returns 200 with an EMPTY body (Informer convention), NOT 204 as the prose claims. Runs in a DB transaction.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

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:

ParameterTypeDescription
idstringVisual id
extstringOutput format: json (default) or html

Query Parameters:

ParameterTypeDescription
downloadbooleanForce 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.

Get a visual’s component (JSON)GET /v/component/00000000-0000-4000-8000-000000000001.json
GET /v/component/{id}.{ext} (the .md’s `/api/visuals/{id}/component.{ext}` is wrong — this route is on the `viz` deployer, root `/v`). With ext=json (the default) it returns the Visual `component` getter: a single-key { type: { dataset, ...config } } object. Its pre does a plain findById with NO read_access scope, so any authenticated user can read any visual’s component JSON. ext=html instead renders a swig template to text/html (not captured here).
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/v/component/00000000-0000-4000-8000-000000000001.json"
}
},
"pivot": {
"dataset": null,
"layout": "tabular"
}
}
Captured from the API examples test suite; ids and timestamps are normalized.
Wider read surface than the CRUD route

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.