Skip to content
Nowlark
API reference
Browse the API reference

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. It is not second class: a notification arriving this way goes through the same code as a native one.

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.

Prowl's own API#

For scripts and senders whose server address you can change. Nowlark answers Prowl's own endpoints, so a sender like that needs only a new address and a Nowlark key.

Point it at https://nowlark.com and paste a Nowlark key where it asks for a Prowl key. That is the whole change.

GETPOST /publicapi/add#

Form-encoded or query string, exactly as Prowl accepted both.

Prowl field Becomes Notes
apikey
The credential
Your nlk_… key. Up to five, comma separated, as Prowl allowed
application
source
256
event
title
1024
description
body
10000
priority
priority
Same scale, −2…2. The numbers in your scripts still mean what they meant
url
url
512. A web page or an app's link; links that could run code, such as javascript: and file:, are refused (see Links)
providerkey
(none)
Accepted and ignored
curl -sS https://nowlark.com/publicapi/add \
  -d apikey=nlk_… \
  -d application=Backup \
  -d event=Finished \
  -d description="412 GB in 38 minutes." \
  -d priority=1

The response is Prowl's XML, because that is what the clients parse:

<?xml version="1.0" encoding="UTF-8"?>
<prowl>
<success code="200" remaining="999" resetdate="1789665600" />
</prowl>

GETPOST /publicapi/verify#

Checks a key. Same XML, and it does not spend one of your hourly calls.

Status codes#

Prowl's, not HTTP's obvious ones. A client written against Prowl branches on exactly these:

Code Meaning
200
Accepted
400
Bad request: no application, no event or description, a non-http url, priority out of range
401
Unknown or missing key. One bad key in a comma-separated list refuses the whole call and sends nothing
406
The key's 1000/hour is spent. Not 429: Prowl used 406 and clients back off on it
500
Something unexpected

What is deliberately different#

  • A non-http url is refused. Prowl passed anything through.
  • verify is free. Prowl's spent a call, which is why its own docs told you not to call it before every send.
  • A muted source returns success. It was accepted and recorded; the sender does not get a vote on whether you want to hear from it, and an error would make a script retry something that never failed.
  • providerkey does nothing. It existed so Prowl could revoke one third-party application across every user at once, which needs a registry and a company to run it.

The two APIs are one implementation#

A notification sent this way is indistinguishable from a native one by the time it reaches a phone: same validation, same rate limit, same dedupe, same muting, same storage, same fan-out. Prowl calls reach the same code /v1/notify uses. Nothing about what a valid notification is, is decided twice.

The compatibility endpoints are not in the OpenAPI document. That is deliberate: the schema describes what you should write against, and this is what you should be migrating away from.

The thorough one: rewrite what you own#

Worth doing for the senders that are yours: the native API has dedupe, grouping, per-source silencing, free key checks and structured outcomes. The field names change, the response becomes JSON, and one shared client replaces your hand-written senders.

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. A web page or an app's link; links that could run code, such as javascript: and file:, are refused
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
Spends no call

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 › Connect an App › 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

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.