Custom Content API
Publish your own messages, metrics, and monitoring status to a PulseBoard over a simple HTTPS API, then add the source to any of your displays.
Overview
A Custom Content Source is a reusable feed you own. You create one in the portal, PulseBoard gives you a secret API token and a publish URL, and any system that can make an outbound HTTPS request — Home Assistant, a monitoring tool, a cron script — posts structured JSON to it. The most recent publication is what your displays show. Because publishing is outbound HTTPS, it works from behind a home firewall with no inbound ports.
You send structured content (a small set of templates), not markup — so it renders cleanly and consistently at any panel size, and PulseBoard keeps control of fonts, colour and layout. Custom Content is a PulseBoard Plus feature.
Quick start
- In the portal, open Custom Content and create a source (e.g. “Internet Health”).
- Copy the API token shown once on creation, and the publish endpoint.
- POST a JSON payload to the endpoint with the token:
curl -X POST \
https://portal.pulseboard.ca/api/v1/content-sources/pbcs_yoursource/publish \
-H "Authorization: Bearer pb_live_your_token" \
-H "Content-Type: application/json" \
-d '{
"template": "metric",
"title": "Internet",
"value": "842",
"unit": "Mbps",
"subtitle": "12 ms latency",
"status": "good",
"ttl_seconds": 600
}'
- Back in the portal, add the source to one or more displays. That's it — the next publish appears on the panel within seconds.
Authentication
Every request carries a source-scoped bearer token in the Authorization header:
Authorization: Bearer pb_live_xxxxxxxxxxxxxxxxx
- Tokens are generated per source and shown once — store the value when you create it.
- A token can only publish to its own source, so a leak is contained.
- Up to 2 active tokens per source, so you can rotate without downtime: issue a new one, switch your integration over, then revoke the old.
- Revoke a token any time from the source page; a revoked token immediately returns
401.
Publishing content
POST/api/v1/content-sources/{source_id}/publish
Replaces the source's current content. The body is a JSON object with a template field
and that template's fields (below), plus optional common fields. A successful
publish returns 201; re-sending identical content returns 200 (it's treated as a
heartbeat and doesn't re-render).
Example response:
{
"publication_id": "8f1c…",
"source_id": "pbcs_yoursource",
"accepted": true,
"unchanged": false,
"published_at": "2026-07-21T12:30:01Z",
"expires_at": "2026-07-21T12:40:01Z",
"request_id": "req_…"
}
Template: message
A short message, reminder, or alert.
{
"template": "message",
"title": "Reminder",
"message": "Garbage day is tomorrow",
"status": "info"
}
| Field | Req | Notes |
|---|---|---|
message | yes | The body text (≤ 280 chars). Scrolls if it doesn't fit. |
title | no | Heading (≤ 100 chars). Coloured by status. |
status | no | See status. |
Template: metric
A primary value with an optional unit and trend.
{
"template": "metric",
"title": "Basement",
"value": "21.4",
"unit": "°C",
"subtitle": "Humidity 44%",
"trend": "up",
"status": "good"
}
| Field | Req | Notes |
|---|---|---|
value | yes | String or number (≤ 32 chars), shown large. |
unit | no | ≤ 12 chars. |
subtitle | no | ≤ 64 chars. |
trend | no | up, down, or flat — draws an arrow. |
title, status | no | The value is coloured by status. |
Template: status
A service or system health card with optional detail rows.
{
"template": "status",
"title": "Home Network",
"status": "warning",
"message": "WAN latency is elevated",
"details": [
{ "label": "Latency", "value": "87 ms" },
{ "label": "Packet Loss", "value": "2.1%" }
]
}
| Field | Req | Notes |
|---|---|---|
status | yes | Drives the card colour. See status. |
message | no | Headline (≤ 160 chars). |
details | no | Up to 6 {label, value} pairs (each ≤ 24 chars); the panel shows the first that fit. |
title | no | ≤ 100 chars. |
Template: list
Several short label/value rows, each optionally coloured by its own status.
{
"template": "list",
"title": "Server Status",
"items": [
{ "label": "Web", "value": "Online", "status": "good" },
{ "label": "Database", "value": "Degraded", "status": "warning" },
{ "label": "Backup", "value": "Failed", "status": "critical" }
]
}
| Field | Req | Notes |
|---|---|---|
items | yes | 1–10 rows of {label, value, status?} (label/value ≤ 24 chars); the panel shows the first that fit. |
title | no | ≤ 100 chars. |
Common fields
Any template may also include these:
| Field | Notes |
|---|---|
status | One of neutral, info, good, warning, critical. Maps to colour. |
ttl_seconds | How long this content stays fresh (see below). Mutually exclusive with expires_at. |
expires_at | ISO-8601 instant after which the content is stale. Mutually exclusive with ttl_seconds. |
published_at | ISO-8601 time the source system generated the data (optional). |
external_reference | Your own event/request id (≤ 150 chars), echoed for troubleshooting. |
icon | A small icon shown on the card — see Icons. |
Icons
Add an icon to any template to show a small graphic on the left of the card —
handy for telling cards apart at a glance without reading. Send either a name or a
supported emoji; both resolve to the same curated bitmap, sized for the panel:
{ "template": "status", "title": "Dog meds", "status": "good", "icon": "dog" }
{ "template": "status", "title": "My meds", "status": "warning", "icon": "💊" } // same as "pill"
Supported icons (name — emoji):
| pill 💊 | dog 🐶 | cat 🐱 | check ✅ | cross ❌ | warning ⚠️ | heart ❤️ |
| bell 🔔 | clock 🕐 | calendar 📅 | home 🏠 | thermometer 🌡️ | battery 🔋 | bolt ⚡ |
| signal 📶 | droplet 💧 | plant 🌱 | package 📦 | car 🚗 | person 🧑 | moon 🌙 |
| sun ☀️ | star ⭐ | fire 🔥 | leaf 🍃 | gear ⚙️ | lock 🔒 | flag 🚩 |
An unsupported icon returns 422. The set is curated to stay legible on the LED grid;
tell us what you'd like added. Icons stay on the card even when a source goes stale, so you can see
which one is offline.
Text & characters
Text fields (title, message, value, labels…) render in a bitmap
LED font that covers standard ASCII — letters, digits, and common punctuation.
A character with no glyph (an emoji or accented letter in a text field) renders as a blank
space, not an error. For graphics, use the icon field rather than
putting an emoji in the text.
Freshness & expiry
Monitoring data goes stale if the source stops publishing. Set a freshness window with
ttl_seconds (or a source-level default in the portal); when it lapses the display
follows the source's configured behaviour:
| Behaviour | Effect |
|---|---|
SHOW_OFFLINE | Show a standard “offline” card (default). |
SHOW_FALLBACK | Show the fallback title/message you set on the source. |
SHOW_STALE | Keep showing the content, marked stale. |
KEEP_LAST_VALUE | Keep showing the content with no stale marker. |
HIDE | Drop the source from the rotation until it publishes again. |
Limits
| Limit | Value |
|---|---|
| Max request size | 16 KB |
| Requests per minute, per source | 30 |
| Requests per hour, per source | 500 |
| Sources per account | 10 |
| Active tokens per source | 2 |
Rate-limited requests return 429 with a Retry-After header.
Errors
Errors return a JSON envelope:
{
"error": {
"code": "INVALID_TEMPLATE",
"message": "The template 'dashboard' is not supported.",
"field": "template",
"request_id": "req_…"
}
}
| Status | Meaning | Retry? |
|---|---|---|
200 | Accepted — identical to current content | — |
201 | Accepted — new content | — |
400 | Malformed JSON / request | No — fix it |
401 | Missing / invalid / revoked token | No |
403 | Token can't access this source | No |
404 | Source not found | No |
413 | Payload too large | No |
422 | Valid JSON, invalid content | No — fix it |
429 | Rate limit exceeded | Yes, after Retry-After |
5xx | Server error | Yes, with backoff |
All endpoints
All under https://portal.pulseboard.ca/api/v1/content-sources/{source_id}/, authenticated with your source token.
| Endpoint | Purpose |
|---|---|
POST/publish | Replace current content. |
POST/validate | Check a payload without publishing. |
GET/current | Read the current content (testing/debugging). |
DEL/current | Clear current content (activates the fallback). |
GET/health | Source status without returning content. |
Home Assistant
A rest_command plus an automation publishes a sensor on a schedule:
rest_command:
publish_pulseboard_temperature:
url: "https://portal.pulseboard.ca/api/v1/content-sources/pbcs_yoursource/publish"
method: POST
headers:
authorization: "Bearer !secret pulseboard_api_token"
content-type: "application/json"
payload: >
{
"template": "metric",
"title": "Living Room",
"value": "{{ states('sensor.living_room_temperature') }}",
"unit": "°C",
"subtitle": "Humidity {{ states('sensor.living_room_humidity') }}%",
"ttl_seconds": 600
}
automation:
- alias: Publish living room temperature to PulseBoard
trigger:
- platform: state
entity_id: sensor.living_room_temperature
- platform: time_pattern
minutes: "/5"
action:
- service: rest_command.publish_pulseboard_temperature
Questions? Contact us or see the FAQ.
Icons designed by OpenMoji — the open-source emoji and icon project. License: CC BY-SA 4.0.