Send a notification
Users create tokens in the app (Apps → + → New token) or mint one for your site automatically at pairing. Either way, one request delivers:
curl -X POST …/api/v1/push \ -H "Content-Type: application/json" \ -d '{ "token": "bpt_…", "title": "Order shipped", "message": "Your package is on the way", "priority": 1, "color": "#D9EF00", "url": "https://example.com/orders/123", "tag": "orders" }' → {"ok":true,"id":"…","delivered":1}
| field | type | notes |
|---|---|---|
token | string | required — the push credential |
title | string | required (or message) |
message | string | body text, ≤2000 chars |
priority | int | −2…2 · 2 = urgent, bypasses quiet hours |
icon, color, sound | string | appearance overrides |
url | string | tap-through link |
tag | string | grouping / collapse key |
actions | array | up to 3 {id,title} buttons — see below |
Delivery is stored in the user's inbox as well — notifications survive dismissal.
Interactive notifications (two-way, anonymous)
Attach actions and the notification becomes a question. The user
taps a choice; you poll for the answer with the same push token — you learn
which option, never who. Priority <= -1 stays
quiet on the user's device if they enabled bundling; actionable pushes always
alert.
POST …/api/v1/push {"token":"bpt_…","title":"Acme","message":"Table for 2 at 7pm — coming?", "actions":[{"id":"confirm","title":"Confirm"}, {"id":"cancel","title":"Cancel"}]} → {"ok":true,"id":"<notificationId>","delivered":1} # poll for the user's choice (every ~2s) POST …/api/v1/action/poll {"token":"bpt_…","id":"<notificationId>"} → {"response":"confirm"} // null until they pick
Pushover-compatible endpoint
Tools with a built-in Pushover integration (Radarr, Uptime Kuma, Grafana,
Healthchecks, OctoPrint, …) work by overriding the API URL — if the tool lets
you set one. The user pastes their bpt_… token into the
user key field (or the app-token field; either is accepted).
POST …/api/1/messages.json # form-encoded or JSON; fields: user|token, message (required), # title, priority, sound, url, url_title, device → {"status":1,"request":"…"}
POST /1/users/validate.json is implemented for clients that
verify the recipient first.
api.pushover.net with no URL
field? Use its generic webhook option with the endpoint below
instead.Lenient webhook
For sources that send their own fixed payload — or where you can only paste a URL. The token lives in the path; Lauther makes sense of whatever arrives (recognizes common title/message field names, has an Alertmanager special case, falls back to a compact dump so no event is lost).
# any JSON / form / text body: POST …/api/v1/hook/bpt_YOURTOKEN # or a bare URL from anything that can open one: GET …/api/v1/hook/bpt_YOURTOKEN?title=Hi&message=It+works # optional on either: &priority=-2..2
Register your service
Pairing and name-based verification require a registered service — this closes the gap where anyone could previously call the API claiming to be any site. One key, kept server-side forever.
POST …/api/v1/services/register {"name":"Acme"} → {"serviceId":"…","apiKey":"lsk_…"} // apiKey shown once — store it
Send apiKey as Authorization: Bearer lsk_… on
every /v1/pair/* and name-based /v1/identity/verify
call below. Push (/v1/push, Pushover-compat, webhooks) is
unaffected — those already authenticate via the user's own bpt_…
token.
Sign in with Lauther
OAuth-grade sign-in with two endpoints and a QR code. Each user is pairwise: your app receives an ID and alias generated only for you — two services can never correlate the same person.
# 1 · create a pairing request (Authorization: Bearer lsk_…) POST …/api/v1/pair/start {"scopes":["identify","notify"], "url":"https://acme.example", // enables tap-to-open SSO "dataScopes":["email","address"]} // optional consent scopes → {code, pollToken, deepLink, expiresInMs} # 2 · show the code/QR (deepLink for mobile: # intent://pair/CODE#Intent;scheme=lauther;package=com.beaconapp.beacon;end # on Android, or the deepLink as-is on iOS). # The user approves in the app and picks a name for YOUR service. # 3 · poll until resolved (every ~2s, 5 min TTL) POST …/api/v1/pair/poll {code, pollToken} → {"status":"approved","beaconId":"bcn_…","nickname":"alias", "pushToken":"bpt_…","sharedData":{"address":"…"}}
Store the values against your user record. pushToken is your
credential for notifications and re-verification; beaconId is
the stable pairwise identifier.
{"code":"alias_taken"} means someone else registered with you
under that name already; the pairing request stays pending, so just ask for
a different one and resolve again with the same code. Two
different services can freely have their own user both known as "nova".dataScopes may request email, name,
phone, address. Each appears as an optional,
unchecked box on the user's approval sheet; sharedData
contains only what they ticked — possibly nothing. Sign-in always succeeds
regardless. Design for the empty case; treat every granted field as a gift.
Lauther relays values once and does not store them.Identity verification (passwordless 2FA)
Challenge the user you paired with — their phone shows “Approve sign-in to Acme as @alias?” with Approve/Deny. Two-minute TTL.
POST …/api/v1/identity/verify {"token":"bpt_…"} → {requestId, pollToken} POST …/api/v1/identity/poll {requestId, pollToken} → {"status":"approved","beaconId":"bcn_…","nickname":"alias"}
Haven't paired with this person yet but know the name they use with you? Challenge by name instead (Authorization: Bearer lsk_… required) — scoped to your own service's aliases, never a global directory:
POST …/api/v1/identity/verify {"nickname":"nova"} → {requestId, pollToken} // 404 if nobody registered that name with you
Tap-to-open SSO
If you passed url at pairing, users can open your site from the
Lauther app pre-authenticated. You'll receive a one-time code as
?beacon_session=bsc_… — redeem it server-side within two
minutes:
POST …/api/v1/session/claim {"code":"bsc_…"} → {"beaconId":"bcn_…","nickname":"alias"} // single-use
Look up the beaconId in your user table and start your own
session. Codes are 128-bit, single-use, app-bound.
Broadcast topics
Fan a message out to many subscribers with zero knowledge of who they are. Subscribers join client-side (via a code), so you can reach them but never enumerate them — ideal for a public status page, a feed, or an alert channel.
# create a topic — keep the publishKey secret, share the code POST …/api/v1/topics/create {"name":"Acme Status"} → {"code":"K7Q2-9F3D","publishKey":"ltk_…","topicId":"t_…"} # users subscribe in the Lauther app with the code (nothing stored server-side) # broadcast to every subscriber POST …/api/v1/topics/publish {"publishKey":"ltk_…","title":"Acme","message":"All systems operational"} → {"ok":true}
priority (−2…2) is honoured.Apprise
Using Apprise or a tool that
embeds it? A Lauther plugin provides the
lauther://-style URL syntax:
apprise --plugin-path beacon_standalone.py \
-t "Backup done" -b "Nightly job finished" \
beacon://bpt_YOURTOKEN
Grab the plugin from the app's integrations folder (upstream PR pending).
Errors
| status | meaning |
|---|---|
400 | missing/invalid fields |
401 | missing credential |
403 | invalid or revoked token |
404 | unknown code / nickname / request |
409 | alias already taken for this service — retry with a different one |
410 | expired or already used |
All errors return {"error":"human-readable reason"}
(Pushover-compat endpoints use Pushover's
{"status":0,"errors":[…]} shape).