Moving your apps from Prowl to Nowlark
There are two ways, and the fast one takes a minute.
The fast one: change the address#
Nowlark answers Prowl's own API. Point the sender at https://nowlark.com and paste a
Nowlark key where it asks for a Prowl key, and it works: same application, event,
description, priority and url, same XML response, same −2…2 priority scale. See
Prowl's own API.
This is the route for any Prowl sender whose server address you can change, and it is perfectly fine as a permanent answer. Nothing about it is second class. A notification arriving this way goes through the same validation, rate limit, dedupe, muting, storage and fan-out as a native one, because it is literally the same function.
Not every app with Prowl built in. Many of them post to a
hardcoded api.prowlapp.com (checked in their source, 2026-09-18), so there is no address to
change. If yours has an ntfy notifier, use that instead: server https://nowlark.com, any topic,
and your Nowlark key as the access token. See ntfy's publish API.
The thorough one: rewrite what you own#
Worth doing for the senders that are yours, because the native API has things Prowl never had: dedupe, grouping, per-source colour and silencing, free key checks, structured outcomes instead of a bare 200. That is the rest of this document, and it is small: the field names change, the response becomes JSON, and one shared client replaces the hand-written senders scattered across your machines.
The field mapping#
| Prowl | Nowlark | Changed? |
|---|---|---|
apikey (form field) |
X-Nowlark-Key header |
Moved out of the body |
application |
source |
Renamed. Keep each script's own name here, not one shared name: each becomes its own row in Sources (why) |
event |
title |
Renamed |
description |
body |
Renamed |
url |
url |
Same. Now http/https only |
priority |
priority |
Same scale, −2…2. Numbers in your scripts still mean what they meant |
providerkey |
(none) | Gone. There is no provider concept |
| (none) | group_key |
New: stack related alerts |
| (none) | dedupe_key |
New: a retrying cron job cannot double-notify |
| form-encoded POST | JSON POST | |
| XML response | JSON response | |
| 1000/hour per key | 1000/hour per key | Same |
verify spends a call |
/v1/key/check does not |
Better |
Field limits are unchanged (256 / 1024 / 10000 characters), so any payload that fitted Prowl fits Nowlark.
Step 1: get a key#
In the Nowlark app: Settings › API Key › Create Your Key. Name it after the thing that will use it. The key is shown once; copy it then.
One key is free. With the Upgrade Pass you can give each script its own: the rate limit is per key, so one runaway loop cannot silence the rest, and replacing a key that leaked does not disturb anything else.
Step 2: put it where your scripts already look#
If your scripts read PROWL_API_KEY from their environment (a crontab's environment block, a
launchd plist, a .env file), put NOWLARK_API_KEY beside it:
# crontab -e, beside the existing PROWL_API_KEY line
NOWLARK_API_KEY=nlk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
A launchd agent does not inherit cron's environment, so a script that runs both ways needs the
key in both places. The Python client below reads it back out of crontab -l when the
environment does not have it, so a crontab entry alone is enough for it.
Keep PROWL_API_KEY there until the move is finished and checked.
Step 3: one client instead of many senders#
If you have several scripts that each grew their own "send a notification" function, they have almost certainly drifted: different timeouts, different error handling, one that sends the URL and one that forgot. Replace them with one shared client.
nowlark.py is standard library only, with no requests, so it drops into
a script that deliberately uses urllib without adding a dependency. For anything that is not
Python there is nowlark, a POSIX shell script.
curl -O https://nowlark.com/clients/nowlark.py
One pattern worth copying: where a wrapper's name is load bearing (a function called prowl()
that a test counts calls to), keep the name and change the inside. The call sites and the
test never move, and the rename can happen later or never.
Step 4: check before you cut over#
NOWLARK_API_KEY=nlk_… python3 nowlark.py --check
Prints the key's label and remaining calls, and spends nothing. Then send one for real:
python3 nowlark.py "Migration test" "Nowlark is live" \
"If you can read this, cut the rest over." --priority 1
delivered in the output is the number of your devices Apple accepted it for. If it is 0 with
devices: 0, no phone has registered yet: open the app once.
Things that behave differently, on purpose#
- A duplicate is a success.
dedupe_keyreturns200with"duplicate": true. Do not treat it as a failure and retry. - A muted source is a success.
"muted": true, stored in history, not pushed. The user muted it; the sender does not get a vote. delivered: 0is not an error. It means no device accepted it, usually because none is registered yet. The notification is still in history and appears when the app is opened.- Non-http URLs are refused. Prowl accepted anything. A
urlis something a phone will open on a tap, so a remote sender does not get to choose the scheme. - The client stays quiet by default.
notify()returnsNonerather than raising when the server is unreachable, because a scanner should not die over a notification. Passraise_on_error=Truewhere you want the opposite.
What you gain by doing this#
- Priority 1 and 2 break through a Focus. Prowl's priority only changed the banner's look, so a Do Not Disturb window meant the alert was simply missed. For a watcher whose whole value is being seen within seconds, this is the biggest difference.
- Priority 2 repeats until you open it, every few minutes for up to an hour by default, so an alert at 3am is not one buzz you sleep through.
- Dedupe. A rule like "only one push per event" no longer has to live in each script. A
dedupe_keyenforces it in the service. - Per-source muting without editing a script.
- Replacing a key that ended up somewhere it should not is one tap.
- No old keys lying around. A Prowl key hardcoded as a fallback in a script is a live credential to somebody else's service. Once everything sends to Nowlark, it can be deleted.