No account. Encrypted. Sends from anything that can send an HTTP request.
Get a push notification to your device
from anything that can make an HTTP request
Install the app, grab your key, and send an HTTP request to
notifi.it/send. The notification shows up on your iPhone or Mac,
encrypted with your public key, so neither we nor Apple can read it.
$ curl https://notifi.it/send -d key=nk_yourkeyhere -d title="hello world" {"ok":true}
The app
One inbox,
on every Apple device you own.
API
One endpoint.
GET or POST https://notifi.it/send
| Parameter | Type | Notes |
|---|---|---|
| key required | string | The key from the app. It picks which device gets the push. Send it as a
header, Authorization: Bearer nk_yourkey, which keeps it out of
logs; or pass it as this parameter. |
| title required | string | The notification title. Up to 200 characters. |
| message | string | The notification body. Markdown, up to 16,000 characters. The push shows a short preview; the app renders the full text. |
| link | URL | URL opened when the notification is tapped. Up to 2,048 characters. |
| image | URL | Image displayed with the notification. Must be https.
PNG, JPEG or GIF, 5 MB max, URL up to 2,048 characters. |
| occurred_at | integer | When the event actually happened, as unix milliseconds, useful when a send is queued or retried. Only changes the timestamp shown in the app; defaults to when we receive the request. |
| is_critical | boolean | Ask for a critical alert: it breaks through Focus and stays on the lock screen,
but does not sound through silent mode. The key must also have Critical
alerts switched on in the app; a send that asks without that arrives as an
ordinary notification rather than failing, and the reply carries a
warning saying so. |
Send
The same one request,
from wherever you run.
Copy one and put your key in it. Anything that can make an HTTP request can send.
$ export NOTIFI_KEY=
curl -X POST https://notifi.it/send \ -H "Authorization: Bearer $NOTIFI_KEY" \ -d "title=Backup complete" \ -d "message=4.2 GB in 3m 11s" \ -d "link=https://console.internal/backups"
await fetch("https://notifi.it/send", { method: "POST", headers: { "Authorization": `Bearer ${process.env.NOTIFI_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ title: "Backup complete", message: "4.2 GB in 3m 11s", }), });
import os, requests requests.post( "https://notifi.it/send", headers={"Authorization": f"Bearer {os.environ['NOTIFI_KEY']}"}, json={ "title": "Backup complete", "message": "4.2 GB in 3m 11s", }, )
package main import ( "net/http" "net/url" "os" "strings" ) func main() { form := url.Values{ "title": {"Backup complete"}, "message": {"4.2 GB in 3m 11s"}, } req, _ := http.NewRequest("POST", "https://notifi.it/send", strings.NewReader(form.Encode())) req.Header.Set("Authorization", "Bearer "+os.Getenv("NOTIFI_KEY")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") http.DefaultClient.Do(req) }
import Foundation var request = URLRequest(url: URL(string: "https://notifi.it/send")!) request.httpMethod = "POST" request.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode([ "title": "Backup complete", "message": "4.2 GB in 3m 11s", ]) _ = try await URLSession.shared.data(for: request)
require "net/http" uri = URI("https://notifi.it/send") req = Net::HTTP::Post.new(uri) req["Authorization"] = "Bearer #{ENV['NOTIFI_KEY']}" req.set_form_data( "title" => "Backup complete", "message" => "4.2 GB in 3m 11s" ) Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
<?php $ch = curl_init("https://notifi.it/send"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("NOTIFI_KEY")], CURLOPT_POSTFIELDS => [ "title" => "Backup complete", "message" => "4.2 GB in 3m 11s", ], ]); curl_exec($ch);
// Fires when Claude stops. { "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "curl -s https://notifi.it/send \ -H \"Authorization: Bearer $NOTIFI_KEY\" \ -d \"title=Claude finished\" \ -d \"message=$CLAUDE_PROJECT_DIR\"" }] }] } }
# Put NOTIFI_KEY in the repository's secrets. - name: Tell me it broke if: failure() run: | curl -s https://notifi.it/send \ -H "Authorization: Bearer $NOTIFI_KEY" \ -d "title=$GITHUB_WORKFLOW failed" \ -d "message=$GITHUB_REF_NAME at $(git log -1 --format=%s)" \ -d "link=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" env: NOTIFI_KEY: ${{ secrets.NOTIFI_KEY }}
# Notify for any command that took longer than a minute, and say # whether it worked. autoload -Uz add-zsh-hook _notifi_start() { _NOTIFI_T=$SECONDS; _NOTIFI_CMD=$1 } _notifi_end() { local code=$? secs=$(( SECONDS - ${_NOTIFI_T:-SECONDS} )) (( secs < 60 )) && return curl -s https://notifi.it/send \ -H "Authorization: Bearer $NOTIFI_KEY" \ -d "title=$([[ $code == 0 ]] && echo ok || echo failed) after ${secs}s" \ -d "message=$_NOTIFI_CMD" >/dev/null } add-zsh-hook preexec _notifi_start add-zsh-hook precmd _notifi_end
# Drop this in /etc/systemd/system/, then add one line to any unit: # OnFailure=notifi-failed@%n.service # Every unit on the box can share it. %i is the unit that failed. [Unit] Description=Push a notification when %i fails [Service] Type=oneshot EnvironmentFile=/etc/notifi.env ExecStart=/usr/bin/curl -s https://notifi.it/send \ -H "Authorization: Bearer $NOTIFI_KEY" \ -d "title=%i failed on %H" \ --data-urlencode "message=$(systemctl status %i --lines=10 --no-pager)"
# Put the key in a Secret, then curl at the end of any Job's command. apiVersion: batch/v1 kind: CronJob metadata: name: nightly-backup spec: schedule: "0 3 * * *" jobTemplate: spec: template: spec: restartPolicy: Never containers: - name: backup image: alpine/curl command: ["/bin/sh", "-c"] args: - | ./backup.sh && curl -s https://notifi.it/send \ -H "Authorization: Bearer $NOTIFI_KEY" \ -d "title=Backup complete" env: - name: NOTIFI_KEY valueFrom: { secretKeyRef: { name: notifi, key: key } }
// reqwest = { version = "0.12", features = ["json"] } let key = std::env::var("NOTIFI_KEY")?; reqwest::Client::new() .post("https://notifi.it/send") .bearer_auth(key) .form(&[ ("title", "Backup complete"), ("message", "4.2 GB in 3m 11s"), ]) .send() .await?;
Uses
Anything that finishes,
fails, or changes.
Agents
- A Claude Code run finished
- An agent stopped and needs a decision
- A watched price dropped below your number
Builds and deploys
- A deploy failed, with the failing step
- A release went out, with the tag
- A flaky test failed again on main
Servers and jobs
- A disk crossed 90%
- A health check has been down five minutes
- A training run finished, with the final loss
Home and hardware
- A leak sensor under the sink went wet
- The UPS switched to battery
- The car finished charging overnight
How it works
Your phone holds the only key.
No account
No signup, no email, no password. The app generates a keypair on first launch and that keypair is your identity.
Neither we nor Apple can read your notifications
Nothing kept after delivery
The server deletes a notification as soon as your device confirms it has collected it. Your history lives on your device, not in our database.
Critical alerts
Send with is_critical=1 on a key you have marked critical, and the
notification breaks through Focus and lands on the lock screen.
One key per script, revocable
CI gets a key, the backup job gets a key, the scraper gets a key. If one leaks, revoke it and the rest keep working.
Send from anywhere
Plain HTTP and no SDK, so anything that can make an HTTP request can send: a Linux box, a CI runner, a router, a home hub, another phone.
Open source
The app, the API and the crypto are on GitHub.
Download
Get notifi on iPhone and Mac.
On the Mac it lives in the menu bar. Each key delivers to one device, so a script pages your phone, your Mac, or both if you give it two keys.
Reviews
What people write on the App Store.
Unfortunately, we didn't realize we had any users when we took down the servers.