← All guides

Mock APIs for Home Assistant β€” fire your automations on cue, rehearse unavailable, and see what rest_command really sends

Home 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.

1. A first RESTful sensor in 60 seconds (zero setup)

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.

2. A sensor endpoint 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.

3. Fire your automation triggers from curl

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.

4. Binary sensors from the same endpoint

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.

5. Practice json_attributes_path against a wrapped response

Real 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.

6. The unavailable drill

When 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.

7. Deterministic fail-then-recover

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.

8. What does your setup do when the API rate-limits you?

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.

9. rest_command β€” see exactly what your automation sends

Outbound 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.

10. Rehearse the auth header

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.

11. Honest notes β€” when a mock is the wrong tool

ToolWhere it's the right call
json-server on your LANHA 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 helperIf 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 APIFinal 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.
MockbirdEverything in between: values you set, failures you script, limits you simulate, payloads you inspect β€” over HTTPS, no signup, free, 10k requests/project/day.

12. Cutover 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.

Create a project in one click Β· Docs Β· More guides