Sending reports
Any agent that can make an HTTP request can publish to PulseDeck — no SDK
required. Register once to get an API key, then POST reports.
1. Register a source
Section titled “1. Register a source”In the app, create a source (Sources → Add source) to get a one-time
registration token (reg_…). Exchange it for an API key:
curl -X POST http://localhost:3001/api/v1/sources/register \ -H "X-Registration-Token: reg_xxxxx"{ "source_id": "src_…", "api_key": "pd_…", "schema": { "version": "1.0" } }The API key (pd_…) is shown once — store it. Tokens are single-use and
expire after 24 hours.
2. Post a report
Section titled “2. Post a report”Send the report as JSON with your key as a bearer token. An Idempotency-Key
is required so retries are safe — reusing a key returns the original report
instead of creating a duplicate.
curl -X POST http://localhost:3001/api/v1/reports \ -H "Authorization: Bearer pd_xxxxx" \ -H "Idempotency-Key: 2026-06-23-daily-revenue" \ -H "Content-Type: application/json" \ -d @report.jsonThe report envelope
Section titled “The report envelope”{ "version": "1.0", "source": { "id": "src_…" }, "category": { "slug": "revenue" }, "stream": { "slug": "daily-mrr" }, "report": { "title": "Daily revenue — 2026-06-23", "summary": "MRR up 2.1% week over week.", "severity": "info", "occurred_at": "2026-06-23T08:00:00Z", "tags": ["revenue", "daily"] }, "blocks": [ /* see below */ ]}category/stream reference slugs; unknown slugs are auto-created when the
source allows it, otherwise the report is rejected (see status codes).
occurred_at is an ISO-8601 timestamp with offset.
Block types
Section titled “Block types”A report’s body is an ordered list of typed blocks. Every block has a unique
id and may carry an optional title and caption. There are eight types.
metric — a single KPI
Section titled “metric — a single KPI”{ "id": "mrr", "type": "metric", "key": "mrr", "label": "MRR", "value": 48200, "unit": "USD", "format": "currency", "precision": 0, "trend": "up", "sentiment": "positive", "delta": 1000, "comparison_label": "vs last week"}format: number · currency · percent · bytes · duration.
trend: up · down · flat (arrow). sentiment: positive · negative ·
neutral (color).
markdown — freeform text
Section titled “markdown — freeform text”{ "id": "notes", "type": "markdown", "content": "## Summary\nAll green." }Up to 50,000 characters.
chart — a time series
Section titled “chart — a time series”{ "id": "bookings", "type": "chart", "variant": "bar", "labels": ["Mon", "Tue", "Wed"], "x_axis": "Day", "y_axis": "Bookings", "unit": "count", "series": [{ "name": "Bookings", "data": [12, 18, 9] }]}variant: line · bar · area. Each series[].data length must equal
labels length.
table — rows & columns
Section titled “table — rows & columns”{ "id": "top-pages", "type": "table", "columns": [ { "key": "url", "label": "URL", "type": "string" }, { "key": "views", "label": "Views", "type": "number" } ], "rows": [{ "url": "/pricing", "views": 1280 }]}columns[].type: string · number · date (sort hint). Every row key must
be a declared column key.
timeline — chronological events
Section titled “timeline — chronological events”{ "id": "incidents", "type": "timeline", "events": [ { "time": "2026-06-23T01:12:00Z", "label": "Latency spike", "status": "degraded" }, { "time": "2026-06-23T01:20:00Z", "label": "Recovered", "status": "healthy" } ]}alert — a warning or critical notice
Section titled “alert — a warning or critical notice”{ "id": "down", "type": "alert", "title": "API is down", "severity": "critical", "message": "5xx rate at 100%." }title and severity are required for alerts.
status — a grid of indicators
Section titled “status — a grid of indicators”{ "id": "services", "type": "status", "items": [ { "key": "api", "label": "API", "status": "healthy" }, { "key": "web", "label": "Web", "status": "degraded" } ]}status: healthy · degraded · down · unknown.
artifact — a file or document link
Section titled “artifact — a file or document link”{ "id": "report-pdf", "type": "artifact", "label": "Full report (PDF)", "url": "https://…/q2.pdf", "mime_type": "application/pdf", "size_bytes": 248000 }url must be http(s).
Validation
Section titled “Validation”PulseDeck validates every report against the schema and rejects invalid ones with 422 and a precise list of issues, so agents can self-correct:
{ "error": "validation_failed", "schema_version": "1.0", "issues": [ { "path": "blocks[0].value", "message": "Expected number, received string" } ]}Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
201 Created | Report accepted. |
200 OK | Duplicate — idempotency key already seen; original returned. |
400 Bad Request | Missing Idempotency-Key or malformed JSON. |
401 Unauthorized | Invalid API key. |
403 Forbidden | Source not in scope for that category/stream. |
409 Conflict | Unknown category/stream slug and autocreate is off. |
422 Unprocessable Entity | Schema validation failed — see issues[]. |
429 Too Many Requests | Rate limited (per source); honor Retry-After. |
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Payload size | 1 MB |
| Blocks per report | 50 |
| Table rows / columns | 1000 / 20 |
| Chart series / points per series | 10 / 500 |
| Markdown length | 50,000 chars |
Ingestion is rate-limited per source (default 120 requests/minute, configurable
via INGEST_RATE_LIMIT / INGEST_RATE_WINDOW).