How to send a push notification from Go
A worker drains a queue, a batch job wraps up, a healthcheck flips red — and your phone buzzes. In Go it's a net/http POST to your Lert URL using nothing but the standard library. Here's the idiomatic, close-the-body version.
What you'll need
The Lert app on your iPhone and any recent Go toolchain. Copy your endpoint from the app — it looks like https://lert.dev/p/your-id. That URL is the authentication; there's no key or token to manage. Keep it out of committed source and regenerate it in the app if it leaks.
Because this is nothing but net/http, it slots into whatever you're building — a background worker, a CLI, an HTTP handler, or a long-running daemon. Read the endpoint from the environment with os.Getenv so the URL never lands in source control and you can point a dev binary at a separate channel.
The minimal example
Marshal a struct with encoding/json and POST it with http.Post. The title, body, and tag fields are all optional; omitempty keeps unset ones out of the payload.
package main import ( "bytes" "encoding/json" "net/http" ) func main() { payload, _ := json.Marshal(map[string]string{ "title": "Job done", "body": "queue drained · 0 left", }) resp, err := http.Post( "https://lert.dev/p/your-id", "application/json", bytes.NewReader(payload), ) if err != nil { panic(err) } defer resp.Body.Close() }
That defer resp.Body.Close() isn't optional etiquette — Go holds the connection open until the body is closed, so skipping it leaks connections over time.
A reusable Notify function
Wrap it into something you can call from anywhere. Read the endpoint from an environment variable, use a struct with JSON tags, give the client a timeout, and return an error the caller can handle.
package lert import ( "bytes" "encoding/json" "fmt" "net/http" "os" "time" ) type msg struct { Title string `json:"title,omitempty"` Body string `json:"body,omitempty"` Tag string `json:"tag,omitempty"` } var client = &http.Client{Timeout: 10 * time.Second} func Notify(title, body, tag string) error { payload, err := json.Marshal(msg{title, body, tag}) if err != nil { return err } resp, err := client.Post( os.Getenv("LERT_URL"), "application/json", bytes.NewReader(payload), ) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("lert: unexpected status %d", resp.StatusCode) } return nil }
The tag is a free-form label that shows up in your in-app history, so a worker ping is easy to distinguish from a deploy ping later.
Checking the status code
A success is 200 with {"delivered":1}. Decode the body when you want to be sure the message actually reached a device rather than just being accepted.
var result struct { Delivered int `json:"delivered"` Warning string `json:"warning"` Error string `json:"error"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return err } if result.Delivered == 0 { return fmt.Errorf("lert: not delivered: %s", result.Warning) }
Worth handling: 404 {"error":"unknown endpoint"} means the URL is wrong or was regenerated, and 200 with delivered:0 means no device is registered yet — open the Lert app once on your phone.
Backing off on 429
Lert allows 60 requests/minute per IP and 50/day on the free plan. When you exceed a limit you get 429 with a Retry-After header; honor it.
if resp.StatusCode == http.StatusTooManyRequests { wait, _ := strconv.Atoi(resp.Header.Get("Retry-After")) if wait == 0 { wait = 5 } time.Sleep(time.Duration(wait) * time.Second) // retry the request once here }
Plain text vs JSON
For a one-line message you can skip JSON. Send the text with strings.NewReader and a text/plain content type — important when the message contains = or &, so it isn't parsed as form data.
resp, err := http.Post(
os.Getenv("LERT_URL"),
"text/plain",
strings.NewReader("Healthcheck failed: db=down & cache=ok"),
)
A real-world snippet
Fire a notification from a defer so it runs whether the worker returns cleanly or panics its way out — you hear about both.
func runWorker() { var err error defer func() { if err != nil { Notify("Worker failed ❌", err.Error(), "worker") } else { Notify("Worker finished ✅", "clean exit", "worker") } }() err = process() }
Deploying this somewhere scheduled? The same Notify slots into a cron job, a monitoring loop, or a deploy step. Prefer another language? See Python, Node.js, or the curl deep-dive.
Why it's this simple
There's no Go module to go get because there's nothing to configure. The endpoint URL is the credential — no auth handshake, no token refresh, no client to keep current. Notifying your phone from Go is standard library and a struct.
Keep Notify in a small internal package and it composes with the rest of your code: call it from a defer, hook it into your structured logger so an error-level line also pings you, or fire it from a context cancellation path. One reusable function, and every service you run gains a direct line to your pocket.
FAQ
Do I need a third-party HTTP library in Go?
No. The standard library net/http and encoding/json are all you need to POST a JSON body to Lert. There's no SDK to add.
Why must I close resp.Body?
Go keeps the connection open until the body is closed, so skipping defer resp.Body.Close() leaks connections. Always close it, even if you don't read the body.
How do I send plain text instead of JSON?
Pass a strings.NewReader with your text and set the content type to text/plain. Do this when the message contains = or & so it isn't treated as form data.
How do I handle a 429 rate limit?
Check for resp.StatusCode == http.StatusTooManyRequests, read the Retry-After header, sleep that many seconds, and retry. Limits are 60 requests/minute per IP and 50/day on the free plan.
Related guides
Ship a Notify, hear from your code.
Download Lert, copy your URL, drop in a Notify function. Your Go services tell you the moment they're done.