Skip to content

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 reg token → api key 2 POST report + Idempotency-Key 3 Validate issues[] on fail 4 Store immutable 5 Live feed + dashboards
A source registers once, then posts reports; each is validated, stored immutably, and pushed live.

In the app, create a source (Sources → Add source) to get a one-time registration token (reg_…). Exchange it for an API key:

Terminal window
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.

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.

Terminal window
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.json
{
"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.

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.

{
"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).

{ "id": "notes", "type": "markdown", "content": "## Summary\nAll green." }

Up to 50,000 characters.

{
"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.

{
"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.

{
"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" }
]
}
{ "id": "down", "type": "alert", "title": "API is down", "severity": "critical", "message": "5xx rate at 100%." }

title and severity are required for alerts.

{
"id": "services", "type": "status",
"items": [
{ "key": "api", "label": "API", "status": "healthy" },
{ "key": "web", "label": "Web", "status": "degraded" }
]
}

status: healthy · degraded · down · unknown.

{ "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).

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" }
]
}
CodeMeaning
201 CreatedReport accepted.
200 OKDuplicate — idempotency key already seen; original returned.
400 Bad RequestMissing Idempotency-Key or malformed JSON.
401 UnauthorizedInvalid API key.
403 ForbiddenSource not in scope for that category/stream.
409 ConflictUnknown category/stream slug and autocreate is off.
422 Unprocessable EntitySchema validation failed — see issues[].
429 Too Many RequestsRate limited (per source); honor Retry-After.
LimitValue
Payload size1 MB
Blocks per report50
Table rows / columns1000 / 20
Chart series / points per series10 / 500
Markdown length50,000 chars

Ingestion is rate-limited per source (default 120 requests/minute, configurable via INGEST_RATE_LIMIT / INGEST_RATE_WINDOW).