The API
One address, a token in the header, and the notification is on your phone. No SDK: if your language can make an HTTP request, it can use this.
Get started
Create a token in My tokens and send it in the Authorization header. That's it — the notification lands in your own inbox.
curl https://api.achtung-app.com/v1/notify \
-H "Authorization: Bearer achtung_token" \
-d "The nightly backup failed"That shortcut uses the first line as the title. When you want more control, send JSON.
Send a notification
POST /v1/notify — accepts JSON, a form, or plain text. Responds 202 with the notification's identifier.
curl https://api.achtung-app.com/v1/notify \
-H "Authorization: Bearer achtung_token" \
-H "Content-Type: application/json" \
-d '{
"title": "Tests failing on main",
"content": "## 3 of 44\n\n- checkout\n- payments\n- shipping",
"content_type": "markdown",
"url": "https://ci.example.com/build/8412",
"priority": "high"
}'| Field | What it does |
|---|---|
| title | Required. What gets read on the lock screen. |
| content | The body. Up to 100,000 characters. |
| content_type | text, markdown or html. Defaults to text. |
| url | What opens when the notification is tapped. |
| priority | normal or high. Anything marked high is delivered immediately and cuts through the phone's focus modes. |
| attachments | Up to 10: images, documents, audio or video. |
Attaching things
Attachments are sent by URL: the notification stores the link and the app downloads it when opened.
{
"title": "Festival programme",
"content_type": "html",
"content": "<h2>Festival 2026</h2><p>12 to 15 September.</p>",
"attachments": [
{ "kind": "image", "url": "https://ayto.es/feria/cartel.jpg" },
{ "kind": "document", "url": "https://ayto.es/feria/programa.pdf",
"filename": "programa.pdf" }
]
}HTML is sanitised before it's stored: links, images, tables and formatting stay; scripts and event handlers go.
Checking a token
GET /v1/me tells you whether the token is valid and where it writes. Handy when setting up a new service.
curl https://api.achtung-app.com/v1/me \
-H "Authorization: Bearer achtung_token"Radarr and Sonarr
Radarr and Sonarr send their webhook however they like and don't let you change it, so Achtung is the one that learns to read it. Create a token in My tokens with the “Radarr and Sonarr” format: that token understands whatever comes from either of them and drops it in your inbox. It's a single format because both send the same thing; which one it came from is told by whether it talks about a movie or a series.
In Radarr or Sonarr: Settings → Connect → +, choose Webhook and paste this into URL, with method POST. Tick the events you want to receive: nothing is filtered on our side.
https://api.achtung-app.com/v1/notify?token=achtung_tokenThe token goes in the address because the webhook doesn't let you set headers. If your version does, better in Authorization: Bearer with a clean address.
Press Test and a test notification arrives. From there, every ticked event comes with its own title — what was downloaded, in which quality, from which indexer — and, if the service's general settings have its address set, with a link to the item's page. Health warnings at error level and the ones asking for manual intervention arrive with high priority.
App Store Connect
App Store Connect sends a webhook as a version moves along — waiting for review, in review, rejected, published — when a build finishes processing or fails, and for what happens in TestFlight. Create a token in My tokens with the “App Store Connect” format and name it after the app: Apple's webhook doesn't say which app it's about, it only sends identifiers, so the token's name is what shows up in the notification title. One token per app.
In App Store Connect: Users and Access → Integrations → Webhooks. Paste this into Payload URL, pick the app and tick the events you want to receive: nothing is filtered on our side. The Secret String field is required, but Achtung doesn't use it: the token in the address is what authenticates.
https://api.achtung-app.com/v1/notify?token=achtung_tokenPress Test on the webhook's page and a test notification arrives. Rejections, an invalid binary and a build that fails processing arrive with high priority.
V2C chargers
V2C Cloud sends a webhook when one of your chargers starts a charge and when it finishes, with the energy delivered. Create a token in My tokens with the “V2C” format and name it after the charger: V2C's webhook doesn't say what the charger is called, it only sends its identifier, so the token's name is what shows up in the notification title. The webhook belongs to the account, not to the charger: if you have several, each one's identifier is in the notification body.
In v2c.cloud, under API in the user menu, paste this as the webhook address. V2C doesn't sign its webhooks or let you set headers: the token in the address is what authenticates.
https://api.achtung-app.com/v1/notify?token=achtung_tokenThere is no test button: the first notification arrives with the first charge. The start one says when it began and with which card; the end one, how much energy was delivered and how long it took, with a chart of the kWh in each hour.
Vercel spend
Vercel Spend Management sends a webhook when your team's spend reaches 50%, 75% and 100% of the budget, and when a new billing cycle starts. Create a token in My tokens with the “Vercel” format.
In Vercel, under Settings → Billing → Spend Management, paste this as the webhook address. Vercel signs the body, but Achtung doesn't check it: the token in the address is what authenticates.
https://api.achtung-app.com/v1/notify?token=achtung_tokenThere is no test button: the first notification arrives with the first threshold. The 100% one arrives with high priority. If Pause Production Deployments is on, Vercel is already pausing your projects by then.
When something goes wrong
| 400 | invalid_request | The title is missing, or some field doesn't fit. |
| 401 | invalid_token | The token doesn't exist, was revoked, or expired. |
| 410 | channel_gone | The token's channel no longer exists. |
| 429 | rate_limited | More than 600 notifications in an hour with the same token. |
Private channels
A private channel stays out of the directory and can't be followed by guessing its address. You get in with an access link, created from Channels.
The link works for several people on purpose: the normal case is pasting it in the team chat so whoever needs to be there subscribes, not handing it out one by one. If it ends up somewhere it shouldn't, you change it and the old one stops working immediately — anyone already subscribed stays in, because their subscription doesn't depend on the link.
For an ops team's alerts, the usual setup is a private channel and one token per service publishing into it.
If a token leaks
Rotate it from My tokens. Rotating creates the replacement and revokes the old one, in that order, so you copy the new one, put it in its service, and you're done. Your other services' tokens don't notice: which is why each should have its own.