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

  1. In the portal, open Custom Content and create a source (e.g. “Internet Health”).
  2. Copy the API token shown once on creation, and the publish endpoint.
  3. 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
  }'
  1. Back in the portal, add the source to one or more displays. That's it — the next publish appears on the panel within seconds.
The exact publish URL and token are shown on your source's page in the portal. Copy them from there — the examples below use placeholders.

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"
}
FieldReqNotes
messageyesThe body text (≤ 280 chars). Scrolls if it doesn't fit.
titlenoHeading (≤ 100 chars). Coloured by status.
statusnoSee 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"
}
FieldReqNotes
valueyesString or number (≤ 32 chars), shown large.
unitno≤ 12 chars.
subtitleno≤ 64 chars.
trendnoup, down, or flat — draws an arrow.
title, statusnoThe 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%" }
  ]
}
FieldReqNotes
statusyesDrives the card colour. See status.
messagenoHeadline (≤ 160 chars).
detailsnoUp to 6 {label, value} pairs (each ≤ 24 chars); the panel shows the first that fit.
titleno≤ 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" }
  ]
}
FieldReqNotes
itemsyes1–10 rows of {label, value, status?} (label/value ≤ 24 chars); the panel shows the first that fit.
titleno≤ 100 chars.

Common fields

Any template may also include these:

FieldNotes
statusOne of neutral, info, good, warning, critical. Maps to colour.
ttl_secondsHow long this content stays fresh (see below). Mutually exclusive with expires_at.
expires_atISO-8601 instant after which the content is stale. Mutually exclusive with ttl_seconds.
published_atISO-8601 time the source system generated the data (optional).
external_referenceYour own event/request id (≤ 150 chars), echoed for troubleshooting.
iconA 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:

BehaviourEffect
SHOW_OFFLINEShow a standard “offline” card (default).
SHOW_FALLBACKShow the fallback title/message you set on the source.
SHOW_STALEKeep showing the content, marked stale.
KEEP_LAST_VALUEKeep showing the content with no stale marker.
HIDEDrop the source from the rotation until it publishes again.

Limits

LimitValue
Max request size16 KB
Requests per minute, per source30
Requests per hour, per source500
Sources per account10
Active tokens per source2

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_…"
  }
}
StatusMeaningRetry?
200Accepted — identical to current content—
201Accepted — new content—
400Malformed JSON / requestNo — fix it
401Missing / invalid / revoked tokenNo
403Token can't access this sourceNo
404Source not foundNo
413Payload too largeNo
422Valid JSON, invalid contentNo — fix it
429Rate limit exceededYes, after Retry-After
5xxServer errorYes, with backoff

All endpoints

All under https://portal.pulseboard.ca/api/v1/content-sources/{source_id}/, authenticated with your source token.

EndpointPurpose
POST/publishReplace current content.
POST/validateCheck a payload without publishing.
GET/currentRead the current content (testing/debugging).
DEL/currentClear current content (activates the fallback).
GET/healthSource 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.