unavailable, and see what rest_command really sendsHome Assistant's RESTful sensor turns any JSON endpoint into an entity β and by default it polls that endpoint every 30 seconds, which is 2,880 requests per day per sensor. Building against a real API, that math gets awkward fast: free weather and air-quality tiers cap daily calls well below it, some (PurpleAir being the community's sore example) moved to paid, metered access β and no real API will return an error because you asked, which is exactly what you need while writing the automation that handles one.
A hosted, stateful mock inverts all of it. The endpoint returns whatever you set, changes when you curl a new value into it, fails with any status on demand, and doesn't care if you poll it every 10 seconds all afternoon. To be fair: Home Assistant runs on your LAN, so unlike cloud app builders it can reach a local json-server β but a static local file can't inject failures, simulate rate limits, run deterministic fail-then-recover sequences, or show you the exact bytes your rest_command sent. That control is the point of this page. Every curl below was verified against production before publishing.
The public demo project has a /health route. Add this to configuration.yaml, restart (or reload REST entities), and you have a live polled sensor:
sensor:
- platform: rest
name: mockbird_demo_health
resource: https://mockbird.mockbird.workers.dev/m/demo/health
value_template: "{{ value_json.status }}"
scan_interval: 30
curl 'https://mockbird.mockbird.workers.dev/m/demo/health'
# β {"status":"ok", "service":"mockbird-demo", "time":"2026-09-21T14:23:41.981Z", β¦}
sensor.mockbird_demo_health now reads ok. That proves the plumbing; the interesting part is a sensor whose value you control.
Create a project (one curl, no signup) and give it a readings collection:
curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H 'content-type: application/json' -d '{"name":"ha-lab","blank":true}'
# β {"id":"abc123xyz9", "adminKey":"β¦", β¦} β save both
curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123xyz9/resources \
-H 'content-type: application/json' -H 'x-admin-key: YOUR_ADMIN_KEY' \
-d '{"name":"readings","fields":[{"name":"station","type":"city"},
{"name":"temperature","type":"number"},{"name":"humidity","type":"percent"},
{"name":"aqi","type":"number"},{"name":"online","type":"boolean"}],"seed":3}'
curl -X PUT https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1 \
-H 'content-type: application/json' \
-d '{"station":"Living Room","temperature":21.5,"humidity":47,"aqi":12,"online":true}'
# β {"id":1, "station":"Living Room", "temperature":21.5, "humidity":47, "aqi":12, "online":true}
Now the sensor, with the extra fields exposed as attributes:
sensor:
- platform: rest
name: living_room_temp
unique_id: mock_living_room_temp
resource: https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1
value_template: "{{ value_json.temperature }}"
unit_of_measurement: "Β°C"
device_class: temperature
json_attributes:
- humidity
- aqi
- online
scan_interval: 10
scan_interval: 10 is 8,640 polls/day β comfortably inside the project's 10,000 requests/day free cap, and nobody's quota anxiety applies. The record persists until you change it, and the adminKey never appears in your HA config β it's only for managing the mock's schema, never needed to read it.
This is the drill real APIs can't give you. Say you're writing an overheat alert:
automation:
- alias: "Overheat alert"
trigger:
- platform: numeric_state
entity_id: sensor.living_room_temp
above: 30
action:
- service: notify.notify
data:
message: "Living room is {{ states('sensor.living_room_temp') }} Β°C"
Make it fire, right now, without pointing a hair dryer at a thermometer:
curl -X PATCH https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1 \
-H 'content-type: application/json' -d '{"temperature":31.2}'
# β next poll (β€10 s): sensor crosses 30, the automation runs
PATCH it back to 21.5 and you've tested the recovery side too. Every threshold, hysteresis band, and for: duration in your automations becomes directly exercisable β set the value, watch the trace.
The RESTful binary sensor reads the same record:
binary_sensor:
- platform: rest
name: station_online
resource: https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1
value_template: "{{ value_json.online }}"
device_class: connectivity
curl -X PATCH β¦ -d '{"online":false}' flips it to off on the next poll β your "station went offline" automation is now testable on demand.
json_attributes_path against a wrapped responseReal APIs love envelopes β {"data": {β¦}}, {"result": {β¦}} β and that's what json_attributes_path (a JSONPath expression) is for. Rehearse it by asking the mock for any wrapper shape with ?mock_envelope:
curl 'https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1?mock_envelope=data'
# β {"data":{"id":1, "station":"Living Room", "temperature":21.5, β¦}}
sensor:
- platform: rest
name: living_room_temp_wrapped
resource: https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1?mock_envelope=data
value_template: "{{ value_json.data.temperature }}"
json_attributes_path: "$.data"
json_attributes:
- humidity
- aqi
When the real API's wrapper is decided, mirror it exactly with a custom envelope template (any JSON shape with a "$data" placeholder) and your templates are already correct on cutover day. XML APIs are fair game too β Home Assistant auto-converts XML responses to JSON before your templates see them, and custom routes can serve XML with any content type.
unavailable drillWhen a RESTful sensor's request times out or errors, the entity goes unavailable β and half the dashboards on the forums have a card or automation that mishandles exactly that state. The sensor's timeout defaults to 10 seconds; set it lower than a forced delay and you can produce the state on cue:
sensor:
- platform: rest
name: flaky_temp
resource: https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1?mock_delay=4000
value_template: "{{ value_json.temperature }}"
timeout: 3
Every poll takes 4 s against a 3 s timeout β sensor.flaky_temp is reliably unavailable. Prefer an HTTP error? ?mock_status=503 makes every poll fail instantly instead. Both verified live:
curl -o /dev/null -w '%{time_total}s %{http_code}\n' \
'https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1?mock_delay=4000'
# β 4.8s 200
curl -o /dev/null -w '%{http_code}\n' \
'https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1?mock_status=503'
# β 503
Now write the watchdog you always meant to:
automation:
- alias: "Sensor watchdog"
trigger:
- platform: state
entity_id: sensor.flaky_temp
to: "unavailable"
for: "00:02:00"
action:
- service: notify.notify
data:
message: "flaky_temp has been unavailable for 2 minutes"
β¦and prove it fires by leaving the param on, then prove it doesn't false-positive by taking it off.
Random failures (?mock_chaos) exist, but for testing you usually want a script: fail exactly once, then recover. ?mock_seq serves a fixed status sequence per URL β first poll 503, every later poll the real response:
curl -o /dev/null -w '%{http_code} ' 'https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1?mock_seq=503,200' # 1st β 503
curl -o /dev/null -w '%{http_code} ' 'https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1?mock_seq=503,200' # 2nd β 200
# sticks on the last entry: every later poll β 200
Point the sensor at that URL and you can watch, in one minute of logbook, the entity blip to unavailable and come back β the exact trace your watchdog automation (and its for: debounce) needs to be verified against. Re-run the drill any time by adding &mock_seq_reset=1 to one request. Longer scripts test the debounce window precisely: with 10-second polls, mock_seq=503,503,503,200 is ~30 s of failure β under a 2-minute for:, so no alert; string out eighteen 503s and the alert must fire.
The polite-polling anxiety around metered APIs is really one question: what happens when I get a 429? Find out empirically:
for i in 1 2 3 4 5; do curl -s -o /dev/null -w '%{http_code} ' \
'https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1?mock_ratelimit=3'; done
# β 200 200 200 429 429 (429s carry Retry-After + x-ratelimit-* headers)
?mock_ratelimit=3 allows 3 requests per 60 s per client, then returns real 429s with a Retry-After. Set a sensor's scan_interval below the window and watch the logs: polls that hit 429 turn the sensor unavailable, and it recovers when the window resets β now you know how your dashboard degrades before the real API's billing page teaches you.
rest_command β see exactly what your automation sendsOutbound calls are the other half of REST in Home Assistant, and rest_command's templated payloads are where the escaping bugs live. Add a catch-all route to the project β it accepts anything and logs everything:
curl -X POST https://mockbird.mockbird.workers.dev/api/projects/abc123xyz9/routes \
-H 'content-type: application/json' -H 'x-admin-key: YOUR_ADMIN_KEY' \
-d '{"method":"ANY","path":"/hook/*","status":200,"body":{"ok":true}}'
rest_command:
set_ac:
url: https://mockbird.mockbird.workers.dev/m/abc123xyz9/hook/ac
method: POST
content_type: "application/json"
payload: '{"mode":"{{ mode }}","temp":{{ temp }}}'
Call the service (Developer Tools, or an automation action) with mode: cool, temp: 22, then open the request inspector β dashboard, or the API:
curl https://mockbird.mockbird.workers.dev/api/projects/abc123xyz9/requests -H 'x-admin-key: YOUR_ADMIN_KEY'
# β [{"method":"POST", "path":"/hook/ac", "status":200,
# "body":"{\"mode\":\"cool\",\"temp\":22}",
# "headers":{"content-type":"application/json", β¦}}, β¦]
Method, path, headers, and the exact rendered body β template output verified before it ever hits the real device's cloud API. Unquoted numbers, missing quotes around string substitutions, a wrong content type: all visible here instead of inside a mystery 400 from the vendor.
The RESTful sensor supports basic/digest auth and arbitrary headers. If the real API wants a Bearer token, practice the wiring with mock auth β any email/password returns a real signed JWT:
curl -X POST https://mockbird.mockbird.workers.dev/m/abc123xyz9/auth/login \
-H 'content-type: application/json' -d '{"email":"ha@example.com","password":"anything"}'
# β {"token":"eyJβ¦", β¦}
sensor:
- platform: rest
name: living_room_temp_authed
resource: https://mockbird.mockbird.workers.dev/m/abc123xyz9/readings/1
headers:
Authorization: !secret mock_api_token # secrets.yaml: mock_api_token: "Bearer eyJβ¦"
value_template: "{{ value_json.temperature }}"
Flip the project to protected mode and the header stops being decorative: polls without a valid token get a real 401 (sensor β unavailable), with the token they get 200 β verified both ways while writing this. Set a short token expiry and you can even rehearse the token-went-stale failure mode before the real vendor springs it on you.
| Tool | Where it's the right call |
|---|---|
| json-server on your LAN | HA can reach it (unlike cloud app builders), and for a static happy-path payload it's fine. But there's no failure injection, no rate-limit simulation, no request inspection, no auth β and it's one more process to keep alive on the Pi. |
Template sensor + input_number helper | If you only need a fake entity (not a fake HTTP API), a template sensor driven by a helper slider tests numeric automations with zero external calls. It won't exercise sensor.rest's HTTP path, timeouts, auth, or rest_command at all. |
| The real API | Final validation, always. Schemas drift; a mock proves your configuration and automation logic, not the vendor's current payload. Keep one low-frequency real sensor alongside the mock while developing. |
| Mockbird | Everything in between: values you set, failures you script, limits you simulate, payloads you inspect β over HTTPS, no signup, free, 10k requests/project/day. |
Everything above hangs off the resource: URL. When you're ready for the real API, change that one line (and the real Authorization secret), keep every value_template, attribute mapping, and automation untouched β you already rehearsed the envelope shape in Β§5, so the templates match. If you mirrored the real API's wrapper with a custom envelope, the diff really is just the hostname.
Related: the ESP32/Arduino version of this guide, the Apple Shortcuts version, the Node-RED version, testing loading and error states, simulating rate limits, mock JWT auth, and request bins.