Send Push Notifications from Go: the net/http Version
Send a push notification from Go in about 15 lines: one net/http POST, no SDK. Ping your iPhone when a job finishes or dies.

You kicked off a 40-minute data migration in a Go binary on a remote box, closed the laptop, and went to make dinner. Did it finish? Did it panic on row two million? Right now the only way to know is to SSH back in and squint at the logs.
Go is the language of CLIs, daemons, and background workers: the exact kind of thing that runs unattended and really ought to tell you how it went. A systemd unit, a Kubernetes CronJob, a worker on a box you log into twice a month: when one of those dies quietly, the gap between the failure and you noticing it is measured in days.
You don't need a logging stack or a Slack bot for this. One HTTP POST to TheNotificationApp and your phone buzzes. And because it's plain net/http, there's nothing to install: no SDK, no third-party module, no go get. The whole golang push notification is one function and the standard library.
What you'll need
- A free TheNotificationApp account and an app key. It goes in the
app_keyheader: the same key the curl docs use, so if you've tested with curl already, you're set. - Go (any recent 1.x) and
net/httpfrom the standard library. That's the entire dependency list. - The app installed on your iPhone, signed in with the same account. That's the device the ping lands on.
One function does the whole job
Here's the core. It marshals a JSON body, fires a POST, and decodes what comes back, including the one error you'll eventually hit (more on that later). Drop it in any package:
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type notifyResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
DevicesSent int `json:"devices_sent"`
Error string `json:"error"`
Limit int `json:"limit"`
}
func notify(apiKey, title, body string) error {
payload, err := json.Marshal(map[string]string{
"title": title,
"body": body,
})
if err != nil {
return err
}
req, err := http.NewRequest(
http.MethodPost,
"https://thenotification.app/api/sendNotification",
bytes.NewReader(payload),
)
if err != nil {
return err
}
req.Header.Set("app_key", apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var result notifyResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if resp.StatusCode == http.StatusTooManyRequests {
return fmt.Errorf("notify: quota hit (%s, limit %d)", result.Error, result.Limit)
}
if !result.Success {
return fmt.Errorf("notify: %s", result.Error)
}
return nil
}Two headers, two required fields. title and body are the only things the API needs; link and image are optional and you can add them to that map the same way. This is the same go http push notification you'd send from any other language. The request shape is identical across the Node.js version and the rest. Go just happens to need zero extra packages to do it.
Wire it into a real job
The function is only useful when something calls it at the right moment. The pattern that earns its keep: ping on the way out, whether the work succeeded or blew up.
func runImport() error {
// your actual work goes here
return nil
}
func main() {
const apiKey = "YOUR_API_KEY"
if err := runImport(); err != nil {
notify(apiKey, "Import failed", err.Error())
return
}
if err := notify(apiKey, "Import done", "All rows processed cleanly."); err != nil {
fmt.Println("could not send notification:", err)
}
}One ping on success, one on failure, and the failure message carries the actual error text straight to your lock screen. If you'd rather guarantee a ping no matter how the function returns, a defer at the top of main works too, but the explicit two-branch version makes the title accurate, and "Import failed" vs "Import done" is the thing you actually want to read at a glance.
Make the ping tappable
A title and body get you the alert. A link makes it actionable, tap the notification and land straight on the failed run instead of digging for it. It's the same map, one more key:
payload, err := json.Marshal(map[string]string{
"title": "Build failed",
"body": "main branch, exit code 1",
"link": "https://ci.example.com/runs/4821",
})For a Go service running somewhere headless, this is the field that earns its place. Drop in the URL of whatever you'd open next (the CI run, the Grafana board, the log viewer), and the notification stops being a heads-up and becomes a one-tap shortcut to the thing that broke. There's an optional image field too if you want a chart or a screenshot riding along; neither link nor image costs you anything beyond the one notification you were already sending.
What comes back
On success the API returns a small JSON object, which is why the function decodes into notifyResponse:
{
"success": true,
"message": "Notification sent to 1 device(s)",
"devices_sent": 1,
"failed_count": 0
}Checking devices_sent is handy: if it's zero, the call technically worked but nothing reached you (usually because no device is signed in yet). Worth logging in production.
When you run out of quota, you get a 429 instead, and the body tells you exactly what happened:
{
"success": false,
"error": "Monthly message limit exceeded",
"limit": 1000,
"received": 1000
}That's the branch the function turns into a real Go error so a drained quota doesn't fail silently. Full field list is in the API reference.
A word about goroutines
Go makes it a one-liner to fan out a thousand notifications from a worker pool. Resist. Each call to notify spends one notification from your quota, so a for loop that pings on every processed item will empty your allowance before lunch. Notify on the outcome (job started, job finished, job died), not on every unit of work inside it.
The honest tradeoff
Fair warning on the numbers: the free tier is 100 notifications total, not per month, total, across up to 3 apps. That's genuinely fine for "tell me when the nightly job finishes," which is one ping a day. But if you wire this into a hot path or a chatty goroutine, you'll burn the lot in an afternoon and then hit that 429. The Pro tier is $2.99/month for 1,000 notifications a month, which is the realistic line for anything you run on a schedule. Either way, keep the pings on outcomes and you'll stay well under. The current numbers live on the pricing page.
That's it
One function, the standard library, and your Go jobs stop disappearing into the void. Worth a look if you're tired of SSHing back in just to find out whether something finished, grab a free key at thenotification.app and wire it into the next job you'd hate to babysit.
New to this? Start with what a push notification API actually is.



