<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Paperplane</title><description>Paperplane sends notifications from your servers, scripts, pipelines and AI agents straight to your phone. Create a channel, POST to its URL — that is the whole integration. Notifications can carry buttons, so you can re-run a job or approve a deploy from the message itself, and an agent can wait for your answer before it continues.</description><link>https://paprpla.in/</link><language>en-us</language><item><title>Your agent can&apos;t approve its own deploy</title><link>https://paprpla.in/agents/cant-approve-its-own-deploy/</link><guid isPermaLink="true">https://paprpla.in/agents/cant-approve-its-own-deploy/</guid><description>The token an agent holds can send an ask, but it can never answer one — only a signed-in human, tapping in the app, can resolve it.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A blocking approval only means something if the thing being approved can’t approve itself. So the rule on Paperplane’s backend is simple: a personal access token can send an &lt;code&gt;ask&lt;/code&gt;, poll it, and read its result — but it can never be the thing that answers it.&lt;/p&gt;
&lt;h2 id=&quot;why-a-token-isnt-enough&quot;&gt;Why a token isn’t enough&lt;/h2&gt;
&lt;p&gt;The token an agent holds to send an ask is, cryptographically, the same kind of credential that could be used to answer one. If sending and answering used the same check, an agent — or anything holding its token — could ask itself a question and approve it, and you’d never know the “approval” wasn’t a person.&lt;/p&gt;
&lt;p&gt;So answering is deliberately &lt;strong&gt;session-only&lt;/strong&gt;: it requires a signed-in human, tapping a button in the app, on a device that authenticated with a password or an OAuth sign-in. A &lt;code&gt;pplt_…&lt;/code&gt; token, however many scopes it carries, is rejected on that route.&lt;/p&gt;
&lt;h2 id=&quot;what-this-actually-stops&quot;&gt;What this actually stops&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;An agent can’t grant itself permission by racing its own question.&lt;/li&gt;
&lt;li&gt;A leaked or overly-broad token can page you, but can’t act on your behalf when you’re the thing being asked.&lt;/li&gt;
&lt;li&gt;The audit trail (&lt;code&gt;answeredBy&lt;/code&gt; on every resolved ask) always names a person, never a service.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;what-it-doesnt-stop&quot;&gt;What it doesn’t stop&lt;/h2&gt;
&lt;p&gt;It’s not a defense against a compromised phone, or against you tapping Approve on something you didn’t read. The guarantee is narrower and more useful than that: whatever asked the question isn’t the same thing that gets to answer it.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;/agents/make-your-agent-wait/&quot;&gt;make your agent wait&lt;/a&gt; for the mechanics, and &lt;a href=&quot;/agents/scoped-agent-tokens/&quot;&gt;scoped agent tokens&lt;/a&gt; for what a token can and can’t do more broadly.&lt;/p&gt;
</content:encoded><category>Agents</category></item><item><title>Make your agent wait for you</title><link>https://paprpla.in/agents/make-your-agent-wait/</link><guid isPermaLink="true">https://paprpla.in/agents/make-your-agent-wait/</guid><description>paperplane_ask sends a question with buttons to your phone and blocks your agent until you tap one, or it times out.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;An agent that can act without asking is fast and occasionally wrong in ways you find out about later. &lt;code&gt;paperplane_ask&lt;/code&gt; is the other option: the agent sends a question, your phone shows buttons, and the tool call doesn’t return until you tap one — or the timeout fires.&lt;/p&gt;
&lt;h2 id=&quot;the-call&quot;&gt;The call&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Deploy staging to production?&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;12 files changed, all checks green.&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;options&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;approve&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;label&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Approve&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;tone&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;positive&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;deny&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;label&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Deny&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;tone&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;destructive&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;timeoutSec&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;300&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Omit &lt;code&gt;options&lt;/code&gt; and you get a default Approve/Deny pair. &lt;code&gt;timeoutSec&lt;/code&gt; runs 5 seconds to 24 hours; without &lt;code&gt;defaultOptionId&lt;/code&gt;, an expired ask resolves as &lt;code&gt;timeout&lt;/code&gt; with no option chosen — treat that as a denial, not a green light.&lt;/p&gt;
&lt;h2 id=&quot;what-happens-while-it-waits&quot;&gt;What happens while it waits&lt;/h2&gt;
&lt;p&gt;The agent polls in the background — starting at 2 seconds, backing off ×1.5, capped at 15 seconds — so a fast answer costs almost nothing and a slow one doesn’t hammer the server. You see a normal notification with buttons; tapping one ends the poll on the next check.&lt;/p&gt;
&lt;h2 id=&quot;what-comes-back&quot;&gt;What comes back&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;status&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;answered&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;optionId&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;approve&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;optionLabel&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Approve&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;answeredBy&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&amp;lt;username&amp;gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;elapsedSec&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;41&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;answeredBy&lt;/code&gt; is who tapped it, which matters the moment more than one person can answer a channel’s asks.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;/agents/scoped-agent-tokens/&quot;&gt;scoped agent tokens&lt;/a&gt; for why the agent can’t answer its own question, and &lt;a href=&quot;/agents/tool-call-timeout-recovery/&quot;&gt;tool-call timeout recovery&lt;/a&gt; for what to do if your MCP client gives up before the person does.&lt;/p&gt;
</content:encoded><category>Agents</category></item><item><title>Get a push notification when your coding agent finishes</title><link>https://paprpla.in/agents/notify-when-agent-finishes/</link><guid isPermaLink="true">https://paprpla.in/agents/notify-when-agent-finishes/</guid><description>paperplane_send_event pages your phone the moment a long-running job ends, so you stop checking a terminal that isn&apos;t done yet.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Not every notification needs an answer. When you just want to know a job finished — a long test run, a build, a migration — &lt;code&gt;paperplane_send_event&lt;/code&gt; is the plain version of paging you: no buttons, no wait, just a push.&lt;/p&gt;
&lt;h2 id=&quot;the-call&quot;&gt;The call&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Tests finished&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;482 passed, 0 failed, 3 skipped — 14m runtime.&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;nature&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;critical&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set &lt;code&gt;nature&lt;/code&gt; to &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;failure&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;critical&lt;/code&gt; or &lt;code&gt;pending&lt;/code&gt; — or &lt;code&gt;priority&lt;/code&gt; to &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;crit&lt;/code&gt;, &lt;code&gt;critical&lt;/code&gt; or &lt;code&gt;urgent&lt;/code&gt; — and it’s promoted from a plain notification to an alert you can acknowledge or resolve, badged until you do. Anything else is a message you read and move past.&lt;/p&gt;
&lt;h2 id=&quot;setup&quot;&gt;Setup&lt;/h2&gt;
&lt;p&gt;Add &lt;code&gt;paperplane-mcp&lt;/code&gt; to your MCP client config once:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;mcpServers&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;paperplane&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;command&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;npx&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;args&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;-y&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;paperplane-mcp&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;env&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;PAPERPLANE_TOKEN&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;pplt_...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From there, any agent that speaks MCP — Claude Code, Claude Desktop, Cursor — can page you without you asking it to check in.&lt;/p&gt;
&lt;h2 id=&quot;when-to-reach-for-paperplane_ask-instead&quot;&gt;When to reach for &lt;code&gt;paperplane_ask&lt;/code&gt; instead&lt;/h2&gt;
&lt;p&gt;If the next step actually depends on your answer — approve, pick a path, confirm something destructive — use &lt;a href=&quot;/agents/make-your-agent-wait/&quot;&gt;&lt;code&gt;paperplane_ask&lt;/code&gt;&lt;/a&gt; instead. It’s the same channel, but it blocks the agent until you’ve actually decided, rather than notifying you and moving on regardless.&lt;/p&gt;
</content:encoded><category>Agents</category></item><item><title>A revocable, scoped token for your agent</title><link>https://paprpla.in/agents/scoped-agent-tokens/</link><guid isPermaLink="true">https://paprpla.in/agents/scoped-agent-tokens/</guid><description>A pplt_ personal access token is not your sign-in — six scopes, SHA-256 storage, and a fixed list of what it can never do.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Handing an agent your Paperplane sign-in would mean handing it everything: your account, every channel, every other token. A personal access token is narrower on purpose.&lt;/p&gt;
&lt;h2 id=&quot;the-token&quot;&gt;The token&lt;/h2&gt;
&lt;p&gt;Minted in the app under &lt;strong&gt;Settings → Developer tokens&lt;/strong&gt;, shown once as &lt;code&gt;pplt_&lt;/code&gt; followed by 64 hex characters. Paperplane stores only its SHA-256 hash — a lost token can’t be recovered, only replaced.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;mcpServers&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    &quot;paperplane&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;command&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;npx&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;args&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;-y&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;paperplane-mcp&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;      &quot;env&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        &quot;PAPERPLANE_TOKEN&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;pplt_...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        &quot;PAPERPLANE_DEFAULT_CHANNEL&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Builds&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;six-scopes&quot;&gt;Six scopes&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;events:write&lt;/code&gt;, &lt;code&gt;channels:read&lt;/code&gt;, &lt;code&gt;messages:read&lt;/code&gt;, &lt;code&gt;messages:write&lt;/code&gt;, &lt;code&gt;tickets:read&lt;/code&gt;, &lt;code&gt;tickets:write&lt;/code&gt;. A token minted in the app carries all six; nothing stops you from asking for a narrower one for a script that should only ever send events.&lt;/p&gt;
&lt;h2 id=&quot;what-no-token-can-do&quot;&gt;What no token can do&lt;/h2&gt;
&lt;p&gt;Regardless of scope, a personal access token can never create or delete a channel, rotate an endpoint key, manage membership, run message retention, or mint or revoke another token. Those stay session-only — a signed-in human, in the app. The same rule is what makes &lt;a href=&quot;/agents/cant-approve-its-own-deploy/&quot;&gt;an agent unable to approve its own deploy&lt;/a&gt;: answering an &lt;code&gt;ask&lt;/code&gt; is session-only too.&lt;/p&gt;
&lt;h2 id=&quot;housekeeping&quot;&gt;Housekeeping&lt;/h2&gt;
&lt;p&gt;Up to 20 active tokens per account, an optional expiry from 1 to 3,650 days, and &lt;code&gt;lastUsedAt&lt;/code&gt; visible in the app so a stale token is easy to spot. Revoking one is immediate — the next request with it gets a 401, and the MCP connector fails fast on that with a clear stderr message rather than a confusing mid-conversation error.&lt;/p&gt;
</content:encoded><category>Agents</category></item><item><title>When your agent&apos;s tool call times out but you haven&apos;t answered</title><link>https://paprpla.in/agents/tool-call-timeout-recovery/</link><guid isPermaLink="true">https://paprpla.in/agents/tool-call-timeout-recovery/</guid><description>paperplane_check_ask recovers an answer after your MCP client gives up on paperplane_ask — the ask itself keeps resolving on the server regardless.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;code&gt;paperplane_ask&lt;/code&gt; can legitimately take minutes — that’s the point, it’s waiting on you. But most MCP clients impose their own tool-call timeout, shorter than the &lt;code&gt;timeoutSec&lt;/code&gt; you set. If the client’s clock runs out first, you’ll see a client-side timeout error, not the ask’s actual result.&lt;/p&gt;
&lt;h2 id=&quot;the-ask-doesnt-know-the-client-gave-up&quot;&gt;The ask doesn’t know the client gave up&lt;/h2&gt;
&lt;p&gt;Nothing about that timeout reaches Paperplane’s backend. The ask keeps waiting exactly as configured, on the server, independent of whatever the MCP client decided to do. If you tap Approve two minutes after the client stopped waiting, the ask still resolves to &lt;code&gt;answered&lt;/code&gt; — the client just never heard about it.&lt;/p&gt;
&lt;h2 id=&quot;recovering-it&quot;&gt;Recovering it&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// paperplane_check_ask&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{ &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;channel&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Builds&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;messageId&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;msg_8a2f...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// → non-blocking read, any time after&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{ &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;status&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;answered&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;optionId&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;approve&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;answeredBy&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&amp;lt;username&amp;gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;paperplane_check_ask&lt;/code&gt; is a plain read — no polling, no wait. Call it with the &lt;code&gt;messageId&lt;/code&gt; from the original &lt;code&gt;paperplane_ask&lt;/code&gt; call (visible in your own conversation, or found via &lt;code&gt;paperplane_list_messages&lt;/code&gt;) and you get whatever the ask has resolved to right now: &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;answered&lt;/code&gt;, or &lt;code&gt;timeout&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-practical-rule&quot;&gt;The practical rule&lt;/h2&gt;
&lt;p&gt;Set &lt;code&gt;timeoutSec&lt;/code&gt; shorter than you’d like to wait, not as long as you can stand — a client-side timeout is worse than a slightly early &lt;code&gt;timeout&lt;/code&gt; status, because a client timeout gives you no result to act on at all. If you don’t control the client’s own limit, keep &lt;code&gt;paperplane_check_ask&lt;/code&gt; as your fallback path rather than re-asking, which would send a second notification for the same decision.&lt;/p&gt;
&lt;p&gt;Paperplane currently sends no MCP progress notifications on a pending ask, so a client that extends its own timeout on progress updates gets no help here — plan around the hard timeout, not around progress.&lt;/p&gt;
</content:encoded><category>Agents</category></item><item><title>ntfy alternative</title><link>https://paprpla.in/compare/ntfy-alternative/</link><guid isPermaLink="true">https://paprpla.in/compare/ntfy-alternative/</guid><description>ntfy is free, open source, and self-hostable, and already has tap-to-fire action buttons. Paperplane trades that for a blocking agent ask, an alert lifecycle, and a ticket board.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ntfy gets more right than most alternatives on this list. It’s free, it’s open source, you can self-host it in one container or use the public instance, and — this is the one worth being upfront about — it already has action buttons: an &lt;code&gt;http&lt;/code&gt; action fires a webhook when you tap the notification, the same shape as a Paperplane callback.&lt;/p&gt;
&lt;p&gt;So the honest comparison isn’t “Paperplane has buttons and ntfy doesn’t.” It’s narrower: ntfy’s action doesn’t carry a cooldown or a logged trigger history the way a Paperplane callback does, and there’s nothing past that single tap — no lifecycle, no blocking question an agent can wait on, no shared board for the follow-up. If a topic needs an account-free, self-hosted pub/sub channel with a webhook button, ntfy already does that, for free.&lt;/p&gt;
&lt;p&gt;What it doesn’t do is the part built for agents: &lt;code&gt;paperplane_ask&lt;/code&gt; blocking a tool call until a human taps an option, with a token that can page you but can’t answer its own question. If that’s what you’re after, that gap is the whole reason to look at Paperplane instead.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;/agents/make-your-agent-wait/&quot;&gt;make your agent wait&lt;/a&gt; for how the blocking ask works.&lt;/p&gt;
</content:encoded><category>Compare</category></item><item><title>Pushbullet alternative</title><link>https://paprpla.in/compare/pushbullet-alternative/</link><guid isPermaLink="true">https://paprpla.in/compare/pushbullet-alternative/</guid><description>Pushbullet is a device-sync tool with a push API attached. Paperplane is built the other way — one channel per system, with buttons, a lifecycle, and a blocking ask for agents.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Pushbullet’s core product is mirroring — notifications, links, files, copy-paste, moving between your own phone and laptop. Its API for sending a custom push is a secondary feature bolted onto that, which is why people using it for machine alerts tend to hit the free tier’s push cap and then face a subscription meant for a different use case.&lt;/p&gt;
&lt;p&gt;There’s no per-source channel model: a push from your CI pipeline and a push from your monitoring rule land the same way a shared link does, with no thread, no lifecycle, and no button that fires a webhook back at your infrastructure. It’s also a single-user tool — sharing means syncing your own devices, not inviting a teammate into a channel.&lt;/p&gt;
&lt;p&gt;If what you actually want is “mirror my phone’s notifications to my laptop,” Pushbullet does that well and Paperplane doesn’t try to. If you want a channel per system with buttons that do something and an agent that can wait on your answer, that’s a different tool.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;/docs/&quot;&gt;the API contract&lt;/a&gt; for how a Paperplane channel works.&lt;/p&gt;
</content:encoded><category>Compare</category></item><item><title>Pushcut alternative</title><link>https://paprpla.in/compare/pushcut-alternative/</link><guid isPermaLink="true">https://paprpla.in/compare/pushcut-alternative/</guid><description>Pushcut triggers Shortcuts automations right on your iPhone. Paperplane is cross-platform, built around one channel per system, and adds a blocking ask for agents.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Pushcut’s strength is specific: a notification can trigger an Apple Shortcuts automation directly on your phone, no server round-trip required. If your workflow already lives in Shortcuts and you’re iOS-only, that’s a real capability Paperplane doesn’t have at all — Paperplane’s callbacks fire a webhook you host, not a Shortcut on your device.&lt;/p&gt;
&lt;p&gt;What Pushcut doesn’t have is a model for &lt;em&gt;systems&lt;/em&gt;. It’s built around notification templates you configure once, not a channel per source that reads like a thread of everything that system has said. There’s no ack/resolve lifecycle, no ticket board to defer something onto, and — since it’s a personal automation tool rather than a developer inbox — no concept of an AI agent blocking on your approval before it continues.&lt;/p&gt;
&lt;p&gt;If you want deep on-device automation and don’t need Android, Pushcut is the better fit. If you want a cross-platform inbox organized by source, with an agent that can genuinely wait for you, that’s the gap Paperplane fills.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;/docs/&quot;&gt;the API contract&lt;/a&gt; for how a channel’s endpoint works, or &lt;a href=&quot;/agents/make-your-agent-wait/&quot;&gt;make your agent wait&lt;/a&gt; for the blocking ask.&lt;/p&gt;
</content:encoded><category>Compare</category></item><item><title>Pushover alternative</title><link>https://paprpla.in/compare/pushover-alternative/</link><guid isPermaLink="true">https://paprpla.in/compare/pushover-alternative/</guid><description>Pushover is cheaper, older, and more integrated. Paperplane adds blocking agent approvals, action-button callbacks, and an alert lifecycle.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Pushover does one thing and has done it for over a decade: a token, a user key, and a POST turns into a push notification. It’s cheap, it’s everywhere, and it isn’t going to disappear.&lt;/p&gt;
&lt;p&gt;Where it stops is where Paperplane starts. Pushover has no notion of an agent that blocks on your answer — it can &lt;em&gt;repeat&lt;/em&gt; an emergency-priority alert until you acknowledge it via a separate API call, which is close to a lifecycle but isn’t buttons on the notification. It has no way to attach a labelled webhook that fires when you tap it. And it has nothing for AI agents specifically — no MCP connector, no concept of a token that can page you but can’t answer its own question.&lt;/p&gt;
&lt;p&gt;If your need is “get a reliable ping from a script,” that gap doesn’t matter and Pushover wins on price and maturity alone. If you want an agent to ask you something and actually wait for a human, or a notification with real buttons wired to your infrastructure, that’s the part Pushover doesn’t have.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&quot;/agents/make-your-agent-wait/&quot;&gt;make your agent wait&lt;/a&gt; for the blocking-ask mechanics, or the &lt;a href=&quot;/docs/&quot;&gt;API contract&lt;/a&gt; for the full payload.&lt;/p&gt;
</content:encoded><category>Compare</category></item><item><title>Get notified when a cron job or systemd service fails</title><link>https://paprpla.in/recipes/cron-systemd/</link><guid isPermaLink="true">https://paprpla.in/recipes/cron-systemd/</guid><description>Wrap a scheduled job so failure — and only failure — reaches your phone, without cron&apos;s noisy default of emailing every run.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Cron’s default is to email every run’s output to a local mail spool nobody reads. The fix is the same either way: check the exit code, and only notify on failure.&lt;/p&gt;
&lt;h2 id=&quot;cron&quot;&gt;Cron&lt;/h2&gt;
&lt;p&gt;Wrap the job in a small script rather than editing the crontab line itself — easier to test and to reuse:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;#!/usr/bin/env bash&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;set&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -euo&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pipefail&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; /usr/local/bin/nightly-backup.sh&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;then&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  curl&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -sS&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -X&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; POST&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;https://&amp;lt;host&amp;gt;/events/&amp;lt;username&amp;gt;/Backups&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    -H&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;Content-Type: application/json&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    -d&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      &quot;title&quot;: &quot;Nightly backup failed&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      &quot;message&quot;: &quot;&apos;&quot;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;hostname&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&quot;&apos; — see /var/log/backup.log&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      &quot;priority&quot;: &quot;High&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      &quot;nature&quot;: &quot;error&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    }&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  exit&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;fi&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Point cron at the wrapper, not the underlying command:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;0 3 * * * /usr/local/bin/backup-with-alert.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;systemd&quot;&gt;systemd&lt;/h2&gt;
&lt;p&gt;A oneshot service can use &lt;code&gt;OnFailure=&lt;/code&gt; to trigger a second unit only when the first one fails — no wrapper script needed:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ini&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# /etc/systemd/system/backup.service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;[Unit]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Description&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=Nightly backup&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;OnFailure&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=notify-paperplane@%n.service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;[Service]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=oneshot&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ExecStart&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=/usr/local/bin/nightly-backup.sh&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ini&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# /etc/systemd/system/notify-paperplane@.service&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;[Unit]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Description&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=Notify Paperplane that %i failed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;[Service]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Type&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=oneshot&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ExecStart&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;=/usr/bin/curl -sS -X POST &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://&amp;lt;host&amp;gt;/events/&amp;lt;username&amp;gt;/Backups&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  -H &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Content-Type: application/json&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  -d &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;{&quot;title&quot;:&quot;systemd unit failed&quot;,&quot;message&quot;:&quot;%i&quot;,&quot;priority&quot;:&quot;High&quot;,&quot;nature&quot;:&quot;error&quot;}&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;%i&lt;/code&gt; and &lt;code&gt;%n&lt;/code&gt; expand to the failed unit’s name, so one &lt;code&gt;notify-paperplane@&lt;/code&gt; template covers every service you attach it to.&lt;/p&gt;
</content:encoded><category>Recipes</category></item><item><title>Get a phone notification when a GitHub Actions run fails</title><link>https://paprpla.in/recipes/github-actions/</link><guid isPermaLink="true">https://paprpla.in/recipes/github-actions/</guid><description>Add one curl step to a workflow and CI failures show up as an alert on your phone, without a GitHub App or a third-party integration.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;GitHub’s own notification settings bury a CI failure in an inbox with everything else. A &lt;code&gt;curl&lt;/code&gt; step at the end of the job sends it straight to a channel of its own.&lt;/p&gt;
&lt;h2 id=&quot;the-step&quot;&gt;The step&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;yaml&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;- &lt;/span&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;Notify Paperplane&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;failure()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;  run&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    curl -sS -X POST &quot;${{ secrets.PAPERPLANE_ENDPOINT }}&quot; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      -H &quot;Content-Type: application/json&quot; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      -H &quot;X-Paperplane-Endpoint-Key: ${{ secrets.PAPERPLANE_KEY }}&quot; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      -d &apos;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;title&quot;: &quot;CI failed on main&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;message&quot;: &quot;&apos;&quot;${{ github.workflow }}&quot;&apos; · ${{ github.sha }}&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;priority&quot;: &quot;High&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;nature&quot;: &quot;error&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      }&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PAPERPLANE_ENDPOINT&lt;/code&gt; is the full channel URL from &lt;strong&gt;Channel → Details&lt;/strong&gt;; &lt;code&gt;PAPERPLANE_KEY&lt;/code&gt; is that channel’s endpoint key, if you set one. &lt;code&gt;priority: High&lt;/code&gt; and &lt;code&gt;nature: error&lt;/code&gt; promote this from a message you scroll past to an alert you have to acknowledge or resolve.&lt;/p&gt;
&lt;h2 id=&quot;only-on-success-too&quot;&gt;Only on success, too&lt;/h2&gt;
&lt;p&gt;Drop &lt;code&gt;if: failure()&lt;/code&gt; for &lt;code&gt;if: always()&lt;/code&gt; and branch on &lt;code&gt;${{ job.status }}&lt;/code&gt; in the message if you also want a quiet confirmation when a deploy job succeeds — useful for anything long enough that you’ve stopped watching the terminal.&lt;/p&gt;
&lt;h2 id=&quot;add-a-button&quot;&gt;Add a button&lt;/h2&gt;
&lt;p&gt;A Paperplane callback fires a plain, unauthenticated &lt;code&gt;GET&lt;/code&gt;/&lt;code&gt;POST&lt;/code&gt; — GitHub’s re-run API needs a bearer token, so you can’t point a callback straight at &lt;code&gt;api.github.com&lt;/code&gt;. Put a small relay in between (a Cloudflare Worker route or similar) that holds the GitHub token and calls the re-run API when hit:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;callback&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  { &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;label&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Re-run&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;&quot;webhook&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;https://your-relay.example.com/rerun/RUN_ID&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See the &lt;a href=&quot;/docs/&quot;&gt;API contract&lt;/a&gt; for the full callback shape and its cooldown.&lt;/p&gt;
</content:encoded><category>Recipes</category></item><item><title>Route Grafana alerts to your phone</title><link>https://paprpla.in/recipes/grafana-alerting/</link><guid isPermaLink="true">https://paprpla.in/recipes/grafana-alerting/</guid><description>Grafana&apos;s webhook contact point sends its own payload shape — a small relay reshapes it into a Paperplane event, with the alert&apos;s labels carried through as context.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Grafana’s alerting webhook payload carries a full alert group — labels, annotations, state, a &lt;code&gt;commonLabels&lt;/code&gt; block — not a flat &lt;code&gt;title&lt;/code&gt;/&lt;code&gt;message&lt;/code&gt;. Rather than fight Grafana’s templating for an exact match, point it at a small relay that reshapes the payload and forwards it.&lt;/p&gt;
&lt;h2 id=&quot;the-relay&quot;&gt;The relay&lt;/h2&gt;
&lt;p&gt;A few lines is enough — this one runs on Cloudflare Workers, but any small HTTP handler works the same way:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  async&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;request&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;env&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; alert&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; request.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;json&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; first&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; alert.alerts?.[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; firing&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; alert.status &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;firing&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`https://&amp;lt;host&amp;gt;/events/&amp;lt;username&amp;gt;/Grafana`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      method: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;POST&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      headers: { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      body: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        title: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;firing&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ?&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;Alert&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;Resolved&apos;}: ${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;alert&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;commonLabels&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;?.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;alertname&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;Grafana&apos;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        message: first?.annotations?.summary &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; alert.message &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;No summary provided&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        priority: firing &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;High&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;Low&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        nature: firing &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;error&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; undefined&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        dataJson: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(alert.commonLabels &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;??&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {}),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Response&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ok&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;wiring-it-up&quot;&gt;Wiring it up&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Alerting → Contact points → Add contact point&lt;/strong&gt;, type &lt;strong&gt;Webhook&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the URL to your relay, not directly to Paperplane.&lt;/li&gt;
&lt;li&gt;Attach the contact point to a notification policy scoped to whichever alert rules should reach your phone — not everything Grafana tracks needs to page you.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;why-a-relay-instead-of-grafanas-own-templating&quot;&gt;Why a relay instead of Grafana’s own templating&lt;/h2&gt;
&lt;p&gt;Grafana can template the webhook body directly in newer versions, which avoids the extra hop if you’re comfortable with its templating syntax. The relay is the more durable recipe here: it doesn’t depend on which Grafana version you’re running, and it’s the same pattern you’d reuse for any tool — like Stripe — whose webhook payload doesn’t already match Paperplane’s shape.&lt;/p&gt;
</content:encoded><category>Recipes</category></item><item><title>Notify your phone from any shell script</title><link>https://paprpla.in/recipes/shell-script/</link><guid isPermaLink="true">https://paprpla.in/recipes/shell-script/</guid><description>The entire integration is one curl — no SDK, no client library, works from any script that can shell out to curl.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If a script can run &lt;code&gt;curl&lt;/code&gt;, it can reach your phone. This is the whole integration — everything else in the recipes section is this same request, reshaped for a specific tool’s webhook format.&lt;/p&gt;
&lt;h2 id=&quot;the-one-liner&quot;&gt;The one-liner&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -X&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; POST&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;https://&amp;lt;host&amp;gt;/events/&amp;lt;username&amp;gt;/&amp;lt;Channel%20Name&amp;gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  -H&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;Content-Type: application/json&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  -d&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;{&quot;title&quot;:&quot;Job finished&quot;,&quot;message&quot;:&quot;&apos;&quot;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;hostname&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&quot;&apos; at &apos;&quot;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -Is&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&quot;&apos;&quot;}&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;title&lt;/code&gt; and &lt;code&gt;message&lt;/code&gt; are the only required fields. Everything else — &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;nature&lt;/code&gt;, &lt;code&gt;dataJson&lt;/code&gt;, &lt;code&gt;callback&lt;/code&gt; — is optional on the same payload; see the &lt;a href=&quot;/docs/&quot;&gt;API contract&lt;/a&gt; for the full list.&lt;/p&gt;
&lt;h2 id=&quot;a-reusable-function&quot;&gt;A reusable function&lt;/h2&gt;
&lt;p&gt;Drop this in &lt;code&gt;.bashrc&lt;/code&gt; or a shared script library and every job just calls &lt;code&gt;notify&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;notify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  local&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; title&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;$1&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; message&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;$2&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nature&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;${3&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:-&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;  curl&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -sS&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -X&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; POST&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;https://&amp;lt;host&amp;gt;/events/&amp;lt;username&amp;gt;/Scripts&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    -H&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;Content-Type: application/json&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    -d&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;$(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;printf&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;{&quot;title&quot;:%s,&quot;message&quot;:%s%s}&apos; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      &quot;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;jq&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -Rn&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --arg&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; t &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$title&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot; &apos;$t&apos;)&quot; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      &quot;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;jq&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -Rn&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --arg&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; m &quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$message&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot; &apos;$m&apos;)&quot; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;      &quot;${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;nature&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;+,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;nature&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;$nature&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}&quot;)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;notify&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;Backup done&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;$(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;hostname&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;) — $(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;date&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;notify&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;Disk almost full&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;/var at 94%&quot;&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;critical&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;jq -Rn --arg&lt;/code&gt; handles the JSON-escaping for you, so a message with quotes or newlines in it doesn’t break the payload — worth the one extra dependency the moment your messages aren’t hand-typed constants.&lt;/p&gt;
</content:encoded><category>Recipes</category></item><item><title>Get notified when a Stripe payment fails</title><link>https://paprpla.in/recipes/stripe-webhooks/</link><guid isPermaLink="true">https://paprpla.in/recipes/stripe-webhooks/</guid><description>Stripe sends its own webhook payload shape, so a small relay verifies the signature and forwards a clean title/message to your phone.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Stripe webhooks POST Stripe’s own event shape — &lt;code&gt;{ &quot;type&quot;: &quot;invoice.payment_failed&quot;, &quot;data&quot;: { &quot;object&quot;: {...} } }&lt;/code&gt; — not something you can point at a Paperplane endpoint directly. The honest recipe is a small relay: receive the Stripe event, verify its signature, forward a Paperplane-shaped payload.&lt;/p&gt;
&lt;h2 id=&quot;the-relay&quot;&gt;The relay&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;js&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Stripe &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;stripe&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; stripe&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; new&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Stripe&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(process.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; default&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; function&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; handler&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;req&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;res&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; signature&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; req.headers[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;stripe-signature&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  let&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; event;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  try&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    event &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; stripe.webhooks.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;constructEvent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(req.rawBody, signature, process.env.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;STRIPE_WEBHOOK_SECRET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  } &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;catch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; res.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;400&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;send&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Invalid signature&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (event.type &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;===&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;invoice.payment_failed&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    const&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; invoice&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; event.data.object;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; fetch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;https://&amp;lt;host&amp;gt;/events/&amp;lt;username&amp;gt;/Billing&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      method: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;POST&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      headers: { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      body: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        title: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Payment failed&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        message: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;`${&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;invoice&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;customer_email&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ??&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; invoice&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;customer&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;} — $${&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;invoice&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;amount_due&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; /&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;toFixed&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;}`&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        priority: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;High&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        nature: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;error&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        dataJson: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;JSON&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;stringify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;({ invoiceId: invoice.id }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;      }),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  res.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;status&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;send&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;ok&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Verifying &lt;code&gt;stripe-signature&lt;/code&gt; matters here specifically: this endpoint is publicly reachable, and skipping verification means anyone who finds the URL can send you a fake “payment failed” page.&lt;/p&gt;
&lt;h2 id=&quot;add-a-resolve-button&quot;&gt;Add a resolve button&lt;/h2&gt;
&lt;p&gt;If you refund or retry the charge from a dashboard you host, attach a &lt;code&gt;callback&lt;/code&gt; so the notification can trigger that directly — see &lt;a href=&quot;/docs/&quot;&gt;the API contract&lt;/a&gt; for the shape, and &lt;a href=&quot;/recipes/cron-systemd/&quot;&gt;cron/systemd&lt;/a&gt; for a simpler starting point if webhook signing feels like overkill for now.&lt;/p&gt;
</content:encoded><category>Recipes</category></item><item><title>Send Uptime Kuma alerts to your phone</title><link>https://paprpla.in/recipes/uptime-kuma/</link><guid isPermaLink="true">https://paprpla.in/recipes/uptime-kuma/</guid><description>Wire Uptime Kuma&apos;s Webhook notification type to a Paperplane channel with a custom body template, so a monitor going down becomes an alert, not a log line.</description><pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Uptime Kuma’s default webhook payload is shaped for its own dashboard, not for Paperplane’s &lt;code&gt;title&lt;/code&gt;/&lt;code&gt;message&lt;/code&gt; fields — so the notification needs a custom body template, which Uptime Kuma supports natively.&lt;/p&gt;
&lt;h2 id=&quot;set-up-the-notification&quot;&gt;Set up the notification&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Settings → Notifications → Setup Notification&lt;/strong&gt;, type &lt;strong&gt;Webhook&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the URL to your channel’s endpoint: &lt;code&gt;https://&amp;lt;host&amp;gt;/events/&amp;lt;username&amp;gt;/Uptime&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Turn on &lt;strong&gt;Custom Body&lt;/strong&gt;, and set the request body to match Paperplane’s payload, using Uptime Kuma’s template variables:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;json&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;title&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;{{ monitorJSON.name }} is {{ heartbeatJSON.status }}&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;message&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;{{ msg }}&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;priority&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;High&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;  &quot;nature&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;pending&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;If the channel has an endpoint key, add it as a custom header field if your Uptime Kuma version exposes one for webhook notifications — otherwise lock the channel down by IP or leave it open behind Uptime Kuma’s own network.&lt;/li&gt;
&lt;li&gt;Attach the notification to each monitor you want on your phone, or set it as a default for new monitors.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;why-nature-pending&quot;&gt;Why &lt;code&gt;nature: pending&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;An Uptime Kuma “down” event isn’t necessarily an error you can fix yet — it might resolve itself. &lt;code&gt;pending&lt;/code&gt; still promotes it to an alert you have to acknowledge, without asserting it’s already a confirmed failure. Switch to &lt;code&gt;error&lt;/code&gt; if you’d rather every down event page you as a hard failure from the start.&lt;/p&gt;
&lt;p&gt;Uptime Kuma sends a second notification when the monitor recovers — that one lands as a plain message unless you also set &lt;code&gt;nature&lt;/code&gt;/&lt;code&gt;priority&lt;/code&gt; in a second template for the “up” case.&lt;/p&gt;
</content:encoded><category>Recipes</category></item></channel></rss>