Skip to content

Hooks and recipes / Hooks

Hooks

A URL per recipient that turns a plain text, Discord, Slack or Apprise request into a notification.

A hook is a secret URL that belongs to one recipient. Anything that can call a webhook can send that recipient a notification through it: no API key, no JSON to build and no code to write. Monitoring tools, CI pipelines and shell scripts are the usual callers. Ready-made setups are in Recipes for tools and Shell one-liners.

⫻

plain text

curl https://api.meslzy.com/push/v1/hooks/hk_9Qm2Xv7Lc4Tz8Rb1Nw6Ks3Jd5Yf0Hg2P \  -H "Title: Backup finished" \  -H "Priority: high" \  -H "Tags: backup,db-1" \  -H "Click: https://example.com/backups/812" \  -d "db-1 was copied to cold storage in 4 minutes."

What a hook is#

  • A hook is owned by exactly one recipient. It can only notify that recipient, never another one in the project.
  • Its token is hk_ followed by 32 letters and digits, and it sits in the URL path. The same token works with four request shapes, picked by the end of the path:

⫻

hook URLs

https://api.meslzy.com/push/v1/hooks/hk_9Qm2Xv7Lc4Tz8Rb1Nw6Ks3Jd5Yf0Hg2Phttps://api.meslzy.com/push/v1/hooks/hk_9Qm2Xv7Lc4Tz8Rb1Nw6Ks3Jd5Yf0Hg2P/discordhttps://api.meslzy.com/push/v1/hooks/hk_9Qm2Xv7Lc4Tz8Rb1Nw6Ks3Jd5Yf0Hg2P/slackhttps://api.meslzy.com/push/v1/hooks/hk_9Qm2Xv7Lc4Tz8Rb1Nw6Ks3Jd5Yf0Hg2P/apprise
  • A hook request is a normal send: it goes to the recipient's browsers as a browser notification, with the same validation, delivery policy, billing and statuses as POST /v1/notifications.
  • Every hook request is live. There is no test mode for hooks, so each send creates real attempts and counts toward usage like a live API key.
NoteThe Discord, Slack and Apprise shapes are only how a tool talks to Push. Push does not post anything to Discord or Slack: every hook request becomes a notification in the recipient's browsers.

Creating, rotating and revoking#

  • Create a hook on the recipient's page in the dashboard and give it a name, such as the tool that will call it.
  • A recipient can have up to 10 active hooks. Revoked hooks do not count.
  • The full URL is shown once, when the hook is created. Push stores only a SHA-256 hash of the token and keeps its first 12 characters so you can tell hooks apart.
  • Rotate gives the hook a new token and shows the new URL once. The old URL stops working at once; the hook keeps its name and history. Rotating a revoked hook brings it back with the new token.
  • Revoke stops the hook on the next request. Callers then get 401 UNAUTHORIZED, or the shape's own error.
  • Each hook shows when it was last used.

The four shapes#

PathBodyUse it for
/v1/hooks/{token}Plain text, with ntfy-style headerscurl, shell scripts, cron jobs, CI steps
/v1/hooks/{token}/discordA Discord webhook messageAny tool with a Discord webhook option
/v1/hooks/{token}/slackA Slack incoming-webhook messageAny tool with a Slack webhook URL option
/v1/hooks/{token}/appriseApprise's JSON notificationApprise and tools built on it

Each shape is turned into a title, a body, an optional link and a priority by the rules below, then sent like any other notification.

Plain text#

curl https://api.meslzy.com/push/v1/hooks/hk_9Qm2Xv7Lc4Tz8Rb1Nw6Ks3Jd5Yf0Hg2P \  -H "Title: Backup finished" \  -H "Priority: high" \  -H "Tags: backup,db-1" \  -H "Click: https://example.com/backups/812" \  -d "db-1 was copied to cold storage in 4 minutes."
  • The request body is read as plain text, whatever its Content-Type.
  • Title (also X-Title or t) sets the title, and the whole body becomes the notification body.
  • Without a title header, the first non-empty line of the body is the title and the lines after it are the body.
  • Tags (also X-Tags or ta) is a comma-separated list. Up to 10 tags are kept and passed to the notification as data.tags; they count toward the 1,024-byte limit of data.
  • Click (also X-Click) becomes the notification's url. It must be an https URL, or the request fails with 400 VALIDATION_ERROR.
  • Priority (also X-Priority or p) takes ntfy's values, in any letter case:
Priority headerPush priority
1, min, 2, lowlow
3, defaulthigh (the default)
4, highhigh
5, max, urgenturgent

Any other value is ignored and the notification goes out with the default priority, high.

Discord#

⫻

Discord

curl https://api.meslzy.com/push/v1/hooks/hk_9Qm2Xv7Lc4Tz8Rb1Nw6Ks3Jd5Yf0Hg2P/discord \  -H "Content-Type: application/json" \  -d '{    "username": "Status bot",    "embeds": [{      "title": "api is down",      "description": "3 checks failed in a row",      "url": "https://status.example.com/api",      "fields": [{ "name": "Region", "value": "eu-west" }]    }]  }'
  • The body is Discord's webhook JSON. multipart/form-data is accepted too, with the JSON in a payload_json field or plain content and username fields. Attached files are ignored.
  • Only the first embed is read.
  • Title: the embed's title, else its author.name, else username, else the first line of content.
  • Body: content (without its first line when that line became the title), then the embed's description, one name: value line per field, and the footer text.
  • Link: the embed's url. Large image: the embed's image.url. Icon: avatar_url. Each is used only when it is an https URL and is dropped silently otherwise.
  • Discord markdown is turned into plain text: bold, italic, strikethrough, spoilers, inline code and code fences are unwrapped, [label](url) becomes its label, quotes lose their > and mentions are removed. <t:…> timestamps become YYYY-MM-DD HH:mm UTC.
  • A message with no content and no embed fails with Discord's error 50006, Cannot send an empty message.

Slack#

⫻

Slack

curl https://api.meslzy.com/push/v1/hooks/hk_9Qm2Xv7Lc4Tz8Rb1Nw6Ks3Jd5Yf0Hg2P/slack \  -H "Content-Type: application/json" \  -d '{    "text": "*Disk usage above 90%*\ndb-1 is at 93%. <https://example.com/servers/db-1|Open db-1>"  }'
  • The body is Slack's incoming-webhook JSON. A form post with the JSON in a payload field is accepted too.
  • Title: the text of the first header block, else the first attachment's title, else the first line of text.
  • Body: text (without its first line when that line became the title), then the text and fields of every section block, then the first attachment's pretext, text and one title: value line per field.
  • Link: the first attachment's title_link, else the first <https://…> link found in text, the section texts or the attachment text. Only https links are used.
  • Slack's mrkdwn is turned into plain text: <url|label> becomes its label, bare <url> becomes the URL, mentions are removed, and bold, italic, strikethrough and inline code markers are removed.
  • A message with no text, no blocks and no attachment fails with Slack's no_text.

Apprise#

⫻

Apprise

curl https://api.meslzy.com/push/v1/hooks/hk_9Qm2Xv7Lc4Tz8Rb1Nw6Ks3Jd5Yf0Hg2P/apprise \  -H "Content-Type: application/json" \  -d '{ "title": "Nightly job failed", "body": "Exit code 2", "type": "failure" }'
  • The body is the JSON that Apprise's json:// and jsons:// services send. Other fields, such as version and attachments, are ignored.
  • Title: title. Without it, the first line of body (or message) is the title and the rest is the body.
  • Body: body, or message when there is no body.
  • type sets the priority: info and success become normal, warning becomes high and failure becomes urgent. Without type the priority is high. Any other type value fails with 400 VALIDATION_ERROR.

Rules for every shape#

  • An empty title becomes Notification.
  • The title is cut to 120 characters and the body to 480, each ending with … when it was cut. The cut text then goes through the normal field rules and size limits.
  • Without a priority from the shape, the notification is sent with priority high, as an API send would be.
  • Hooks cannot schedule or set a TTL: every hook notification is sent at once with the default TTL of 86,400 seconds. Use the API for scheduling.
  • The request body may be up to 1 MiB, so a tool's large payload is read whole; only the extracted notification has to fit the normal limits.
  • An Idempotency-Key header (1–255 characters) works exactly as on the API: the same key and the same extracted notification replay the first result, and a different notification gets 409 IDEMPOTENCY_CONFLICT. See Idempotency.
  • A recipient with no connected browsers gets 422 INVALID_RECIPIENT, and a send the credits cannot cover gets 402 QUOTA_EXCEEDED, in the shape's own error format. See Hook endpoints.

Rate limits#

  • Each hook takes up to 60 requests per minute.
  • Hook requests also count toward the project's 300 and the account's 600 requests per minute, shared with API keys. See Rate limits.
  • Requests with an unknown or revoked token count toward the per-IP limit of 120 invalid requests per minute.
  • Over a limit, the answer is 429 with a Retry-After header in seconds.

Keep the URL secret#

WarningA hook URL is a credential. Unlike a live API key, it is accepted from any origin, browsers included, because the tools that call hooks run everywhere. Anyone who has the URL can notify the recipient and spend your credits, so keep it in a secret store and rotate it if it leaks.
NoteA hook is tied to one recipient on purpose: a leaked URL can only reach that recipient's browsers, and revoking it does not affect anything else in the project.