Skip to main content

Bundles

A Bundle is a named, tenant-scoped manifest of entities (its entry rows) that can later be exported to a .tgz archive and deployed or imported into another tenant. Each entry names one entity to ship (a Tag, Report, Dataset, Query, Datasource, Team, User, Job, Template, Letterhead, Assistant, Library, Code, App, Toolkit, or Integration) and is backed by an entry-type driver that knows how to export and import that kind of entity.

There is no license-feature gate on any Bundle route. Reads are open within the tenant (no permission check). Every write route is guarded by a Superuser permission check: POST /api/bundles requires permission.bundles.create, and the bundle and entry write/delete routes require permission.bundle.write (the delete and rename abilities delegate to bundle:write). All three reduce to req.auth.credentials.superuser.

Bundles are not owner-scoped (there is no createdById column), so any Superuser in the tenant manages any Bundle.

GET /api/bundle-drivers

List the registered bundle entry-type drivers. Use a driver id as the type when creating an entry.

Authentication: Required (read-only, no permission check)

Response:

A HAL collection under _embedded["inf:bundle-driver"], in registration order. Each descriptor carries an id, a name (an i18n key the client resolves to the active locale), and an icon hint (materialIcon, icon, or iconSvg). The editor field is a UI spec; it is a function and is dropped by JSON serialization. The registered ids are code, global, assistant, dataset, datasource, query, report, tagged, user, team, job, template, letterhead, library, app, toolkit, and integration.

List bundle entry-type driversGET /api/bundle-drivers
GET /api/bundle-drivers (rel inf:bundle-drivers) returns the registered bundle entry-type drivers as a collection, embedded under inf:bundle-driver (href ./{item.id}). Each descriptor carries { id, name (an i18n key), and an icon hint: materialIcon | iconSvg }; the `editor` spec is a function and is dropped by JSON. Use a driver id as the `type` when creating a bundle entry. Read-only, no permission check. Sorted in registration order (no ORDER BY); the capture stable-sorts by id for byte-identical re-runs.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundle-drivers"
}
},
"start": 0,
"count": 17,
"total": 17,
"_embedded": {
"inf:bundle-driver": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundle-drivers/app"
}
},
"id": "app",
"name": "App",
"materialIcon": "widgets",
"editor": {
"appBundleEntry": {}
}
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundle-drivers/assistant"
}
},
"id": "assistant",
"name": "Assistant",
"icon": "fas fa-atom",
"editor": {
"assistantBundleEntry": {}
}
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundle-drivers/code"
}
},
"id": "code",
"name": "Code",
"iconSvg": "/images/admin/codes.svg",
"editor": {
"codeBundleEntry": {}
}
},
"… 14 more items (17 total) — omitted from docs"
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.
global is a driver, not a builder

global is a bundle driver (it appears here and ships no icon), but it is not an entry builder, so it does not appear in GET /api/bundle-templates.


GET /api/bundle-drivers/{id}

Retrieve a single driver descriptor.

Authentication: Required (read-only, no permission check)

Path Parameters:

ParameterTypeDescription
idstringA registered bundle-driver id (e.g. dataset, tagged, report)

Response:

The driver descriptor for that id. An unregistered id throws 400 (boom.badRequest) from the DriverManager, not a 404.

Get a bundle entry-type driverGET /api/bundle-drivers/dataset
GET /api/bundle-drivers/{id} (rel inf:bundle-driver) returns one driver descriptor via dm("bundle").get(id). An unregistered id throws boom.badRequest (400) from the DriverManager, not a 404. Read-only, no permission check.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundle-drivers/dataset"
}
},
"id": "dataset",
"name": "Dataset",
"materialIcon": "find_in_page",
"editor": {
"datasetBundleEntry": {}
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/bundle-templates

Discover the bundle-entry-builder palette: the "add entry" choices the UI shows. This is not a CRUD list.

Authentication: Required (read-only, no permission check)

Response:

A HAL collection whose items each carry a value with group ("__top"), name (translated), an icon hint, entry (with the type to send when creating an entry), and a component spec. There is one builder per top-level entry type. The global driver is intentionally absent (it is a driver, not a builder).

List bundle entry templatesGET /api/bundle-templates
GET /api/bundle-templates (rel inf:bundle-entry-templates) is NOT a CRUD list — it discovers the `bundle-entry-builder` agency: the "add entry" palette the UI shows. Each item is { group: "__top", name (translated), icon|materialIcon|iconSvg, entry: { type }, component }, one per top-level entry type. NOTE: `global` is a bundle driver but NOT an entry builder, so it does not appear here.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundle-templates"
}
},
"items": [
{
"detective": "00000000-0000-4000-8000-000000000001",
"id": "00000000-0000-4000-8000-000000000001:0",
"value": {
"group": "__top",
"name": "Dataset",
"materialIcon": "find_in_page",
"entry": {
"type": "dataset"
},
"component": {
"datasetBundleEntry": {}
}
}
},
{
"detective": "00000000-0000-4000-8000-000000000002",
"id": "00000000-0000-4000-8000-000000000002:0",
"value": {
"group": "__top",
"name": "Datasource",
"icon": "fas fa-database",
"entry": {
"type": "datasource"
},
"component": {
"datasourceBundleEntry": {}
}
}
},
{
"detective": "00000000-0000-4000-8000-000000000003",
"id": "00000000-0000-4000-8000-000000000003:0",
"value": {
"group": "__top",
"name": "Job",
"icon": "flaticon-schedule",
"entry": {
"type": "job"
},
"component": {
"jobBundleEntry": {}
}
}
},
"… 13 more items (16 total) — omitted from docs"
],
"start": 0,
"count": 16,
"total": 16
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/bundle-list

A fast, flat read of the tenant's Bundles.

Authentication: Required (read-only, no permission check)

Response:

A bare JSON array (not a HAL {start,total,count,items} collection) produced by a raw SQL projection of the bundle table. Each item carries id, naturalId (equal to the slug), name, description, createdAt, and updatedAt, with no per-entry detail. RLS scopes the array to the caller's tenant.

List bundles (fast list)GET /api/bundle-list
GET /api/bundle-list (rel inf:bundle-list) is a fast, flat read: a raw SQL projection of the bundle table returning a bare ARRAY of { id, naturalId (= slug), name, description, createdAt, updatedAt }. It is NOT a HAL {start,total,count,items} collection and carries no per-entry detail. RLS scopes it to the caller’s tenant. No ORDER BY (DB order); a single seeded bundle keeps it deterministic.
Response · 200
[
{
"id": "00000000-0000-4000-8000-000000000001",
"naturalId": "order-desk-starter",
"name": "Order Desk Starter",
"description": "Tags and reports that seed a new Order Desk tenant",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"id": "00000000-0000-4000-8000-000000000002",
"naturalId": "scratch-bundle",
"name": "Scratch Bundle",
"description": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
]
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/bundles

List the tenant's Bundles as a HAL collection.

Authentication: Required (read-only, no permission check)

Response:

A HAL collection under _embedded["inf:bundle"] with the usual start, total, and count envelope. Each Bundle eager-loads its entries. The default page limit applies and there is no explicit sort (database order).

List bundlesGET /api/bundles
GET /api/bundles (rel inf:bundles) is the HAL collection of the tenant’s Bundle rows ({ start, total, count, items }, items embedded under inf:bundle). Each bundle eager-loads its `entries`. Default page limit applies; no explicit sort. Read-only, no permission check.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles"
},
"next": {
"href": "https://informer.example.com/api/bundles?start=4&limit=10"
}
},
"start": 0,
"count": 2,
"total": 4,
"_embedded": {
"inf:bundle": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000001"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Order Desk Starter",
"slug": "order-desk-starter",
"description": "Tags and reports that seed a new Order Desk tenant",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"entries": [
{
"id": "00000000-0000-4000-8000-000000000002",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"id": "00000000-0000-4000-8000-000000000004",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"id": "00000000-0000-4000-8000-000000000005",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
]
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000006"
}
},
"id": "00000000-0000-4000-8000-000000000006",
"name": "Scratch Bundle",
"slug": "scratch-bundle",
"description": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"entries": []
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/bundles

Create a Bundle.

Authentication: permission.bundles.create (Superuser)

Request Body:

FieldTypeRequiredDescription
namestringYesDisplay name
slugstringNoURL-safe identifier; persisted as naturalId
descriptionstringNoDescription

This route has no Joi payload validation: the handler persists the payload verbatim, so name, slug, and description all save and naturalId is set from slug.

Example Request:

{
"name": "Quarterly Reporting Pack",
"slug": "quarterly-reporting-pack",
"description": "Datasets, reports, and templates for the quarterly close"
}

Response:

Responds 201 Created with the Bundle and a Location header resolved from the slug.

Create a bundlePOST /api/bundles
POST /api/bundles requires permission.bundles.create (superuser). It has NO joi payload validation: the handler does Bundle.create(payload) verbatim, so { name, slug, description } all persist (naturalId = slug). Responds 201 with a Location header resolved from the slug. Bundles are not owner-scoped — any superuser in the tenant manages any bundle.
Request body
{
"name": "Quarterly Reporting Pack",
"slug": "quarterly-reporting-pack",
"description": "Datasets, reports, and templates for the quarterly close"
}
Response · 201
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/quarterly-reporting-pack"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"name": "Quarterly Reporting Pack",
"slug": "quarterly-reporting-pack",
"description": "Datasets, reports, and templates for the quarterly close",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z"
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/bundles/{id}

Retrieve a single Bundle with its entries.

Authentication: Required (read-only, no permission check)

Path Parameters:

ParameterTypeDescription
idstringBundle UUID or slug

Response:

The lookup branches on whether id is a UUID, so a Bundle resolves by either its UUID or its slug; a missing Bundle answers 404. The body eager-loads the Bundle's entries and adds inf:download (to ./_export) and inf:deploy (to ./_deploy) link hrefs. Following those links is the binary or destructive path (see below).

Get a bundleGET /api/bundles/order-desk-starter
GET /api/bundles/{id} (rel inf:bundle) resolves via bundle.lookup (Bundle.scope(forTenant).findById, which branches on isUuid so {id} resolves by uuid OR slug; 404 if missing). The body eager-loads the bundle’s `entries` and adds inf:download (-> ./_export) and inf:deploy (-> ./_deploy) link hrefs (following those is the binary/destructive path). Read-only, no permission check.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/order-desk-starter"
},
"inf:download": {
"href": "https://informer.example.com/api/bundles/order-desk-starter/_export"
},
"inf:deploy": {
"href": "https://informer.example.com/api/bundles/order-desk-starter/_deploy"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Order Desk Starter",
"slug": "order-desk-starter",
"description": "Tags and reports that seed a new Order Desk tenant",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"entries": [
{
"id": "00000000-0000-4000-8000-000000000002",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"id": "00000000-0000-4000-8000-000000000004",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"id": "00000000-0000-4000-8000-000000000005",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
]
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/bundles/{id}

Update a Bundle.

Authentication: permission.bundle.write (Superuser)

Path Parameters:

ParameterTypeDescription
idstringBundle UUID or slug

Request Body (partial):

FieldTypeDescription
namestringDisplay name
slugstringURL-safe identifier
descriptionstringDescription

This route is unvalidated, so any Bundle column may be sent.

Example Request:

{
"name": "Order Desk Starter (Updated)",
"description": "Refreshed starter content for new Order Desk tenants"
}

Response:

Responds 200 with the updated Bundle (entries included).

Update a bundlePUT /api/bundles/00000000-0000-4000-8000-000000000001
PUT /api/bundles/{id} resolves the bundle then requires permission.bundle.write (superuser). It is db.upsert(where id = the resolved bundle) and unvalidated, so any Bundle column (name, slug, description) may be sent. Returns the updated bundle.
Request body
{
"name": "Order Desk Starter (Updated)",
"description": "Refreshed starter content for new Order Desk tenants"
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000001"
}
},
"id": "00000000-0000-4000-8000-000000000001",
"name": "Order Desk Starter (Updated)",
"slug": "order-desk-starter",
"description": "Refreshed starter content for new Order Desk tenants",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"entries": [
{
"id": "00000000-0000-4000-8000-000000000002",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"id": "00000000-0000-4000-8000-000000000004",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"id": "00000000-0000-4000-8000-000000000005",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
]
}
Captured from the API examples test suite; ids and timestamps are normalized.

DELETE /api/bundles/{id}

Delete a Bundle.

Authentication: permission.bundle.delete (delegates to bundle:write, Superuser)

Path Parameters:

ParameterTypeDescription
idstringBundle UUID or slug

Response:

Responds 200 with an empty body (Informer's db.remove convention), not 204.

Delete a bundleDELETE /api/bundles/00000000-0000-4000-8000-000000000001
DELETE /api/bundles/{id} resolves the bundle then requires permission.bundle.delete (which delegates to bundle:write, superuser). It is db.remove and cascades to the bundle’s entries (BundleEntry FK onDelete: cascade). Returns 200 with an EMPTY body (Informer db.remove convention), NOT 204.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.
Cascade delete

Deleting a Bundle cascade-deletes all of its entries (the entry rows have a foreign key with onDelete: cascade). The referenced entities (the Tags, Reports, and so on the entries pointed at) are not touched.


GET /api/bundles/{id}/entries

List a Bundle's entries.

Authentication: Required (read-only, no permission check)

Path Parameters:

ParameterTypeDescription
idstringBundle UUID or slug

Response:

A HAL collection under _embedded["inf:bundle-entry"], scoped to the Bundle. It eager-loads the referenced entity per type (tag, report, dataset, query, datasource, team, user, job, template, assistant, library, letterhead, code) and attaches the bundle driver descriptor under each item's embedded inf:bundle-driver. The referenced entity is embedded under its own rel (here inf:tag).

List bundle entriesGET /api/bundles/00000000-0000-4000-8000-000000000001/entries
GET /api/bundles/{id}/entries (rel inf:bundle-entries) is the HAL collection of the bundle’s manifest rows, scoped to bundleId. It eager-loads the referenced entity per type (tag/report/dataset/query/datasource/team/user/job/template/assistant/library/letterhead/code) and attaches the bundle driver descriptor under each item’s `driver` (embedded inf:bundle-driver). Read-only, no permission check. No explicit sort, but the seeded entries are interchangeable (same type + tag), so the sanitized fixture is order-invariant.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000001/entries"
}
},
"start": 0,
"count": 3,
"total": 3,
"_embedded": {
"inf:bundle-entry": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000001/entries/00000000-0000-4000-8000-000000000002"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"report": null,
"dataset": null,
"query": null,
"datasource": null,
"team": null,
"user": null,
"job": null,
"template": null,
"assistant": null,
"library": null,
"letterhead": null,
"code": null,
"_embedded": {
"inf:bundle-driver": {
"_links": {
"self": {
"href": "https://informer.example.com/api/bundle-drivers/tagged"
}
},
"id": "tagged",
"name": "Tag",
"icon": "fas fa-tag",
"editor": {
"taggedBundleEntry": {}
}
},
"inf:tag": {
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000003"
}
},
"permissions": {
"edit": true,
"delete": true,
"write": true,
"rename": true
},
"id": "00000000-0000-4000-8000-000000000003",
"name": "Order Desk"
}
}
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000001/entries/00000000-0000-4000-8000-000000000004"
}
},
"id": "00000000-0000-4000-8000-000000000004",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"report": null,
"dataset": null,
"query": null,
"datasource": null,
"team": null,
"user": null,
"job": null,
"template": null,
"assistant": null,
"library": null,
"letterhead": null,
"code": null,
"_embedded": {
"inf:bundle-driver": {
"_links": {
"self": {
"href": "https://informer.example.com/api/bundle-drivers/tagged"
}
},
"id": "tagged",
"name": "Tag",
"icon": "fas fa-tag",
"editor": {
"taggedBundleEntry": {}
}
},
"inf:tag": {
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000003"
}
},
"permissions": {
"edit": true,
"delete": true,
"write": true,
"rename": true
},
"id": "00000000-0000-4000-8000-000000000003",
"name": "Order Desk"
}
}
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000001/entries/00000000-0000-4000-8000-000000000005"
}
},
"id": "00000000-0000-4000-8000-000000000005",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"report": null,
"dataset": null,
"query": null,
"datasource": null,
"team": null,
"user": null,
"job": null,
"template": null,
"assistant": null,
"library": null,
"letterhead": null,
"code": null,
"_embedded": {
"inf:bundle-driver": {
"_links": {
"self": {
"href": "https://informer.example.com/api/bundle-drivers/tagged"
}
},
"id": "tagged",
"name": "Tag",
"icon": "fas fa-tag",
"editor": {
"taggedBundleEntry": {}
}
},
"inf:tag": {
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000003"
}
},
"permissions": {
"edit": true,
"delete": true,
"write": true,
"rename": true
},
"id": "00000000-0000-4000-8000-000000000003",
"name": "Order Desk"
}
}
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/bundles/{id}/entries

Add an entry to a Bundle.

Authentication: permission.bundle.write (Superuser)

Path Parameters:

ParameterTypeDescription
idstringBundle UUID or slug

Request Body:

FieldTypeRequiredDescription
typestringYesA registered bundle-driver id (a model validator throws otherwise)
<type>IdstringYesThe id of the referenced entity, in the matching column (e.g. tagId for tagged, reportId for report)
dataobjectNoPer-driver options JSON (defaults to {})

bundleId is taken from the URL; the rest of the payload is unvalidated by Joi. The <type>Id column must match the type: tagged uses tagId, report uses reportId, dataset uses datasetId, and so on.

Example Request:

{ "type": "tagged", "tagId": "00000000-0000-4000-8000-000000000002" }

Response:

Responds 201 Created with the entry and a Location header.

Add a bundle entryPOST /api/bundles/00000000-0000-4000-8000-000000000001/entries
POST /api/bundles/{id}/entries resolves the bundle then requires permission.bundle.write (superuser). bundleId is taken from the URL; the rest of the payload is unvalidated by joi. `type` MUST be a registered bundle-driver id (a model validator throws otherwise), and the matching <type>Id column points at the entity (here type:"tagged" + tagId). One guard: a `job` entry whose Job is a system job is rejected with 400. Responds 201 with a Location header.
Request body
{
"type": "tagged",
"tagId": "00000000-0000-4000-8000-000000000002"
}
Response · 201
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000001/entries/00000000-0000-4000-8000-000000000003"
}
},
"id": "00000000-0000-4000-8000-000000000003",
"data": {},
"tenant": "acme",
"type": "tagged",
"tagId": "00000000-0000-4000-8000-000000000002",
"bundleId": "00000000-0000-4000-8000-000000000001",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"datasourceId": null,
"queryId": null,
"datasetId": null,
"reportId": null,
"username": null,
"teamId": null,
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null
}
Captured from the API examples test suite; ids and timestamps are normalized.
System jobs cannot be bundled

A job entry whose Job is a system job is rejected with 400 ("System jobs cannot be added to bundles").


GET /api/bundles/{id}/entries/{entry}

Retrieve a single entry.

Authentication: Required (read-only, no permission check)

Path Parameters:

ParameterTypeDescription
idstringBundle UUID or slug
entrystringEntry UUID

Response:

Resolves the Bundle, then looks up the entry by its bundleId and id. A 404 is returned when either the Bundle or the entry is missing. The body adds an inf:bundle-driver link for the entry's type.

Get a bundle entryGET /api/bundles/00000000-0000-4000-8000-000000000001/entries/00000000-0000-4000-8000-000000000002
GET /api/bundles/{id}/entries/{entry} (rel inf:bundle-entry) resolves the bundle then db.lookup BundleEntry by (bundleId, id). 404 if either the bundle or the entry is missing. Adds an inf:bundle-driver link for the entry’s type. Read-only, no permission check.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000001/entries/00000000-0000-4000-8000-000000000002"
},
"inf:bundle-driver": {
"href": "https://informer.example.com/api/bundle-drivers/tagged"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/bundles/{id}/entries/{entry}

Update an entry.

Authentication: permission.bundle.write (Superuser)

Path Parameters:

ParameterTypeDescription
idstringBundle UUID or slug
entrystringEntry UUID

Request Body (partial):

FieldTypeDescription
dataobjectPer-driver options JSON (e.g. inclusion toggles the export driver reads)

This route is unvalidated. The update is scoped to the entry's bundleId and id.

Example Request:

{ "data": { "includeChildren": true } }

Response:

Responds 200 with the updated entry.

Update a bundle entryPUT /api/bundles/00000000-0000-4000-8000-000000000001/entries/00000000-0000-4000-8000-000000000002
PUT /api/bundles/{id}/entries/{entry} resolves the bundle then requires permission.bundle.write (superuser). It is db.update(where bundleId + id) and unvalidated; `data` is the per-driver options JSON (e.g. inclusion toggles the export driver reads). Returns the updated entry.
Request body
{
"data": {
"includeChildren": true
}
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/bundles/00000000-0000-4000-8000-000000000001/entries/00000000-0000-4000-8000-000000000002"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"bundleId": "00000000-0000-4000-8000-000000000001",
"type": "tagged",
"data": {
"includeChildren": true
},
"datasourceId": null,
"datasetId": null,
"reportId": null,
"username": null,
"queryId": null,
"teamId": null,
"tagId": "00000000-0000-4000-8000-000000000003",
"jobId": null,
"templateId": null,
"letterheadId": null,
"assistantId": null,
"libraryId": null,
"codeId": null,
"appId": null,
"toolkitId": null,
"integrationId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
Captured from the API examples test suite; ids and timestamps are normalized.

DELETE /api/bundles/{id}/entries/{entry}

Delete an entry from a Bundle.

Authentication: permission.bundle.write (Superuser)

Path Parameters:

ParameterTypeDescription
idstringBundle UUID or slug
entrystringEntry UUID

Response:

Responds 200 with an empty body (Informer's db.remove convention), not 204. The referenced entity (for example the Tag) is left untouched; only the manifest row is removed.

Delete a bundle entryDELETE /api/bundles/00000000-0000-4000-8000-000000000001/entries/00000000-0000-4000-8000-000000000002
DELETE /api/bundles/{id}/entries/{entry} resolves the bundle then requires permission.bundle.write (superuser). It is db.remove(where bundleId + id). Returns 200 with an EMPTY body (Informer db.remove convention), NOT 204. The referenced entity (the Tag here) is untouched.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

Export, deploy, import, and restore

The following routes either stream binary archives, upload files, or replace tenant data destructively, so they are documented here without captured examples. Every one of them requires Superuser.

Examples not shown

These endpoints move .tgz archives or mutate tenant data in place. Capturing them would either embed binary payloads or corrupt the capture tenant, so no <ApiExample> fixture exists for them. The shapes below come from the route definitions.

POST /api/bundles/{id}/_export

Export an existing Bundle to a .tgz archive.

Authentication: permission.bundle.write (Superuser)

Response: Streams a .tgz download Resource (binary). This is the target of the inf:download link on a Bundle.

POST /api/bundles/{id}/_deploy

Export an existing Bundle and import it into one or more target tenants.

Authentication: permission.bundle.write (Superuser)

Response: No JSON fixture. This is the target of the inf:deploy link on a Bundle.

Destructive

Deploy exports the Bundle and then imports it into the target tenants, replacing their data. Run it only against tenants you intend to overwrite.

POST /api/_import-bundle

Import a previously exported Bundle archive.

Authentication: Superuser

Request Body: A multipart or streamed file upload of a .tgz archive.

Response: No JSON fixture (file upload).

POST /api/_bundle

Export entities to a .tgz archive in one step, without first persisting a Bundle row.

Authentication: Superuser

Response: Streams a .tgz download (binary).

POST /api/_bundle-deploy

Export entities and deploy them in one step.

Authentication: Superuser

Response: No JSON fixture.

Destructive

This combines an instant export with a deploy into target tenants, replacing their data.

POST /api/_restore

Import an archive with restore=true, replacing tenant data.

Authentication: Superuser

Response: No JSON fixture.

Destructive

Restore replaces the tenant's existing data with the archive contents. It is not captured because it would corrupt the capture tenant.

POST /api/_restore-from-file

Same as _restore, but the archive comes from an uploaded file.

Authentication: Superuser

Request Body: A multipart or streamed file upload of a .tgz archive.

Response: No JSON fixture.

Destructive

This restores from an uploaded archive and replaces tenant data.