E EMBAN / Docs

Scheduled Reports

Scheduled reports render a dashboard on a recurring schedule (daily, weekly, or monthly) and deliver the result as CSV, PNG, or PDF over email, webhook, or both. The scheduler walks every enabled report once a minute and dispatches any whose next_run_at has come due.

One report, one dashboard. A scheduled report targets exactly one dashboard by dashboard_id. CSV reports export every widget on that dashboard as separate sheets in a single file; PNG and PDF reports render the dashboard as a composed visual artifact via headless Chrome server-side.

Schedule kinds

KindRequired fieldsBehavior
dailyhourRuns every day at hour:00 in the configured timezone.
weeklyhour, day_of_weekRuns weekly at hour:00 on the given day (0=Sunday through 6=Saturday).
monthlyhour, day_of_monthRuns monthly at hour:00 on the given day-of-month (131). Months that don't contain the chosen day are skipped without error.

Timezones use IANA names (America/New_York, Europe/Berlin, UTC). DST transitions are respected — hour=9 in Europe/Berlin means 9 AM local, which moves relative to UTC twice a year. Invalid or empty timezone strings fall back to UTC.

Output formats

csv Per-widget data export Every widget on the dashboard produces one CSV sheet with its resolved query result. Useful when the recipient wants to pivot the numbers themselves in Excel or Google Sheets.
png Server-rendered screenshot Headless Chrome renders the dashboard at a fixed viewport and writes the whole thing as a single PNG. Good for email embeds and Slack previews.
pdf Paginated print export Same Chrome renderer, A4 paginated, with a header and timestamp. Best for weekly/monthly summaries that need to live in a shared drive.

Creating a report

Create, update, and delete endpoints require an admin API key or admin-scoped session. Listing and viewing runs is open to any authenticated user.

POST /v1/reports
Authorization: Bearer YOUR_ADMIN_API_KEY
Content-Type: application/json

{
  "dashboard_id": "dash_abc123",
  "name": "Weekly usage summary",
  "schedule_kind": "weekly",
  "hour": 9,
  "day_of_week": 1,
  "timezone": "America/New_York",
  "format": "pdf",
  "email_to": ["cto@example.com", "ops@example.com"],
  "webhook_url": "https://hooks.example.com/emban-reports"
}

The response returns the full report including a computed next_run_at. The scheduler uses that timestamp to decide when to dispatch.

Delivery

Email

Reports send as a MIME multipart/mixed message: a short plain-text body (dashboard name, run timestamp, link back to the app) plus the artifact as an attachment with the correct Content-Type and filename. Multiple email_to recipients go in a single To: header.

Webhook

The webhook receives the raw artifact bytes as the POST body, with content type set to the artifact's MIME and three identification headers:

POST https://hooks.example.com/emban-reports
Content-Type: text/csv | image/png | application/pdf
X-Emban-Report-Id: 17
X-Emban-Report-Name: Weekly usage summary
X-Emban-Dashboard-Id: dash_abc123

<raw artifact bytes>

Unlike alert webhooks, report webhooks do not retry — they run with a 20-second timeout and a single attempt. The artifact is large and delivery is periodic, so the cost-benefit of retrying differs. If your receiver is flaky, terminate quickly so the next scheduled run is fresh.

SSRF protection. Just like alert webhooks, report webhook URLs pointing at private address ranges are rejected before dispatch. Use a public relay if internal delivery is required.

Run history

Every dispatch writes a report_runs row with started_at, finished_at, status (running, success, failed), artifact_bytes, and any error message. This is the source of truth for "did Tuesday's report actually go out":

GET /v1/reports/17/runs

[
  {
    "id": 512,
    "report_id": 17,
    "started_at": "2026-04-21T13:00:01Z",
    "finished_at": "2026-04-21T13:00:07Z",
    "status": "success",
    "artifact_bytes": 184320
  },
  {
    "id": 503,
    "report_id": 17,
    "started_at": "2026-04-14T13:00:02Z",
    "finished_at": "2026-04-14T13:00:19Z",
    "status": "failed",
    "error": "webhook returned 502"
  }
]

Managing reports

# List reports
GET /v1/reports

# Pause/resume (preserves run history)
PATCH /v1/reports/17
{"enabled": false}

# Change time or recipients without recreating
PATCH /v1/reports/17
{"hour": 10, "email_to": ["cfo@example.com"]}

# Delete report (cascades report_runs)
DELETE /v1/reports/17

Design patterns

Pattern 1 Match format to recipient Executives get PDF. Ops teams get PNG embeds in Slack. Analysts get CSV they can pivot. Do not send a 15-widget dashboard as a single PNG to someone who will want to look at one number.
Pattern 2 Schedule in the recipient's timezone A weekly report meant to land on Monday morning in New York needs timezone: "America/New_York", not UTC. Wrong timezone is the most common "my report arrives at 4 AM" bug.
Pattern 3 Publish the dashboard first Reports render the published dashboard, not a draft. If a widget hasn't been published, it will be missing (CSV) or blank (PNG/PDF) in the output.
Pattern 4 Watch report_runs for silent failures Email delivery can fail for weeks without anyone noticing — no one notices the absence of an email. A nightly check that status=success on the last run is cheap insurance.
Pattern 5 Webhook to a storage bucket If recipients change often, send the webhook to an internal endpoint that stashes the artifact in S3 and emails a signed link. You get one delivery target that fans out, plus a searchable archive.

Observability

Webhook attempts are recorded in the shared webhook_deliveries table with source = "report", visible alongside alert deliveries under Admin → Webhooks. Email attempts are not currently surfaced in the admin UI — if email delivery is critical, track it server-side through your SMTP relay's bounce/delivery logs and cross-reference report_runs.

Related: Alerts share the delivery and observability plumbing but fire on condition instead of schedule. See Dashboards for how widgets are composed and published — a report's output quality is bounded by the dashboard it renders.