HookPush

Developers

Every channel has a link. Anything that can make an HTTP request can send with it.

Quick start

Get a link: tap Try it on this device on the front page for one that rings your own devices, or copy a channel's Send link from its Settings in the dashboard. Then:

curl
curl -d "Build failed on main" https://hookpush.live/h/YOUR-LINK

The body becomes the message. For a title and a link, send JSON, a form or query parameters:

curl with a title and a link
curl -H "content-type: application/json" \
  -d '{"title":"Your order shipped","body":"Two prints are on their way.","url":"https://shop.example/track/1234"}' \
  https://hookpush.live/h/YOUR-LINK

curl "https://hookpush.live/h/YOUR-LINK?title=Done&body=All+tests+pass"

The answer is {"ok":true,"delivered":1,"devices":1}. A plain GET with no title or body sends nothing; it only reports how many devices are listening, so a link preview never buzzes anyone.

title
Up to 120 characters. Defaults to "Notification".
body
Up to 1,200 characters. Also accepted as message or text.
url
Opened when the notification is tapped. http or https.
delay, at
Send later. See Scheduling.
image, topic, urgent=1, silent=1, replace, expires, idempotency_key
The same options as the API, as flat fields. Buttons (actions) only in JSON.

Keys and links

There are three kinds of secret, each able to do less than the one before:

API key
Authorization: Bearer hpk_…. Everything your account can do, on /api/v1/* and /mcp. Made in Account, shown once.
Send link
/h/<key>. Sends to one channel and nothing else. No header needed.
Automation address
/in/<key>. Feeds one automation and nothing else.

All three can be replaced from the dashboard if they leak; the old one stops working at once. Requests and answers are JSON. Times are ISO 8601. The examples below use $HP for your key:

shell
export HP="hpk_YOUR_KEY"
curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels

Channels

A channel is anything you send to. Who may follow it depends on its visibility:

visibilityWho can followPage
privateOnly your own signed-in devicesNone
inviteAnyone with the invite link/i/<code>
publicAnyone/@handle
Create a channel
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"name":"Lena Park Prints","visibility":"public","handle":"lena","bio":"New prints every Friday.","url":"https://lena.shop","color":"#2B3CFF"}' \
  https://hookpush.live/api/v1/channels

The answer is the channel, with its id, page_url, invite_url, publish_url (the send link), the embed tag, followers and topics. A handle is 3 to 30 characters of a-z, 0-9 and _, and is needed for invite and public.

List, read, change, delete
curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels
curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID

curl -X PATCH -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"visibility":"invite"}' https://hookpush.live/api/v1/channels/CHANNEL_ID

curl -X DELETE -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID

Changing visibility keeps everyone who already follows. Deleting removes the page, followers, history and automations.

Picture, new send link, new invite link
# a square PNG, JPEG or WebP, up to 512 KB
curl -X PUT -H "Authorization: Bearer $HP" -H "content-type: image/png" \
  --data-binary @avatar.png https://hookpush.live/api/v1/channels/CHANNEL_ID/avatar
curl -X DELETE -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID/avatar

curl -X POST -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID/rotate
curl -X POST -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID/rotate-invite

A link made on the front page, without an account, can join your account as a private channel. The token is the part after /h/:

Add a front-page link to your account
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"token":"YOUR-LINK"}' https://hookpush.live/api/v1/channels/claim

Sending

Only body or title is needed. Everything else is optional.

Send now
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"title":"Drop is live","body":"Tap to shop the Friday prints.","url":"https://lena.shop/drop"}' \
  https://hookpush.live/api/v1/channels/CHANNEL_ID/messages

A message sent now answers 201; a scheduled one 202. Both return the message:

{ "id": "m_…", "status": "sent", "title": "Drop is live", "body": "…",
  "source": "api", "created": "…", "sent_at": "…",
  "stats": { "targeted": 1240, "accepted": 1236, "failed": 1, "removed": 3, "shown": 0, "tapped": 0 } }

Before sending, POST /api/v1/preview with the same body tells you what each platform will show and how big the message is. The dashboard's live preview uses it.

Preview
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"title":"Drop is live","image":"MEDIA_ID","actions":[{"title":"Shop now","url":"https://lena.shop"}]}' \
  https://hookpush.live/api/v1/preview
# {"payload_bytes":612,"platforms":{"iphone":["title","body","icon"],…},"warnings":["iPhone and Mac do not show images."]}

Topics

Followers can pick topics, like "Drops" or "Outages". Someone who picks none gets everything, including topics you add later. A message with a topic reaches only people who chose it, or chose none. Up to 12 per channel.

Add, rename, reorder, delete, send to one
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"name":"Tour dates"}' https://hookpush.live/api/v1/channels/CHANNEL_ID/topics

curl -X PATCH -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"name":"Shows","position":0}' https://hookpush.live/api/v1/channels/CHANNEL_ID/topics/TOPIC_ID

curl -X DELETE -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID/topics/TOPIC_ID

curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"body":"Leeds added: 14 March","topic":"TOPIC_ID"}' \
  https://hookpush.live/api/v1/channels/CHANNEL_ID/messages

On a send link, topic also accepts the topic's name: -d "topic=Tour dates".

Scheduling

Add delay or at. A delay is counted from the moment the request arrives, so clocks and timezones can't disagree: 90 (seconds), 10m, 2h, 3d, 1w, 1h30m.

An at must carry its timezone, as Z or an offset. A time without one is refused, and so is a time in the past. Up to a year ahead.

Later
curl -d "body=Stand-up in 5" -d "delay=25m" https://hookpush.live/h/YOUR-LINK

curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"title":"Doors open","body":"See you inside.","at":"2026-10-24T19:00:00+01:00"}' \
  https://hookpush.live/api/v1/channels/CHANNEL_ID/messages
Scheduled messages
curl -H "Authorization: Bearer $HP" "https://hookpush.live/api/v1/channels/CHANNEL_ID/messages?status=scheduled"

curl -X POST -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/messages/MESSAGE_ID/send-now
curl -X DELETE -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/messages/MESSAGE_ID

# this one and every later one in its repeat series
curl -X DELETE -H "Authorization: Bearer $HP" "https://hookpush.live/api/v1/messages/MESSAGE_ID?series=1"

# on a send link
curl https://hookpush.live/h/YOUR-LINK/scheduled
curl -X DELETE https://hookpush.live/h/YOUR-LINK/scheduled/SCHEDULED_ID

Repeats

A repeat needs the first time in at and an IANA timezone in repeat.tz, so "every Friday at noon" stays at noon when the clocks change. every is day, weekday, week or month. Stop it with until or count, or leave both out.

Every Friday at noon, ten times
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"title":"Friday prints are live","body":"Tap to see them first.","at":"2026-10-23T12:00:00+01:00","repeat":{"every":"week","tz":"Europe/London","count":10}}' \
  https://hookpush.live/api/v1/channels/CHANNEL_ID/messages

Every message in a repeat shares a series id. DELETE /api/v1/messages/MESSAGE_ID?series=1 cancels the rest of the series.

Warm-ups

A warm-up is a second, shorter message sent a set time before the main one: "Drop in 15 minutes", then "Drop is live". It needs at or delay.

Heads-up 15 minutes before
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"title":"Drop is live","at":"2026-10-24T12:00:00-05:00","warmup":{"before":"15m","title":"Drop in 15 minutes","body":"Get ready."}}' \
  https://hookpush.live/api/v1/channels/CHANNEL_ID/messages
# 202 {"message":{…},"warmup":{…}}

Images, buttons, expiry

image
An https address or a media id. Android and Windows show it large; iPhone and Mac don't.
actions
Up to two buttons, each {"title","url"}, titles up to 24 characters.
expires_in, expires_at
After this, devices that were offline skip it. For flash sales and live events.
urgent
Delivered at once, even when a phone is saving battery.
silent
No sound or vibration where the device allows it.
replace
A later message with the same label replaces this one instead of stacking up. For scores and statuses.
callback_url
Gets one POST of the message object when sending finishes.
Upload an image, then send with buttons
# PNG, JPEG, WebP or GIF up to 1 MB; answers {"id":"…","url":"/m/….jpg"}
curl -H "Authorization: Bearer $HP" -H "content-type: image/jpeg" \
  --data-binary @drop.jpg https://hookpush.live/api/v1/media

curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"title":"Flash sale: 30% off seconds","body":"Until midnight.","image":"MEDIA_ID",
       "actions":[{"title":"Shop now","url":"https://lena.shop/seconds"},{"title":"Not today","url":"https://lena.shop"}],
       "expires_in":"6h","urgent":true,"replace":"sale"}' \
  https://hookpush.live/api/v1/channels/CHANNEL_ID/messages

Everything must fit in 4 KB once encoded: titles up to 120 characters, bodies up to 1,200.

History and stats

Every message keeps its counts: accepted by the push services, failed, removed (devices the browser had switched off, now deleted), shown and tapped. Shown and tapped are counted on the device with no record of who. Message text is kept for 90 days, then only the counts.

Messages, one message, followers, daily stats
curl -H "Authorization: Bearer $HP" "https://hookpush.live/api/v1/channels/CHANNEL_ID/messages?limit=50"
# {"messages":[…],"next":"m_…"}  pass ?before=m_… for the next page; ?status=scheduled|sent|failed|cancelled

curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/messages/MESSAGE_ID
curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID/followers
# {"total":1203,"by_topic":{"t1":900},"by_platform":{"android":570,"iphone":384,"mac":136,"windows":112,"linux":25,"other":13}}
curl -H "Authorization: Bearer $HP" "https://hookpush.live/api/v1/channels/CHANNEL_ID/stats?days=30"
# {"days":[{"date":"2026-10-17","followers":1203,"sent":1,"shown":1101,"tapped":212},…]}

Your own devices on a channel can be listed and removed. Adding one needs a browser, so it happens in the dashboard.

Your devices
curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID/devices
curl -X DELETE -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID/devices/DEVICE_ID

Sending safely twice

Networks fail halfway. Send an Idempotency-Key header (or the idempotency_key field) and a retry with the same key on the same channel within 24 hours returns the first result instead of sending again.

Retry without double-sending
curl -H "Authorization: Bearer $HP" -H "Idempotency-Key: order-1234-shipped" -H "content-type: application/json" \
  -d '{"title":"Your order shipped","body":"Order 1234 is on its way."}' \
  https://hookpush.live/api/v1/channels/CHANNEL_ID/messages

Automations

An automation turns something that happens elsewhere into a message. Each has its own address; any service can POST JSON, a form or text to it (up to 64 KB). A template picks the fields:

{{product.title}}
Inserts a value. Dots go into objects; numbers into lists: {{variants.0.price}}.
{{customer.name|a customer}}
Uses the text after | when the value is empty.
{{amount:cents}}, :upper, :lower
Formats a value: 2500 becomes 25.00; text in capitals or small letters.
Filter
{"path":"workflow_run.conclusion","op":"equals","value":"failure"}. op is equals, contains or exists. Events that don't match are kept but not sent.

Presets come ready with a template, a filter and setup steps: generic, shopify_product, shopify_order, github (failed workflows), github_release, github_issue, stripe (payments), stripe_dispute, zapier, make, uptime (Uptime Kuma, Better Stack, Healthchecks.io) and patreon. GET /api/v1/presets lists them with a sample event each.

Create, test, turn on
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"preset":"github","name":"Failed builds","enabled":false}' \
  https://hookpush.live/api/v1/channels/CHANNEL_ID/sources
# {"id":"src_…","inbound_url":"https://hookpush.live/in/…",…}  shown only in this answer: paste it into the other service

curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/sources/SOURCE_ID/events
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" -d '{}' \
  https://hookpush.live/api/v1/sources/SOURCE_ID/test

curl -X PATCH -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"enabled":true,"template":{"title":"{{workflow_run.name}} failed","body":"{{repository.full_name}}","url":"{{workflow_run.html_url}}"}}' \
  https://hookpush.live/api/v1/sources/SOURCE_ID
Send an event yourself
curl -H "content-type: application/json" \
  -d '{"product":{"title":"Harbour at dusk","stock":40}}' \
  https://hookpush.live/in/AUTOMATION_KEY
# 202 {"ok":true,"sent":true}   sent is false when the filter skips it or the automation is off

/test renders the latest event (or a sample you pass) and sends nothing. /events returns the last 20 events, kept for 7 days, with their field names. The full address is stored hashed, so later reads show only its start (inbound_hint); rotate replaces it and returns the new one once.

Presets, list, new address, delete
curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/presets
curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/channels/CHANNEL_ID/sources

curl -X POST -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/sources/SOURCE_ID/rotate
curl -X DELETE -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/sources/SOURCE_ID

Feeds

A feed automation watches a page for new posts: a YouTube channel, a Substack, a blog, a podcast or a status page. Give it the page's address and HookPush finds the RSS or Atom feed (or answers 400 if there is none). It checks every 10 minutes, skips what already exists on the first check, and sends at most 3 new posts per check. Presets youtube, substack and feed come with fitting templates.

New YouTube videos
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
  -d '{"preset":"feed","name":"New videos","feed_url":"https://www.youtube.com/@yourchannel","enabled":true,
       "template":{"title":"New video: {{item.title}}","body":"{{item.summary}}","url":"{{item.link}}","image":"{{item.image}}"}}' \
  https://hookpush.live/api/v1/channels/CHANNEL_ID/sources

Feed fields: item.title, item.link, item.summary, item.image, item.author and feed.title.

Button for your site

Put a "Get notifications" button on any page. It takes on your site's font and colours.

HTML
<script src="https://hookpush.live/embed.js" data-channel="CHANNEL_ID" async></script>

With only this tag, the button opens the channel's page. To let visitors follow without leaving your site, and get notifications under your site's name, also serve a file at /sw.js on your site containing this one line:

/sw.js on your site
importScripts('https://hookpush.live/sw.js');

iPhone visitors still go to the channel page, because Apple only allows notifications from sites on the Home Screen.

data-label, data-label-on
The button's text when off and on.
data-target="#my-button"
Use your own button instead.
data-sw="/sw.js"
Where your service worker lives.
HookPush.subscribe(), HookPush.unsubscribe(), HookPush.state
Script control. A hookpush:change event fires on document.

API keys

Make keys in Account, one per program, named after where it is used. A key is shown once and stored hashed. Delete it and anything using it stops at once.

List and delete keys
curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/keys
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" -d '{"name":"Home server"}' https://hookpush.live/api/v1/keys
curl -X DELETE -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/keys/KEY_ID

MCP for agents

AI agents can make channels and send through the MCP server at /mcp (streamable HTTP, JSON answers). It uses an API key. Without one, an agent can still connect and call create_account or sign_in, which return a key.

Claude Code
claude mcp add --transport http hookpush https://hookpush.live/mcp \
  --header "Authorization: Bearer hpk_YOUR_KEY"
Other MCP clients
{
  "mcpServers": {
    "hookpush": {
      "type": "http",
      "url": "https://hookpush.live/mcp",
      "headers": { "Authorization": "Bearer hpk_YOUR_KEY" }
    }
  }
}
create_account, sign_in
An email and a password; each returns a new API key. Need no key.
notify_me
Sends to your first private channel, making "My devices" if there is none. With no device on it yet, the answer includes add_device_url: a one-time link to open on your phone, no sign-in needed.
add_device_link
A one-time link that adds a phone to a channel as your own device. Works once, for 24 hours.
send_notification
A channel id or handle, plus any message fields above.
list_channels, create_channel, get_channel, update_channel, add_topic
The same objects as the REST API. get_channel includes the link people follow it by.
list_messages, get_message, cancel_message, send_now
History and scheduled messages.
get_account
Limits and usage.

Agents without MCP can read llms.txt, which explains the send link in plain text.

Your account

Read, change, delete
curl -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/me
curl -X PATCH -H "Authorization: Bearer $HP" -H "content-type: application/json" -d '{"email":"you@example.com"}' https://hookpush.live/api/v1/me
curl -X DELETE -H "Authorization: Bearer $HP" https://hookpush.live/api/v1/me

Deleting the account deletes every channel it owns, with followers and history. Links made on the front page without an account are not affected. People sign in in the dashboard, with a passkey or an email and a password.

An account for a program or agent
curl -H "content-type: application/json" -d '{"email":"agent@example.com","password":"at least 10 characters"}' https://hookpush.live/api/v1/accounts
curl -H "content-type: application/json" -d '{"email":"agent@example.com","password":"at least 10 characters"}' https://hookpush.live/api/v1/login

Both need no key and answer with a new one in api_key.key, shown once. The first makes the account; the second signs in to an existing one.

Limits

During the beta, per account and channel:

Channels
10 per account
Followers
5,000 per channel; 20 devices on a private channel
Topics
12 per channel
Sends
60 a minute per channel
Scheduled
500 waiting per channel, up to a year ahead
Automations
20 per channel
API keys
10 per account
Images
1 MB each, 50 MB in all
New links
20 an hour from one address without an account

Errors

Errors are a plain sentence, and name the field when one is at fault:

{ "error": "The time needs a timezone.", "field": "at" }
400
Something in the request is wrong.
401
No key, or not signed in.
403
Not allowed, or the channel is suspended.
404
No such channel, message or link.
409
A conflict or a limit, like a taken handle.
413
Too big.
429
Slow down: wait retry_after seconds (also in the Retry-After header).