How to Send a Ruby Push Notification to Your iPhone
Send a Ruby push notification to your iPhone in about five lines — no gems, no APNs setup. Plain Net::HTTP, plus a Rails example.
Your Sidekiq job swallowed an exception at 6 a.m. and quietly retried itself into oblivion. You found out hours later, when a customer asked why their export came back empty. A push to your phone would have told you in seconds — and from Ruby, sending one is about five lines.
Ruby gives you plenty of ways to catch a failure — a Sidekiq retry, a rescue block, an exception tracker filling up with rows. Every one of them assumes you're at a screen, looking. A push notification doesn't wait for you to check: it lands on your phone the second the rescue block runs, whether you're at your desk or on the train.
Most "send a notification from Ruby" tutorials reach for a gem, an APNs certificate, a provider account, and an OAuth dance. For a personal ping — the job died, the deploy shipped, someone signed up — that's wildly out of proportion. TheNotificationApp is a single HTTP endpoint: one POST, one header, a JSON body. No gem in your Gemfile, no certificate to rotate. Here's the whole thing.
What you need
- A TheNotificationApp account and an
app_key— the free tier is enough to follow along. - The iOS app installed and signed in with the same account, so the notification has somewhere to land.
- Ruby. That's it.
Net::HTTPships in the standard library, so there are no gems to install.
The five lines
Drop your key in an environment variable and fire a POST at the endpoint. This is the entire integration:
require "net/http"
require "json"
Net::HTTP.post(
URI("https://thenotification.app/api/sendNotification"),
{ title: "Deploy finished", body: "Build #4821 is live in production." }.to_json,
"app_key" => ENV["NOTIFICATION_APP_KEY"], "Content-Type" => "application/json"
)Run it with NOTIFICATION_APP_KEY=your_key ruby notify.rb and your phone buzzes. The .to_json is the part that's easy to forget — the API expects a JSON body, and a Ruby hash isn't one until you convert it. Skip it and you'll get a 400 back.
A method you'll actually reuse
An inline call is fine for a throwaway script. For anything you'll call from more than one place, wrap it once so the URL, headers, and error handling live in a single spot:
require "net/http"
require "json"
require "uri"
def notify(title, body, **extra)
uri = URI("https://thenotification.app/api/sendNotification")
payload = { title: title, body: body }.merge(extra).to_json
res = Net::HTTP.post(
uri,
payload,
"app_key" => ENV.fetch("NOTIFICATION_APP_KEY"),
"Content-Type" => "application/json"
)
raise "Notification failed: #{res.code} #{res.body}" unless res.code == "200"
JSON.parse(res.body)
end
notify("Backup done", "Nightly dump finished in 4m 12s.")Two small choices earn their keep here. ENV.fetch raises if the key is missing, so a misconfigured server fails loudly instead of sending nothing. And raising on any non-200 means a broken notification can't hide a broken job — the failure surfaces where you'd expect it.
Sending more than text
Two optional fields are worth knowing about:
link— a URL the notification opens when you tap it. Point it straight at the PR, the dashboard, or the failed run.image— an image URL shown inside the notification.
Because notify takes keyword args, you pass them through without touching the method:
notify(
"New signup",
"alex@example.com just created an account.",
link: "https://dash.example.com/users/8842"
)Both are optional — leave them off and you get a plain title-and-body push. There are only four fields total, so there's nothing to memorize; the API reference has the complete list.
Wiring it into a Rails app
Calling a push notification API from Rails works best where you already care that something went sideways: a background job or a rescue block. A job is the cleanest home, because failures there are otherwise invisible until someone complains:
class ImportJob < ApplicationJob
rescue_from(StandardError) do |error|
notify("Import job failed", "#{self.class.name}: #{error.message}")
raise error
end
def perform(csv_path)
Importer.run(csv_path)
end
endThe rescue_from fires the push and then re-raises, so Active Job (or Sidekiq) still records the failure and retries as configured — you get the alert and keep the normal behavior. Put notify in a small lib/ file or a service object so jobs, mailers, and rake tasks can all reach it. One thing to avoid: don't block a web request on the call. An HTTP round-trip to an external service in the request cycle adds latency to a page load — push it to a job instead.
Reading the response
A success comes back as JSON you can log or ignore:
{ "success": true, "message": "Notification sent to 1 device(s)", "devices_sent": 1, "failed_count": 0 }The codes worth handling: 401 means a missing or wrong app_key, 400 means you left out title or body, and 429 means you hit your limit — the body tells you the cap:
{ "success": false, "error": "Monthly message limit exceeded", "limit": 100, "received": 101 }The notify method above turns each of those into a raised exception, which inside a job means they land in the same place as every other failure you already track. Worth a glance at the rate limits page if you're wiring this into something busy.
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 plenty to wire up "deploy shipped" and "nightly job died" and then forget the whole thing exists. But point it at a busy Sidekiq queue that retries on every blip and you'll burn through 100 in an afternoon. Pro is $2.99/month for 1,000 notifications a month and up to 10 apps. Either way, put a threshold in front of anything chatty — notify on the failure, not on each of 500 retries.
That's the whole thing
No gem, no certificate, one POST you can read at a glance. If you've got a Ruby script or a Rails job whose failures you currently hear about from users first, this is worth a look — you can start free at thenotification.app. Working in more than one language? The Python and Node.js versions are the same idea with different syntax.
Stop babysitting your scripts.
Free to download. Free tier available. Swiss-hosted, private by design.