cURL
Everything the CLI and SDKs do goes over a plain HTTP API, so curl alone is enough for scripts and quick experiments. Sends use simple request headers; if you prefer JSON bodies, every endpoint has a /json twin that takes the same fields as a JSON object.
Authentication
Every request needs a credential. There are no anonymous sends.
- Personal: your API token (app settings, under API Token) in an
API-Tokenheader. - Organization: the org API key in an
Api-Keyheader.
Send a notification
curl -i -X POST https://api.simplepu.sh/v1/notifications \
-H "API-Token: YOUR_API_TOKEN" \
-H "Topic: builds" \
-H "Title: Build succeeded" \
-H "Content: main @ abc123 deployed in 47s"The response carries the id in the X-Notification-Id header: a grpntf_ group id by default (each recipient gets their own instance), or a single ntf_ id with Shared: true.
Response headers
The ids and tokens come back as response headers, and curl does not print those by default. Add -i to see them above the body. To capture a single header in a script, use curl -s -o /dev/null -w '%header{x-notification-id}' ... (curl 7.83 or newer).
Omit the Topic header entirely to send to your own devices: the notification goes to every device on your own account.
Send a task
curl -i -X POST https://api.simplepu.sh/v1/tasks \
-H "API-Token: YOUR_API_TOKEN" \
-H "Topic: deploys" \
-H "Title: Approve deploy?" \
-H "Choice-Input: Production deploy of v2.1.0;Approve,Reject"Add one input header to ask for an answer:
| Header | Value | Notes |
|---|---|---|
Text-Input | description | Free-form text reply. |
Choice-Input | description;opt1,opt2 | One-of-many. Description part optional. |
Action-Input | description;key=Label:style,... | Tap buttons. Description and :style optional. |
Slider-Input | description;min=0;max=14;step=0.1;unit=pH;default=7 | Only min= and max= required. |
Photo-Input | description | Photo from the camera. Description optional. |
Voice-Recording-Input | description | Voice recording. Description optional. |
File-Input | description | Arbitrary file upload. Description optional. |
Location-Input | description | GPS location. Description optional. |
Optional modifiers:
| Header | Effect |
|---|---|
Tag | Label for receiver-side filtering. |
Critical: true | iOS critical alert (bypasses silent switch and Do Not Disturb). |
Auto-Commit: false | Require explicit completion instead of committing once all inputs are filled. |
Reply | Attach a reply composer. One of one-shot, sticky, one-time-per-user (see below). |
Reply modes:
one-shot— the first reply wins and closes the composer for everyone.sticky— any device may reply any number of times.one-time-per-user— each user may reply once per round; others can still reply until they've each had a turn.
The response carries X-Task-Id (grptsk_ group by default, tsk_ with Shared: true) and X-Append-Token for appending subtasks later.
Attach a file
Send the raw bytes as the request body, with Attachment naming the file. URLs in Attachment become link attachments instead:
curl -X POST https://api.simplepu.sh/v1/tasks \
-H "API-Token: YOUR_API_TOKEN" \
-H "Topic: deploys" \
-H "Title: Signed contract" \
-H "Attachment: contract.pdf" \
--data-binary @./contract.pdfWait for the answer inline
Add Wait: true to a send with exactly one input and the connection stays open until the first recipient answers; the answer is the response body. The server emits a heartbeat newline every 30 seconds so proxies don't drop the connection, so strip newlines before use:
RESULT=$(curl -s -X POST https://api.simplepu.sh/v1/tasks \
-H "API-Token: YOUR_API_TOKEN" \
-H "Topic: deploys" \
-H "Choice-Input: Approve deploy?;Approve,Reject" \
-H "Wait: true" | tr -d '\n')
echo "User chose: $RESULT"For file, photo, and voice answers the body is a presigned download URL. Capture it, then curl the URL to fetch the file. The URL is self-authenticating (no API-Token header) and valid for about 5 minutes:
URL=$(curl -s -X POST https://api.simplepu.sh/v1/tasks \
-H "API-Token: YOUR_API_TOKEN" \
-H "File-Input: Upload the file" \
-H "Wait: true" | tr -d '\n')
curl -s "$URL" -o upload.binLong waits
An open HTTP connection can still be dropped by NATs and load balancers on very long waits, and on a drop the answer is lost to that request. For anything beyond quick approvals, use sp collect or an SDK: they collect over a resumable WebSocket.
Append a subtask
Use the X-Append-Token from the original send:
curl -i -X POST https://api.simplepu.sh/v1/subtasks \
-H "API-Token: YOUR_API_TOKEN" \
-H "Append-Token: $TOKEN" \
-H "Content: One more approval needed" \
-H "Choice-Input: Confirm the rollback;Yes,No"The response carries X-Subtask-Id. For a default send it is the grptsk_ group id, since the subtask is appended to every recipient's own copy of the task. For a Shared: true send it is the sub_ id of the single appended subtask.
Organization sends
Address a member by name or every member at once. Both require the org API key:
curl -X POST https://api.simplepu.sh/v1/notifications \
-H "Api-Key: YOUR_ORG_API_KEY" \
-H "Member: Alice" \
-H "Title: Heads up" \
-H "Content: Gate 4 is blocked"
curl -X POST https://api.simplepu.sh/v1/notifications \
-H "Api-Key: YOUR_ORG_API_KEY" \
-H "Broadcast: true" \
-H "Title: All hands" \
-H "Content: Meeting at 3pm, link in your inbox"Errors
Errors come back as { "error": "<code>", "msg": "..." }. The ones worth handling in scripts:
| Code | Status | Meaning |
|---|---|---|
authorization_error | 401 | Missing or wrong credential. |
not_topic_holder | 403 | Personal send to a shared topic you don't hold. |
write_protection_token_required | 401 | The topic is write-protected; send Topic-Auth-Token. |
request_limit_exceeded | 429 | Daily request allowance used up. |
upload_quota_exceeded | 413 | File too large or storage pool full. |