Publishing reports

Publish the HTML an agent made to a persistent URL behind your sign-in: versions, audiences, live data, comments and readership, from any surface.

A report is HTML your agent made — a briefing, a dashboard, a prototype — published to an address that stays the same, readable only by the people it was shared with, through their own sign-in. Every version is kept. The same operations are in the SDKs, the MCP server (publish_report and its neighbours), the CLI and the REST API. For how readers see reports, see Publishing and sharing reports in the guide.

Publish

SDK

import { deliverd } from "@deliverd/sdk";

const { report } = await deliverd.publish({
  title: "Weekly finance report",
  slug: "finance/weekly",
  content: html,                 // one HTML document, up to 2 MB
  audience: ["Finance team"],
  living: true,
});

console.log(report.url);

Small reports can send content (a single HTML document, up to 2 MB) or files as [{ path, content }] inline. Anything larger goes up first as an HTML file or a ZIP through an upload ticket:

REST

# 1. Get an upload ticket, then PUT the bundle (HTML or ZIP)
curl -X POST "$DELIVERD_URL/api/v1/uploads" -H "Authorization: Bearer $DELIVERD_API_KEY"
curl -X PUT "<uploadUrl>" --data-binary @report.zip

# 2. Publish it
curl -X POST "$DELIVERD_URL/api/v1/reports" \
  -H "Authorization: Bearer $DELIVERD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "uploadId": "…", "title": "Weekly finance report",
        "audience": ["Finance team"], "living": true, "slug": "finance/weekly" }'
slug
The report's address within your organisation, such as finance/weekly.
audience
Who can open it, in plain language: people, teams, groups, workspaces, "everyone". See Sharing.
living
A report meant to be updated over time at the same address.
classification
public, internal, confidential or restricted. Your organisation's sharing rules read it.
changeSummary
What changed in this version, shown in its history.
flowId
The flow the report belongs to. A report joins on its first publish.

Every publish is scanned for secrets and unsafe content first. An upload ticket is consumed by the publish that reads it, so a second publish with the same uploadId answers 404 upload_not_found — which usually means the first one worked. Look before you upload again.

Versions

POST /api/v1/reports/{idOrSlug}/versions publishes a new version at the same address, to the same audience. Pass expectedVersion so you do not overwrite somebody else's work: if the report has moved on, the update is refused with 409 version_conflict, and you should re-read and try again.

const { latestVersion } = await deliverd.reports.versions("finance/weekly");

await deliverd.reports.update("finance/weekly", {
  content: html,
  changeSummary: "October figures",
  expectedVersion: latestVersion as number,
});
  • Use latestVersion from GET /api/v1/reports/{idOrSlug}/versions, not the live version's number: a held or declined version takes a number without going live.
  • POST /api/v1/reports/{idOrSlug}/rollback with { "version": 3 } serves an earlier version again. Nothing is deleted; roll forward the same way.
  • POST /api/v1/reports/{idOrSlug}/archive stops the address serving, and { "archived": false } puts it back at the same version and audience.
  • POST /api/v1/reports/{idOrSlug}/copy starts a new report from an existing one or a template, with none of its audience or comments. GET /api/v1/templates lists the templates.

Share, send and take back

CallScopeDoes
POST /api/v1/reports/{idOrSlug}/sharereports:shareGrant an audience access. The address does not change.
DELETE /api/v1/reports/{idOrSlug}/sharereports:shareRemove every grant matching the same audience you granted. The owner's access cannot be removed.
POST /api/v1/reports/{idOrSlug}/sendreports:shareEmail up to 50 addresses a link each, with a covering note. Addresses the organisation has not seen become guests.
POST /api/v1/audiences/resolveaudiences:readCheck what an audience phrase means before you use it.

An ambiguous name, or one matching nobody, is refused with a list of candidates for the agent to confirm with you rather than a guess. Your organisation's sharing rules apply to every grant: a confidential report cannot be made public, and an agent key can never create a public link (403 public_sharing_denied).

Live data

A dataset lets a published report show new numbers without a new version. The report fetches it from its own origin when someone opens it:

<script>
  const figures = await (await fetch("_data/figures")).json();
</script>

Write it with PUT /api/v1/reports/{idOrSlug}/data/{name} (a JSON body, stored verbatim, up to 5 MB; 25 per report), list them with GET /api/v1/reports/{idOrSlug}/data, and remove one with DELETE /api/v1/reports/{idOrSlug}/data/{name}. It is behind the same access check as the report. Datasets are not versioned, so rolling back restores the markup and not the numbers.

Comments and revision briefs

  • GET /api/v1/comments lists every open thread across the organisation — what needs attention. GET /api/v1/reports/{idOrSlug}/comments lists one report's threads, with the quoted passage and any images.
  • POST /api/v1/reports/{idOrSlug}/comments with { body, quote?, threadId? } comments or replies. A quote must occur exactly once in the current version: not there is 404 quote_not_found, more than once is 400 quote_ambiguous.
  • POST /api/v1/reports/{idOrSlug}/revision-briefs composes the open threads into one prompt for producing the next version. Over MCP it is get_revision_brief, with the readers' screenshots as images.
  • POST /api/v1/comments/{threadId}/resolve resolves a thread with a note. DELETE /api/v1/reports/{idOrSlug}/comments/{commentId} retracts your own last comment in a thread nobody has closed.

Readership

GET /api/v1/reports/{idOrSlug}/analytics answers whether anyone read a report: total views on every plan, and unique, internal and external readers, a 30-day series and recent readers on plans with analytics. GET /api/v1/analytics answers across the organisation, including the live reports nobody has opened; below Team it is refused with 402 plan_required. Reader names appear only where your organisation's sharing settings show viewer identities.

Scheduled updates — asking an agent for a recurring publish — are /api/v1/schedules; the MCP tool list_due_schedules tells an agent what is waiting on it. For every field, see the interactive API reference. Deliverd scans and serves the result from a separate origin; see Errors and limits for the plan limits a publish can meet.