Notifications and webhooks

Receive a phone's notifications on the PC, wait for one, react to every one, reply, open or dismiss them, and forward them to a webhook.

Droidline can pass the notifications that arrive on a phone to your PC. It uses Android's notification access, which gives the full title and text (the accessibility service alone only sees a shortened version).

Turn it on

  1. On the phone, open Droidline's Setup tab and allow Notification access.
  2. Choose which apps may send their notifications to the PC. Nothing is sent until you do, which keeps private messages on the phone. Pick apps on the app's Status tab under Notifications sent to the PC, or from code:
d.notify_filter(["com.google.android.gm", "com.google.android.apps.messaging"])
print(d.notify_filter())          # read the current list
await d.notifyFilter(["com.google.android.gm", "com.google.android.apps.messaging"]);
console.log(await d.notifyFilter());   // read the current list
droidline notify_filter com.google.android.gm,com.google.android.apps.messaging
droidline notify_filter

Wait for one notification

wait_notification blocks until a matching notification arrives and returns it:

n = d.wait_notification("textContains", "verification code", 60)
print(n["package"], n["title"], n["text"])
const n = await d.waitNotification("textContains", "verification code", 60);
console.log(n.package, n.title, n.text);
droidline wait_notification textContains "verification code" 60

The first argument says what to compare: text or textContains look at both the title and the text, title at the title only, and package matches the app. Pass package= as well to only accept notifications from one app.

The server does the waiting, so other commands to the same phone keep running in the meantime. It waits for a notification that arrives after the call. To look at what is already showing, use notifications() or has_notification(by, value).

React to every notification

stop = d.on_notification(package="com.google.android.gm",
                         callback=lambda n: print(n["title"], n["text"]))
# ... your program keeps running; call stop() to end it
const stop = await d.onNotification({ package: "com.google.android.gm" },
  (n) => console.log(n.title, n.text));
// ... call stop() to end it; until then the process keeps running
droidline on_notification --package com.google.android.gm

The CLI prints one JSON line per notification until you press Ctrl+C, which makes it easy to pipe into other tools. A notification looks like this:

{"event":"notification","device":"k7d2q9xa","key":"0|com.google.android.gm|1|null|10123","package":"com.google.android.gm","title":"New message","text":"Lunch at 12?","time":1791360000000,"actions":["reply","mark_read"]}

Act on a notification

Each notification has a key. Use it to open, reply to or dismiss that notification:

for n in d.notifications():
    if "Bank" in n["title"]:
        d.notification_click(n["key"])        # opens what the notification points to

d.notification_reply(key, "On my way")       # only for notifications with a reply action
d.notification_dismiss(key)
for (const n of await d.notifications()) {
  if (n.title.includes("Bank")) await d.notificationClick(n.key);
}

await d.notificationReply(key, "On my way");
await d.notificationDismiss(key);
KEY=$(droidline --json notifications | jq -r '.value[0].key')
droidline notification_click "$KEY"

actions lists what a notification offers. notification_reply only works when it includes reply.

While the phone is offline

The phone keeps up to 500 recent notifications while it cannot reach the PC and sends them, in order, when it reconnects.

Webhooks

The server can POST matching notifications to any URL, so other systems can react without an SDK. Add a section per webhook to config.toml and restart droidline serve:

[[webhooks]]
url = "https://example.com/hooks/droidline"
package = "com.google.android.gm"   # optional
text_contains = "order"             # optional, matches title or text
device = "shelf-01"                 # optional

The body is the notification as JSON plus device and device_name. Every request is signed so you can check that it came from your server:

X-Droidline-Timestamp: 1791360000
X-Droidline-Signature: sha256=<hex HMAC-SHA256 of timestamp + "." + body>

Get the signing secret with droidline webhook secret. Checking the signature in a receiver:

import hashlib, hmac

def valid(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    mac = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest("sha256=" + mac, signature)
import { createHmac, timingSafeEqual } from "node:crypto";

function valid(secret, timestamp, body, signature) {
  const mac = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.`).update(body).digest("hex");
  return mac.length === signature.length && timingSafeEqual(Buffer.from(mac), Buffer.from(signature));
}

Failed deliveries are retried twice, after 1 and 4 seconds.

One-time codes on Android 15 and later

Android 15 hides one-time codes inside notifications from apps that are not the system's own. You still get the notification, but the code is masked. Use the notification as a signal, then read the code inside the app:

d.wait_notification("package", "com.example.bank", 120)
d.launch("com.example.bank")
code = d.get_text("id", "otp_value")
await d.waitNotification("package", "com.example.bank", 120);
await d.launch("com.example.bank");
const code = await d.getText("id", "otp_value");
droidline wait_notification package com.example.bank 120
droidline launch com.example.bank
droidline get_text id otp_value