Push Notifications Setup

FreqDroid supports two ways to deliver push notifications. Pick whichever fits your workflow.

ntfyFCM via push.freqdroid.com
SetupSelf-host an ntfy serverJust register in the app
BatteryForeground service or pollingReal push, no service or polling
LatencyInstant (Live) or up to intervalInstant
PlatformsAndroid + iOSAndroid (iOS coming)
Encryption in transitTLS to your serverTLS + per-device sealed-box ciphertext through Google FCM
Trading data leaves your networkYes (to your ntfy)Yes (to push.freqdroid.com → Google FCM, encrypted)

Both transports use the same FreqTrade webhook config — only the URL changes — and the rich exit-notification callback at the bottom of this page works for both.


ntfy

FreqTrade publishes messages to your ntfy server via webhooks. FreqDroid subscribes to the same topics and displays native OS notifications.

FreqTrade bot  →  POST  →  ntfy server  →  stream  →  FreqDroid  →  notification

Step 1 — Run an ntfy server

You can use the public ntfy.sh server for testing, but for production use with trading data a self-hosted instance is strongly recommended.

docker run -p 80:80 -v /var/cache/ntfy:/var/cache/ntfy binwiederhier/ntfy serve

docker-compose

services:
  ntfy:
    image: binwiederhier/ntfy
    command: serve
    ports:
      - "80:80"
    volumes:
      - ntfy-cache:/var/cache/ntfy
volumes:
  ntfy-cache:

Put this behind a reverse proxy (nginx, Caddy, Traefik) with TLS so the URL is https://ntfy.example.com. HTTPS is required for notifications to work reliably on Android 9+ and iOS.

See the ntfy self-hosting docs for full configuration options.

Step 2 — Note your topic names

FreqDroid derives a topic name for each bot automatically:

topic = "freqdroid-" + botName.lowercase()
                               .replace(' ', '-')
                               .filter { alphanumeric or '-' }
Bot name in appntfy topic
MyBotfreqdroid-mybot
Binance BTCfreqdroid-binance-btc
bot_1freqdroid-bot1

The Settings screen shows the computed topic as a hint.

Step 3 — Configure FreqTrade webhooks

In your FreqTrade config.json, add a webhook section that POSTs to your ntfy topic. Use "format": "raw" so FreqTrade sends plain text.

Important: Include #{trade_id} in your messages. FreqDroid looks for #<number> in the notification text to enable tap-to-navigate — tapping opens the trade detail screen directly.

"webhook": {
    "enabled": true,
    "url": "https://ntfy.example.com/freqdroid-mybot",
    "format": "raw",
    "entry": {
        "data": "Entry placed (#{trade_id})\n{direction} {pair} @ {open_rate:.6f} | stake {stake_amount:.2f} {stake_currency}"
    },
    "entry_fill": {
        "data": "Trade opened (#{trade_id})\n{pair} @ {open_rate:.6f} | stake {stake_amount:.2f} {stake_currency}"
    },
    "entry_cancel": {
        "data": "Entry cancelled (#{trade_id})\n{pair} entry order cancelled"
    },
    "exit": {
        "data": "Exit placed (#{trade_id})\n{pair} @ {limit:.6f}"
    },
    "exit_fill": {
        "data": "Trade closed (#{trade_id})\n{pair} | profit {profit_amount:.2f} {stake_currency} ({profit_ratio:.2%})"
    },
    "exit_cancel": {
        "data": "Exit cancelled (#{trade_id})\n{pair} exit order cancelled"
    },
    "status": {
        "data": "Status: {status}"
    }
}

Restart FreqTrade after editing config.json. Test with:

curl -d "Test message (#1)" https://ntfy.example.com/freqdroid-mybot

Step 4 — Enable notifications in FreqDroid

  1. Open the app, tap the hamburger menu, then SettingsNotification Server.
  2. At the top, choose the ntfy transport.
  3. Toggle Enable notifications on (in the parent Settings screen).
  4. Enter your ntfy server base URL (e.g. https://ntfy.example.com) — no trailing slash, no topic name.
  5. If your server requires Basic Auth, enter Username and Password.
  6. On Android 13+, grant the notification permission when prompted.
  7. Choose a delivery mode (see below) and tap Save.

Delivery modes (Android)

Delivery on iOS

iOS uses BGAppRefreshTask to poll ntfy in the background at the configured interval. iOS controls exact timing — the interval is a minimum, not a guarantee. The app also polls when it comes to the foreground.


FCM (push.freqdroid.com)

FreqTrade posts to a single endpoint at push.freqdroid.com. The relay encrypts each payload with your device’s public key (NaCl sealed box, X25519 + XSalsa20-Poly1305) and forwards it to Google FCM. Your device decrypts and displays the notification — Google sees only ciphertext, and the relay never logs payloads.

FreqTrade  →  POST  →  push.freqdroid.com  →  sealed box  →  FCM  →  device

Android only at the moment. iOS support arrives with the App Store release.

Step 1 — Register in the app

  1. Open the app → hamburger menu → SettingsNotification Server.
  2. At the top, switch the transport to FCM.
  3. Tap Register & Save. The app generates a keypair (kept locally), uploads your FCM token + public key to the relay, and the status changes to Registered.
  4. Tap Copy URL to put the webhook URL on your clipboard.
FCM notification settings

The URL is of the form:

https://push.freqdroid.com/v1/push/<deviceToken>

Treat the URL as a shared secret — anyone who knows it can post notifications to your device (they cannot read any of your trade data, only flood your notifications). Tap Unregister any time to invalidate it; tap Register & Save afterwards to get a fresh URL.

Step 2 — Configure FreqTrade webhooks

Use the same webhook block as ntfy, but with format: json and bot_name included so the in-app notification list can group by bot. Send the FreqTrade fields you care about as JSON:

"webhook": {
    "enabled": true,
    "url": "https://push.freqdroid.com/v1/push/PASTE_YOUR_DEVICE_TOKEN_HERE",
    "format": "json",
    "entry": {
        "bot_name": "MyBot",
        "type": "entry",
        "status": "Entry placed (#{trade_id})\n{direction} {pair} @ {open_rate:.6f} | stake {stake_amount:.2f} {stake_currency}"
    },
    "entry_fill": {
        "bot_name": "MyBot",
        "type": "entry",
        "status": "Trade opened (#{trade_id})\n{pair} @ {open_rate:.6f} | stake {stake_amount:.2f} {stake_currency}"
    },
    "entry_cancel": {
        "bot_name": "MyBot",
        "type": "entry_cancel",
        "status": "Entry cancelled (#{trade_id})\n{pair} entry order cancelled"
    },
    "exit": {
        "bot_name": "MyBot",
        "type": "exit",
        "status": "Exit placed (#{trade_id})\n{pair} @ {limit:.6f}"
    },
    "exit_fill": {
        "bot_name": "MyBot",
        "type": "exit",
        "status": "Trade closed (#{trade_id})\n{pair} | profit {profit_amount:.2f} {stake_currency} ({profit_ratio:.2%})"
    },
    "exit_cancel": {
        "bot_name": "MyBot",
        "type": "exit_cancel",
        "status": "Exit cancelled (#{trade_id})\n{pair} exit order cancelled"
    },
    "status": {
        "bot_name": "MyBot",
        "type": "status",
        "status": "Status: {status}"
    }
}

bot_name field — with ntfy the per-bot topic identifies which bot fired. With FCM all of your bots post to the same URL, so include bot_name (or name) in the JSON if you want the bot name shown in the FreqDroid in-app list. The notification system tray title is taken from title/type; the body from status/message.

Trade ID for tap-to-navigate — same as ntfy: include (#{trade_id}) in status for tap-to-navigate.

Restart FreqTrade after editing config.json. Verify the path with curl using your copied URL:

curl -X POST "https://push.freqdroid.com/v1/push/<deviceToken>" \
  -H "Content-Type: application/json" \
  -d '{"bot_name":"Test","type":"entry","status":"Test entry (#1) BTC/USDT"}'

A notification should appear on the device within ~2 seconds. The relay returns {"status":"ok"} once it has handed the message to FCM.

Switching back to ntfy

Tap Unregister in the FCM section, then switch the transport segment to ntfy. The FCM URL becomes invalid the moment you unregister; FreqTrade webhooks pointed at it will get {"detail":"unknown device token"}. Update them to the new ntfy URL.


Rich exit notifications via strategy callback

For richer exit messages — with profit-based emoji, partial-exit handling, trade duration, and exit reason — replace the built-in exit_fill webhook with a custom notification sent from your strategy’s order_filled callback. The same Python works for both ntfy and FCM; only the wrapping changes (raw text vs JSON).

Webhook config (no exit_fill):

"webhook": {
    "enabled": true,
    "url": "https://ntfy.example.com/freqdroid-mybot",
    "format": "raw",
    "retries": 3,
    "retry_delay": 0.2,
    "allow_custom_messages": true,
    "entry": {
        "data": "Entry placed (#{trade_id})\n{direction} {pair} @ {open_rate:.6f} | stake {stake_amount:.2f} {stake_currency}"
    },
    "entry_fill": {
        "data": "Trade opened (#{trade_id})\n{pair} @ {open_rate:.6f} | stake {stake_amount:.2f} {stake_currency}"
    },
    "entry_cancel": {
        "data": "Entry cancelled (#{trade_id})\n{pair} entry order cancelled"
    },
    "exit": {
        "data": "Exit placed (#{trade_id})\n{pair} @ {limit:.6f}"
    },
    "exit_cancel": {
        "data": "Exit cancelled (#{trade_id})\n{pair} exit order cancelled"
    },
    "status": {
        "data": "Status: {status}"
    }
}

For FCM, switch "format": "raw" to "format": "json", change the URL to your https://push.freqdroid.com/v1/push/<deviceToken> and replace each "data": "..." with the JSON fields shown in the FCM section (bot_name, type, status).

Strategy callback:

from freqtrade.persistence import Trade, Order

def order_filled(self, pair, trade: Trade, order: Order,
                 current_time, **kwargs):
    if order.side != trade.exit_side:
        return

    bot_name = self.config.get('bot_name', 'Bot')
    direction = "Short" if trade.is_short else "Long"
    leverage_text = (f" ({trade.leverage:.3g}x)"
                     if trade.leverage and trade.leverage != 1.0 else "")
    enter_tag = trade.enter_tag or ""
    coin = trade.stake_currency or "USDC"

    # Emoji based on profit tier
    def emoji(r):
        if r >= 0.05:   return "\U0001f911"   # 5%+
        elif r >= 0.02: return "\U0001f680"   # 2-5%
        elif r >= 0:    return "✳️" # 0-2%
        elif r >= -0.02: return "⚠️" # 0 to -2%
        else:           return "\U0001f534"   # worse than -2%

    if trade.is_open:
        # Partial exit (DCA out / scale-down)
        # Calculate sub-profit for just this exit order (not the whole position)
        sub_profit = trade.calculate_profit(order.safe_price, order.safe_filled, trade.open_rate)
        ratio = sub_profit.profit_ratio
        amt = sub_profit.profit_abs
        gain = "profit" if amt >= 0 else "loss"
        cumulative = trade.realized_profit or 0

        msg = (f"{emoji(ratio)} *{bot_name}:* Partially exited "
               f"{trade.pair} (#{trade.id})\n"
               f"*Sub Profit:* `{ratio:.2%} ({gain}: {amt:.2f} {coin}"
               f" / {amt:.2f} USD)`\n")
        if cumulative:
            msg += (f"*Cumulative Profit:* `{cumulative:.2f} {coin}"
                    f" / {cumulative:.2f} USD`\n")
        if enter_tag:
            msg += f"*Enter Tag:* `{enter_tag}`\n"
        msg += (f"*Exit Reason:* `{trade.exit_reason or 'unknown'}`\n"
                f"*Direction:* `{direction}{leverage_text}`\n"
                f"*Amount:* `{trade.amount}`\n"
                f"*Open Rate:* `{trade.open_rate:.4f} {coin}`\n"
                f"*Exit Rate:* `{order.safe_price:.4f} {coin}`\n"
                f"*Remaining:* `{trade.stake_amount:.2f} {coin}`")
    else:
        # Final exit — trade fully closed
        exit_reason = trade.exit_reason or "unknown"

        dur = (trade.close_date or current_time) - trade.open_date
        secs = int(dur.total_seconds())
        dur_str = f"{secs // 86400}d " * (secs >= 86400)
        dur_str += f"{(secs % 86400) // 3600}h {(secs % 3600) // 60}m"

        entry_count = trade.nr_of_successful_entries or 1
        exit_count = trade.nr_of_successful_exits or 1
        is_sub = (entry_count > 1 or exit_count > 1) and trade.realized_profit

        if is_sub:
            # Calculate sub-profit for just this final exit order
            sub_profit = trade.calculate_profit(order.safe_price, order.safe_filled, trade.open_rate)
            ratio = sub_profit.profit_ratio
            amt = sub_profit.profit_abs
        else:
            ratio = trade.close_profit or 0
            amt = trade.close_profit_abs or 0

        gain = "profit" if amt >= 0 else "loss"
        prefix = "Sub " if is_sub else ""

        msg = (f"{emoji(ratio)} *{bot_name}:* Exited "
               f"{trade.pair} (#{trade.id})\n"
               f"*{prefix}Profit:* `{ratio:.2%} ({gain}: {amt:.2f} {coin}"
               f" / {amt:.2f} USD)`\n")
        if is_sub:
            final_ratio = trade.close_profit or 0
            final_amt = trade.realized_profit or 0
            msg += (f"*Final Profit:* `{final_ratio:.2%}"
                    f" ({final_amt:.2f} {coin} / {final_amt:.2f} USD)`\n")
        if enter_tag:
            msg += f"*Enter Tag:* `{enter_tag}`\n"
        msg += (f"*Exit Reason:* `{exit_reason}`\n"
                f"*Direction:* `{direction}{leverage_text}`\n"
                f"*Amount:* `{trade.amount}`\n"
                f"*Open Rate:* `{trade.open_rate:.4f} {coin}`\n"
                f"*Exit Rate:* `{order.safe_price:.4f} {coin}`\n"
                f"*Duration:* `{dur_str}`")

    self.dp.send_msg(msg)

Include (#{trade.id}) in the message body so FreqDroid can detect the trade ID for tap-to-navigate.


Troubleshooting

ntfy

No notifications arriving

Notifications stop after a while (Live mode)

The app maintains a long-lived HTTP streaming connection. If the server closes the connection, the app automatically reconnects with exponential back-off (2 s up to 60 s). No action needed.

ntfy requires authentication

Enter your credentials in the Username and Password fields below the server URL. The app sends a Basic auth header when both fields are non-empty. ntfy access tokens are not supported — use a dedicated ntfy user with a password.

FCM

curl returns {"status":"ok"} but no notification appears

The relay successfully forwarded to FCM. Most common cause on Android: an OEM battery manager blocked the background service. Try the curl test with the screen ON first; if it works that way, add FreqDroid to your phone’s battery whitelist or “auto-launch” list (Settings → Apps → FreqDroid → Battery → Unrestricted).

{"detail":"unknown device token"}

The URL is stale (you re-registered, generating a new token). Open the app, copy the current URL from Settings → Notification Server.

Status reads “Failed”

Usually a network issue contacting push.freqdroid.com. Tap the button again to retry.

Common (both)

Tapping a notification doesn’t open the trade

The notification text must contain #<trade_id> (e.g. (#123)). Make sure your webhook templates include #{trade_id} — see the webhook examples above.