Webhook

How to get a webhook when somebody unplugs your phone

The webhook is the only feature in all of Pubby that sends data out. The moment somebody pulls the cable, it POSTs exactly where you tell it to. When you stop the alarm, it sends a second one. What happens with that is entirely up to you.

You turn it on in Settings, in the Webhook card, and it is a VIP feature. Next to the address field there is a “Send a test” button that does a dry run with the same request body as a real alarm.

What exactly arrives

  • Method. POST, with the header content-type: application/json; charset=utf-8.
  • Address. https:// only. Android has blocked cleartext HTTP for apps since version 9. A request to an http:// address would fail deep in the system at the socket, and you would see no sensible error on screen. The address can be two thousand characters long, so a signed link with a token fits into it comfortably.
  • Authentication. None. Both ends of this pipe are yours, so put the token straight into the address (as a query parameter or in the path).
  • Redirects. Not followed. A 3xx answer would send the request to a server you never typed anywhere, and a redirected POST often turns into a GET, so the body would quietly disappear. A redirect is treated as a bad address and we tell you so right away.
  • Retries. Three attempts within about four seconds. But only when your server does not answer at all. The cable coming out and the local Wi-Fi going away often go hand in hand, so one attempt would not be enough. A fourth attempt two minutes later would do nothing for a phone that may not be on the network much longer.
  • Timeout. Four seconds to connect, eight seconds for the whole call.

The request body

unplugged

{
  "app": "pubby",
  "v": 1,
  "event": "unplugged",
  "id": "9f2c4a1b0d3e",
  "at": "2026-09-03T19:33:20Z",
  "grace": 10,
  "shared": true
}

The cable was pulled. This webhook leaves immediately, before the siren's grace period has run out.

stopped

{
  "app": "pubby",
  "v": 1,
  "event": "stopped",
  "id": "9f2c4a1b0d3e",
  "at": "2026-09-03T19:34:02Z"
}

You stopped the alarm yourself. It comes only if an unplugged was successfully sent before it.

test

{
  "app": "pubby",
  "v": 1,
  "event": "test",
  "id": "41d0c8b7a562",
  "at": "2026-09-03T19:20:05Z"
}

Only the “Send a test” button in the app's settings sends this one.

What the fields mean

  • app – string, always pubby. Useful for a shared endpoint that you post several different things to.
  • v – number, the version of the body format. Adding another field to the JSON will not raise it; the number moves only when an existing field changes its meaning.
  • event – string, one of three values: unplugged, stopped, test.
  • id – string, twelve hexadecimal characters. The unplugged and stopped events from one particular alarm carry the same ID, so you can easily pair them up in your log. If the same id arrives twice for the same event, it is a retry (the phone believed your server had not taken the request).
  • at – string, the time of the event in UTC (ISO 8601), truncated to whole seconds. It is the exact moment the cable came out, not the moment it went onto the network.
  • grace – number, available on unplugged only. It says how many seconds are left before the real siren goes off. Zero means it is already screaming.
  • shared – boolean, on unplugged only. It says whether a shared watch was running at that moment (that is, whether anybody else sees the alarm). false means this POST is the only warning that left the phone.

Try it without the phone

Before you start building logic around this, send the same body from the command line. Your server cannot tell the difference:

# Send a test payload exactly like the app does
curl -i -X POST \
  -H 'content-type: application/json' \
  -d '{"app":"pubby","v":1,"event":"unplugged","id":"9f2c4a1b0d3e","at":"2026-09-03T19:33:20Z","grace":10,"shared":true}' \
  https://your-instance/api/webhook/pubby-9f2c4a1b0d3e

The -i is there so you can see straight away what your server answers. Anything other than a 2xx code counts as a refusal as far as Pubby is concerned, and it will then print that number on the settings screen. To try the other half of the alarm, change the event field in the JSON to stopped and leave the grace and shared fields out entirely.

What is not sent

You will not learn that guarding started or stopped at all. You will not get the battery level or the location of the phone – Pubby does not know it. It never asks for the GPS permission, so it has nothing to send anywhere.

Believing that a system is holding a steady connection for you on the strength of one opening message, with no heartbeat, is a mistake. If Pubby's process died quietly, your dashboard would still be showing “guarding” long afterwards. A real heartbeat does exist in Pubby, but it is called the shared watch – it actively holds a connection to your friend's browser and reads unexpected silence as an alarm.

Home Assistant

Below is an example automation with a webhook trigger. Drop it straight into automations.yaml and change only the webhook_id and the entity you want to control:

automation:
  - alias: Pubby – cable pulled
    triggers:
      - trigger: webhook
        # IMPORTANT: Keep this ID secret
        webhook_id: pubby-9f2c4a1b0d3e
        allowed_methods: [POST]
        # Must be false so it works outside your home network
        local_only: false
    conditions:
      - "{{ trigger.json.event == 'unplugged' }}"
    actions:
      - action: light.turn_on
        target:
          entity_id: light.living_room
        data:
          color_name: red

The address you then type into the app will be https://your-instance/api/webhook/pubby-9f2c4a1b0d3e. Three things to remember about it:

  • The webhook_id is your secret password. Anybody who knows it can trigger your automation remotely, so choose one that is long and random.
  • local_only: false has to be there. By default Home Assistant accepts webhooks from the home network only, so a request from a phone on mobile data would not get through. And because Pubby only ever sends over https://, you need an instance reachable from outside (through Nabu Casa, say, or a reverse proxy of your own).
  • You solve the stopped event by copying the same automation. Just change the condition to stopped and the action to light.turn_off. The webhook_id naturally stays the same, since both events land on the same address.

More recipes

  • ntfy, Pushover, Gotify. A push notification straight to your other phone or your watch. With ntfy the address of your topic is enough and it works immediately – the JSON body simply shows up raw as the text of the message.
  • Node-RED or n8n. Add a node that receives POST requests and put a Switch on the value of the event field right behind it. It is the quickest way to make each event trigger a different, more complex action.
  • Discord, Slack, Telegram. You cannot post to these directly. These services expect their own specific fields (content or text, for instance), while Pubby stubbornly sends its own fixed format. You will need something in between (a Cloudflare Worker or n8n) that simply rewrites the first JSON into the second.
  • A script of your own. Two lines of code that just write a timestamp into a log, if what you want to know is how often somebody touches your phone.

What to watch out for with webhooks

  • The guarded phone is the one sending the message. A webhook is not a replacement for the shared watch. If a thief switches the phone off, or the battery runs out quickly, no further POST (stopped) arrives and the automation is left hanging.
  • A force stop sends no stopped. If the system kills the app (Force Stop), the process disappears at once and has no way to send the event. If you switch something off automatically on stopped, keep a timer behind it just in case.
  • unplugged leaves before the grace period. The grace period applies only to the real siren in the room. The message goes out immediately. That is what the grace value in the body is for – so your server knows whether it is still quiet in that café, or already loud.
  • An unpaired stopped never comes. If you switch the webhook on a moment after the cable is already out, the closing message will not arrive, because that first POST never happened.
  • A quiet watch says nothing. If you guarded all evening and nobody so much as touched the cable, nothing reaches your log from the app at all.

When nothing arrives from the phone

Press the “Send a test” button in settings and read what the app tells you straight away. These three messages have three different fixes:

  • “It arrived!” The target server answered properly with a code from the 2xx series.
  • “Someone is there, but answered 404.” An answer came back, but it was not a 2xx, and that number is its real code. For example: 404 means a wrong path, 401/403 a bad token, 3xx is a redirect (which we ignore) and 5xx an error on your own server.
  • “I knocked and nobody opened.” Nothing responded at all. It could be a hostname that does not exist, a closed port, a home network that is down, or processing that took longer than the hard limit of eight seconds.

And one more thing to look at first: your address has to start with https://. Until you change that, Pubby will not even try to talk and the text field stays outlined in red.