terminal Partner API

SEND A PUSH IN
ONE POST.

Deliver rich notifications to any device or group running Notify!. One token, one request, no SDK and no accounts. A simple REST API built by developers, for developers.

cloud Base URL https://push.getnotifyapp.com
Notify! Partner API
REST reference. Base URL https://push.getnotifyapp.com
Machine-readable: the full surface, all 39 operations, is published as an OpenAPI 3.1 specification. Import it into Postman, Insomnia, or a code generator. It is discoverable automatically through this site's RFC 9727 API catalog, and the icon uploader has its own spec.

Public endpoints for device and group messaging. Documented here: GET /link, GET/POST /notify/{deviceId}, GET/POST /notify-group/{groupId}, POST /notify-json/{id}, the GET/POST /ping/{beaconId}/{token} beacon heartbeat, the new /live-activity Lock Screen tiles, and MDM enterprise deployment.

Web devices: a browser registered through the Notify! web app is an ordinary device to this API. Its Device ID is longer (WB + 14 characters instead of 8), but every endpoint on this page accepts it unchanged; senders never need to know or care that the receiving screen is a browser. Live Activities are the one exception: they are an iOS Lock Screen feature, and starting one on a web device returns an honest 400.

Not in this API: on-device monitoring. Feeds (RSS, Atom and JSON Feed) are read by the app on the device, and website watches run either on the device or against your own ChangeDetection.io server. The gateway sees neither, so there are no feed or watch endpoints on this page. Those alerts also stay out of GET /device/notifications: the server writes no row for them by design, and they are held only on the device. To push a feed through this API, poll it in your own script and call POST /notify-json/{id} with the new item.
POST
/notify-json/{id}
star Preferred  Unified JSON endpoint for all integrations
chevron_right

Recommended endpoint. This is the preferred method for all modern integrations. It supports both devices and groups, auto-detection, JSON payloads, custom icons, and notification threading.

Important: Requests must include the Content-Type: application/json header for the body to be parsed correctly.

Overview

Auto-detects device vs group based on ID format (GRP* = group). Supports webhook icons and threading.

Parameters

NameInTypeRequiredDescription
idpathstringrequiredDevice or group ID (auto-detected)
tokenquerystringrequiredDevice or group token
textJSON bodystringrequiredNotification message (no URL encoding needed)
titleJSON bodystringoptionalNotification title
groupTypeJSON bodystringoptionalIdentifier that controls notification threading/grouping
iconUrlJSON bodystringoptionalSender avatar icon URL (HTTPS). Small circular icon next to the title.
imageUrlJSON bodystringoptionalHero image URL (HTTPS) rendered inside the expanded notification. JPEG/PNG/GIF, ≤ 10 MB.

Device Notification

POST /notify-json/ABC12345?token=XYZ789TOKEN123
Content-Type: application/json

{
  "text": "Server CPU at 95%!"
}

Group Notification

POST /notify-json/GRP45678?token=GRPTOKEN456
Content-Type: application/json

{
  "text": "Database maintenance starting in 30 minutes"
}

cURL Examples

Device notification:

curl -X POST "https://push.getnotifyapp.com/notify-json/ABC12345?token=XYZ789TOKEN123" \
  -H "Content-Type: application/json" \
  -d '{"text": "Server CPU at 95%!"}'

Group notification:

curl -X POST "https://push.getnotifyapp.com/notify-json/GRP45678?token=GRPTOKEN456" \
  -H "Content-Type: application/json" \
  -d '{"text": "Database maintenance starting in 30 minutes"}'

Webhook Notifications with Custom Icons

Thread grouping: The groupType parameter controls notification threading. Notifications with the same groupType are grouped together in their own thread, while different values create separate threads.

Basic webhook (default thread):

curl -X POST "https://push.getnotifyapp.com/notify-json/DEVICE_ID?token=TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text": "Server restarted"}'

GitHub notifications (grouped in one thread):

curl -X POST "https://push.getnotifyapp.com/notify-json/DEVICE_ID?token=TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Deploy succeeded",
    "title": "GitHub Actions",
    "groupType": "github-ci",
    "iconUrl": "https://github.com/favicon.ico"
  }'

Jenkins notifications (separate thread from GitHub):

curl -X POST "https://push.getnotifyapp.com/notify-json/DEVICE_ID?token=TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Build #142 passed",
    "title": "Jenkins",
    "groupType": "jenkins-ci",
    "iconUrl": "https://jenkins.io/favicon.ico"
  }'

With a hero image (icon + inline image shown when expanded):

curl -X POST "https://push.getnotifyapp.com/notify-json/DEVICE_ID?token=TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "New photo uploaded to shared album",
    "title": "Photos",
    "iconUrl": "https://example.com/photos-favicon.png",
    "imageUrl": "https://example.com/preview.jpg"
  }'

Malformed JSON handling:

curl -X POST "https://push.getnotifyapp.com/notify-json/DEVICE_ID?token=TOKEN" \
  -H "Content-Type: application/json" \
  -d '{bad json here}'

# Returns: 400 Bad Request
# {
#   "error": "Bad Request",
#   "message": "Invalid JSON in request body",
#   "details": "..."
# }

Custom Icons

When iconUrl is provided, Notify! attempts to load the custom icon. If unavailable, it falls back to a generic icon to ensure notifications always display properly.

Device Response

{
  "success": true,
  "type": "device",
  "deviceId": "ABC12345",
  "message": "Notification sent successfully"
}

Group Response

{
  "success": true,
  "type": "group",
  "groupId": "GRP45678",
  "groupName": "DevOps Team",
  "message": "Group notification sent",
  "deviceCount": 3,
  "successCount": 3,
  "failureCount": 0,
  "results": [
    {"deviceId": "ABC12345", "success": true},
    {"deviceId": "DEF67890", "success": true},
    {"deviceId": "GHI23456", "success": true}
  ]
}

Error Response

{
  "error": "Bad Request",
  "message": "Missing required field: text",
  "required": ["text"],
  "optional": ["title", "iconUrl", "groupType"]
}

Errors: 400 missing text field or invalid JSON, 403 invalid token, 404 ID not found, 415 missing or incorrect Content-Type header

Key Points

  • Single endpoint: /notify-json/{id}
  • Required: Must include Content-Type: application/json header
  • Auto-detects device vs group based on ID (GRP* = group)
  • Returns "type" field so you know what was processed
  • No URL encoding needed for the message text
  • Supports webhook icons via iconUrl parameter (case-sensitive): small sender avatar
  • Supports hero images via imageUrl parameter (case-sensitive): JPEG/PNG/GIF rendered inline when the notification is expanded, ≤ 10 MB
  • iconUrl and imageUrl are independent, use either or both
  • Threading: Use groupType to group notifications - same type = same thread, different type = separate threads (case-sensitive)
  • Robust icon fallback ensures webhook notifications always display properly

Try it live

Build and send a real notification, upload icons, and grab code for your integration.

Open Notification Builder arrow_forward
GET
POST
/notify/{deviceId}
Send a notification to a single device
chevron_right

Parameters

Note: Both GET and POST methods are supported with identical parameters.

New: Now supports title, iconUrl, and groupType for enhanced notifications.

NameInTypeRequiredDescription
deviceIdpathstringrequiredTarget device ID
tokenquerystringrequiredDevice token
bodyquerystringrequiredNotification message. URL-encode if sent in query/form.
titlequerystringoptionalCustom notification title (URL-encode)
iconUrlquerystringoptionalSender avatar icon URL (HTTPS, URL-encode)
imageUrlquerystringoptionalHero image URL (HTTPS, URL-encode) rendered inline on expansion. JPEG/PNG/GIF, ≤ 10 MB.
groupTypequerystringoptionalThread identifier for grouping notifications

Request Examples

Basic notification (GET):

GET /notify/ABC12345?token=XYZ789TOKEN123&body=Hello%20World

Enhanced notification with all features (GET):

GET /notify/ABC12345?token=TOKEN&body=Server%20CPU%20at%2095%25&title=Alert&groupType=monitoring&iconUrl=https%3A%2F%2Fexample.com%2Ficon.png

cURL Examples

Basic notification:

curl "https://push.getnotifyapp.com/notify/ABC12345?token=XYZ789TOKEN123&body=Hello%20World"

Enhanced with custom title and icon:

curl "https://push.getnotifyapp.com/notify/ABC12345?token=TOKEN&body=Server%20down&title=Critical%20Alert&iconUrl=https://example.com/alert.png&groupType=server-alerts"

Using POST with threading:

curl -X POST "https://push.getnotifyapp.com/notify/ABC12345?token=TOKEN&body=Build%20passed&title=CI/CD&groupType=github-actions"

Responses

{
  "success": true,
  "deviceId": "ABC12345",
  "message": "Notification sent successfully",
  "apnsId": "12345678-1234-1234-1234-123456789012"
}

Errors: 403 invalid device token, 404 device not found

GET
POST
/notify-group/{groupId}
Send a notification to all devices in the group
chevron_right

Parameters

Note: Both GET and POST methods are supported with identical parameters.

New: Now supports title, iconUrl, and groupType for enhanced notifications.

NameInTypeRequiredDescription
groupIdpathstringrequiredTarget group ID
tokenquerystringrequiredGroup token
bodyquerystringrequiredNotification message. URL-encode if sent in query/form.
titlequerystringoptionalCustom notification title (URL-encode)
iconUrlquerystringoptionalSender avatar icon URL (HTTPS, URL-encode)
imageUrlquerystringoptionalHero image URL (HTTPS, URL-encode) rendered inline on expansion. JPEG/PNG/GIF, ≤ 10 MB.
groupTypequerystringoptionalThread identifier for grouping notifications

Request Examples

Basic group notification (GET):

GET /notify-group/GRP56789?token=GROUP_TOKEN&body=Hello%20team!

Enhanced notification with all features (GET):

GET /notify-group/GRP56789?token=TOKEN&body=Deploy%20complete&title=DevOps&groupType=deployments&iconUrl=https%3A%2F%2Fexample.com%2Fcheck.png

cURL Examples

Basic group notification:

curl "https://push.getnotifyapp.com/notify-group/GRP56789?token=GROUP_TOKEN&body=Hello%20team!"

Enhanced with custom title and icon:

curl "https://push.getnotifyapp.com/notify-group/GRP56789?token=TOKEN&body=Deployment%20successful&title=Production&iconUrl=https://example.com/success.png&groupType=prod-deploys"

Using POST with threading:

curl -X POST "https://push.getnotifyapp.com/notify-group/GRP56789?token=TOKEN&body=All%20tests%20passed&title=CI/CD&groupType=test-results"

Responses

{
  "success": true,
  "groupId": "GRP56789",
  "message": "Notification sent to 3 devices"
}

Errors: 403 invalid group token, 404 group not found

GET
/ping/{beaconId}/{token}
star New  Beacons: dead man's switch heartbeat
chevron_right

Overview

A Beacon is reverse monitoring: instead of Notify! telling you when something happens, it tells you when something stops happening. Create a beacon in the app (Devices tab > Beacons), pick how often you expect a ping plus a grace period, and paste its ping URL into the cron job, backup script, or device you want watched. If no ping arrives within the period plus grace, every targeted device gets a Down push. When pings resume you get an Up recovery push, and while it stays down you get periodic reminders.

Beacons are created and managed inside the app; only the ping URL below is called from your systems. A beacon can alert a single device or a whole Device Group (Macs running Notify Listener included).

Parameters

NameInTypeRequiredDescription
beaconIdpathstringrequiredBeacon ID, format CHK + 5 characters (shown in the app)
tokenpathstringrequired15-character ping token (the URL from the app already includes it)

Methods: GET, POST, and HEAD all behave identically. The request body and Content-Type are ignored, so it works from anything that can hit a URL. Treat the ping URL as a secret: whoever has it can mark your job "alive".

cURL

curl -fsS --retry 3 "https://push.getnotifyapp.com/ping/CHK7Q2ZK/aB3dE5fG7hJ9kL2" > /dev/null

Crontab heartbeat

# Ping every 30 minutes; alert if two in a row are missed
*/30 * * * * curl -fsS --retry 3 "https://push.getnotifyapp.com/ping/CHK7Q2ZK/aB3dE5fG7hJ9kL2" > /dev/null

Put the curl at the end of a job so a crashed job never pings, or run it on its own schedule as a machine heartbeat.

Responses

200 OK

Errors: 404 unknown beacon or wrong token (identical responses, nothing to probe). There is no failure-signal endpoint: down detection is purely timed, so to test an alert just stop pinging.

Lifecycle

Waiting (created, first ping arms the schedule) → UpDown (no ping for period + grace, checked every minute) → Up on the next ping. Pausing in the app silences alerts; pings still count so resuming re-arms cleanly.

POST
GET
DELETE
/live-activity/{id}
star New  Live Activities: a Lock Screen tile your script drives
chevron_right

Overview

A Live Activity is a single Lock Screen tile that updates in place: it appears when a job starts, changes while it runs (progress bar, live countdown, status), and disappears when it ends. One tile instead of a stack of notifications. The device address is an upsert: your first call starts the tile (it appears even when the Notify! app is closed, via push-to-start, as long as the app has been opened once on the device), every later call to the same address updates it, and &end=1 finishes it. One static URL is the whole lifecycle; the returned activityId exists for precision when you run several tiles at once.

Countdowns cost one request. Send endsIn (seconds from now, never a timestamp: no time zones, no clock skew) and iOS ticks the countdown locally with no further requests. A progress bar costs one request per change. The in-app Builder (Settings > Notification Builder > Live Activity) composes all of this with a live preview and copyable code.

Parameters (start and update share the same fields)

NameInTypeRequiredDescription
idpathstringrequiredYour device ID (starts the tile, then updates it: an upsert), or a specific activityId (LA + 6) when running several tiles
tokenquerystringrequiredYour device token (same credential as /notify)
titlebodystringstart onlyTile title, max 120 chars ("Laundry")
bodybodystringoptionalSecond line, max 300 chars
symbolbodystringoptionalSF Symbol name for the icon ("washer.fill")
tintbodystringoptionalAccent color, #RRGGBB or #AARRGGBB
progressbodynumberoptionalProgress bar, 0 to 100
endsInbodyintegeroptionalCountdown: seconds from now, 1 to 86400
trailingbodystringoptionalStatic trailing text when there is no timer ("queued", "#3"), max 40 chars
statusbodystringoptionalFree-form phase word ("running", "done"), max 40 chars
keepForbodyintegeroptionalDELETE only: seconds the finished tile lingers, 0 to 14400 (4 h). Default 0 = leaves immediately

Updates are partial. Send only what changed: {"progress": 94} is a complete update. An explicit null clears a field ({"endsIn": null} removes the countdown). Content-Type: application/json is required for JSON bodies.

Or skip JSON entirely: one static URL is the whole API. The device address is an upsert: the first call with query parameters starts the tile, every later call updates it, and &end=1 finishes it: /live-activity/{deviceId}?token=...&title=Laundry&endsIn=2700&progress=25. Since the device id and token never change, that URL can live in a service's configuration forever. Running several tiles at once is explicit: pass &new=1 to start extras and address each by its returned activityId; a device call while several are live returns 409 listing them. Add &format=text to a start for a plain-text response containing only the id.

Ending a tile DELETE

Always end your tile. Without an explicit end, iOS leaves a finished tile on the Lock Screen for up to four hours, so a script that simply stops calling strands a frozen "91%" in front of the user. This is the most commonly missed step in a Live Activity integration.

NameInTypeRequiredDescription
idpathstringrequiredAn activityId to end that exact tile, or a device ID to end the device's single live tile
tokenquerystringrequiredYour device token
keepForbodyintegeroptionalSeconds to deliberately leave the finished tile visible. Omit it and the tile clears at once
Any content field from the table above (progress, status, body, and the rest) may be sent too, and becomes the final state the tile shows as it closes.

Idempotent, and forgiving in the device-ID form. Ending an already-finished tile succeeds and reports how it actually finished, so an end-of-job hook never fails for having already worked. Addressed by device ID, no live tile is likewise a success, while several live tiles return the teaching 409 listing them so you can end one by its activityId. Bad credentials always answer a uniform 403, which never reveals whether the id exists.

The one-URL equivalent: &end=1 on the same path does exactly this, so a service that can only be handed a single static URL still gets a clean finish.

# Either form ends it. The body is the last thing the tile shows.
curl -X DELETE "https://push.getnotifyapp.com/live-activity/LA7Q2ZKM?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" -d '{"progress":100,"status":"done"}'

curl "https://push.getnotifyapp.com/live-activity/ABC12345?token=YOUR_TOKEN&end=1&status=done"

Reading status GET

A bare GET, one carrying no content parameters, reads instead of acting:

Called withReturns
an activityIdFull status of that one tile, including endReason for a tile that has already finished. Readable for one day after it ends, then the row is cleaned up
a device IDEvery tile currently live on that device. This is the crash-recovery path: a script that lost its activityId lists its device and reattaches
# Did it finish, and how?
curl "https://push.getnotifyapp.com/live-activity/LA7Q2ZKM?token=YOUR_TOKEN"

# Lost the id? List what is live on the device and reattach
curl "https://push.getnotifyapp.com/live-activity/ABC12345?token=YOUR_TOKEN"

A GET carrying content parameters ACTS instead of reading, with exactly the semantics of POST: ?title=...&endsIn=2700 starts, ?progress=94 updates, &end=1 ends. That is the house /notify idiom, so anything that can only fetch a URL still drives the whole lifecycle.

cURL: start, update, end

# Start: returns { "activityId": "LA7Q2ZKM" } - keep it
curl -X POST "https://push.getnotifyapp.com/live-activity/ABC12345?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Laundry","symbol":"washer.fill","tint":"#7C3AED","progress":0,"endsIn":2700}'
# Update: the bar moves in place
curl -X POST "https://push.getnotifyapp.com/live-activity/LA7Q2ZKM?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"progress":94}'
# End: without this a dead tile can linger for hours
curl -X DELETE "https://push.getnotifyapp.com/live-activity/LA7Q2ZKM?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"progress":100,"status":"done"}'

Responses

Every success is 200 with JSON (a start called with &format=text answers with the bare id instead). The start response, with the id to keep:

{
  "success": true,
  "activityId": "LA7Q2ZKM",
  "expiresAt": "2026-08-10T23:24:00.000Z"
}
# Update
{ "success": true, "activityId": "LA7Q2ZKM" }

# End. Idempotent: ending an already-finished tile still succeeds,
# with "state" reporting how it actually finished ("ended"/"dismissed")
{ "success": true, "activityId": "LA7Q2ZKM", "state": "ended" }

# End on the device URL with no live tile: still success, never an error
{ "success": true, "message": "No live activity to end" }

# GET one tile (works for a day after it finishes; endReason says why:
# "script", "dismissed", "never-started", "overdue", "abandoned")
{ "activityId": "LA7Q2ZKM", "state": "active", "endReason": null,
  "content": { "title": "Laundry", "progress": 80, "endsAt": 1786456988 },
  "endsAt": "2026-08-11T13:56:28.000Z", "startedAt": "...",
  "updatedAt": "...", "endedAt": null, "expiresAt": "..." }

# GET on the device id: your live tiles (empty array when none)
{ "activities": [ { "activityId": "LA7Q2ZKM", "state": "active", ... } ] }

Errors: every error is JSON with error and a human-readable message written to be shown. 403 invalid token or unknown id (identical responses, nothing to probe), 409 the device cannot show tiles yet and the message says why; the device-URL ambiguity 409 (several tiles live, no new=1) also carries an activityIds array so a script can pick one, 410 the tile was dismissed or ended (the message names the reason; start a new one), 400 validation with the failing field named (a malformed JSON body gets the same treatment), 429 Apple is silently ignoring starts for this device: the message says how long to wait, machine-readable in retryAfterSeconds and a Retry-After header (openingTheAppMayHelp says whether opening the Notify app can shortcut it; when false, only the wait will), 502 the start failed, and deliveryState says how: "not-delivered" means no tile exists and starting again cannot duplicate one (retry only if retryAfterSeconds is present, and wait that long; without it the same request will fail the same way), while "unknown" means Apple never answered, a tile may be appearing, and the returned activityId should be polled rather than retried with new=1, 503 Live Activities temporarily disabled server-side (ending and status always keep working).

Good to know

A tile lives at most 8 hours (Apple's limit; the start response echoes it as expiresAt). Longer job? Start a fresh tile when the old one ends. If the user swipes the tile away, that is final: updates to its id return 410, and the device address answers the same 410 while the dismissed job would still have been running, so a looping script cannot respawn a swiped tile by accident (&new=1 starts a fresh tile deliberately, and after the job's window the device URL starts fresh on its own). Because the device address upserts, a retried call can never duplicate a tile. GET /live-activity/{activityId}?token=... reports status and, for finished tiles, why they ended; a bare GET /live-activity/{deviceId}?token=... lists your live tiles. The app records one History entry when a tile appears; the update stream stays out of history by design, so progress noise never buries real notifications. One more Apple quirk the server absorbs for you: updating or reinstalling the app rotates the device's start credential, and starts sent to the old one are accepted by Apple but never appear. The server notices (a start with no tile after 15 minutes reports never-started), backs off with an honest 429 instead of burning the device's spawn budget, and re-sends a pending start automatically the next time the app opens and reports a changed credential. If the credential comes back unchanged nothing is re-sent, deliberately: a start push stays deliverable for the same 15 minutes the row is re-drivable in, so re-sending on a hunch could put a second tile on the Lock Screen that no id addresses.

MDM
Managed App Configuration
Enterprise: auto-join a Device Group fleet-wide
chevron_right

Overview

Deploying Notify! to a fleet? Managed App Configuration auto-joins every managed device into one Device Group, so a single /notify-group webhook pages the whole fleet with zero per-device setup. On launch the app reads two keys from its managed configuration, validates them, and silently joins the group. The group shows an enterprise badge on the device and its Leave button is disabled while managed.

Requirements (read this first)

Most "it does not work" reports are one of these three.

1. The app must be installed as a MANAGED app by your MDM. iOS only delivers managed app configuration to apps the MDM itself installed (App Store / VPP deployment). If the user installed the app manually, or you are testing a TestFlight build, the configuration never reaches the app. There is no error anywhere; it is simply absent.

2. Use your MDM's App Configuration feature, NOT a configuration profile. Installing a .mobileconfig profile (manually or via MDM) does not populate managed app configuration and cannot work. If you used an older Notify! example .mobileconfig file, that approach was wrong and has been retired; use the app-config keys below instead.

3. The key names are case-sensitive: groupID (capital I, capital D) and groupToken. Values must be exact; the app trims stray spaces and newlines but rejects anything else malformed.

Configuration keys

KeyTypeRequiredFormatWhere to find it
groupIDstringrequiredGRP + 5 uppercase alphanumerics (8 total)App > Devices > Device Groups > your group
groupTokenstringrequired15 alphanumerics (mixed case)Same group detail screen

App configuration XML

<dict>
  <key>groupID</key>
  <string>GRPAB12C</string>
  <key>groupToken</key>
  <string>Ab3Df6Gh9Jk2Mn5</string>
</dict>

Per-MDM setup

Jamf Pro: Devices > the managed Notify! app > App Configuration > paste the dict (or add the two keys).

Microsoft Intune: Apps > App configuration policies > Add > Managed devices > select Notify! > add groupID and groupToken in the configuration designer (or paste the XML).

Workspace ONE: Apps & Books > edit Notify! > Assignment > Application Configuration > add the two keys.

App bundle ID: com.pingie.Notify.Notify-for-Change-Detection

Verify and troubleshoot

Success looks like: on the next app launch the group appears in Devices > Device Groups with the enterprise badge, without the user doing anything. The join retries automatically on every launch until it succeeds, so a temporary network failure heals itself.

To see exactly what happened, connect the device to a Mac and open Console.app, then filter on subsystem com.pingie.Notify category MDM:

No managed app configuration present: the configuration never reached the app. This is a deployment problem (requirement 1 or 2 above), not a values problem.

Managed app configuration is malformed: the config arrived but a value failed validation; the log says which key. Re-copy the ID and token from the app.

attempting enterprise auto-join: config is good; any remaining failure is network/server side and will retry next launch.

Tips. For modern integrations, use POST /notify-json/{id} with a JSON body. For query-based calls, URL-encode the body parameter. Both GET and POST methods work for legacy notification endpoints. A request body may be up to 16 KB of text (send long messages in the POST body, not the query string, which is limited to a few kilobytes of URL); the notification itself shows a shortened version (Apple caps a push at 4 KB), and the full text is kept and readable in the app's History, which is where tapping the notification takes you. Use GET /link first to verify credentials.

bolt Quick start

1. Download Notify! from the App Store

2. Get your device ID and token from the app

3. Send your first notification using the examples above

4. Check out our automation integrations for no-code solutions