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 -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 -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
messageortext. 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:
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:
| visibility | Who can follow | Page |
|---|---|---|
private | Only your own signed-in devices | None |
invite | Anyone with the invite link | /i/<code> |
public | Anyone | /@handle |
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/channelsThe 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.
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_IDChanging visibility keeps everyone who already follows. Deleting removes the page, followers, history and automations.
# 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-inviteA link made on the front page, without an account, can join your account as a private channel. The token is the part after /h/:
curl -H "Authorization: Bearer $HP" -H "content-type: application/json" \
-d '{"token":"YOUR-LINK"}' https://hookpush.live/api/v1/channels/claimSending
Only body or title is needed. Everything else is optional.
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/messagesA 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.
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.
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/messagesOn 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.
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/messagescurl -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.
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/messagesEvery 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.
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.
# 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/messagesEverything 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.
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.
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.
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/messagesAutomations
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"}.opisequals,containsorexists. 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.
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_IDcurl -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.
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.
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/sourcesFeed 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.
<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:
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:changeevent fires ondocument.
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.
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_IDMCP 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 mcp add --transport http hookpush https://hookpush.live/mcp \ --header "Authorization: Bearer hpk_YOUR_KEY"
{
"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_channelincludes 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
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/meDeleting 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.
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/loginBoth 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_afterseconds (also in theRetry-Afterheader).