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_key returns 200 with "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: 0 is 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 url is 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() returns None rather than raising when the server is unreachable, because a scanner should not die over a notification. Pass raise_on_error=True where 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_key enforces 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.