PilotFoxDocs

Slack and Discord webhooks

Send new-sale notifications to a Slack or Discord channel through an incoming webhook.

A site can post its new-sale notifications to one Slack channel and one Discord channel, each through an incoming webhook: a secret URL that lets whoever has it post to that channel.

Create the webhook

  1. Create a Slack app for your workspace (or open an existing one) and turn on Incoming Webhooks (Slack's guide).
  2. Click Add New Webhook to Workspace and pick the channel.
  3. Copy the webhook URL. It looks like https://hooks.slack.com/services/T…/B…/….

PilotFox accepts only each service's documented webhook form, so a URL can never send your sales elsewhere:

ServiceAccepted URLs
Slackhttps://hooks.slack.com/services/<id>/<id>/<secret>, letters and digits only, no query string
Discordhttps://discord.com/api/webhooks/<id>/<token> (also with a version, /api/v10/webhooks/…), optionally with ?thread_id=<id> and nothing else

Both: https only, that exact host, no port, no user name or password, no #fragment. Anything else is refused with 400 ("Not a Slack incoming webhook URL (https://hooks.slack.com/services/…)" or "Not a Discord webhook URL (https://discord.com/api/webhooks/…)"), and a URL longer than 512 characters with 400 "Invalid request".

Add it in PilotFox

Open Site settings → Notifications, paste the URL under Slack or Discord, and click Send test to check it. You need the site:update permission (editors and above) and revenue:read on the site, since the channel will receive every sale's amount, source and country.

The URL is stored encrypted and never returned: the settings show only its host and last four characters, such as hooks.slack.com/…a1b2. A webhook can be switched off without deleting it.

Over the API:

# Connect (or replace) the Slack webhook; it is switched on unless you send "enabled": false.
# SLACK_WEBHOOK_URL holds the URL Slack gave you: like the API key, keep it out of the command line itself.
curl -X PUT "https://pilotfox.app/api/v1/sites/SITE_ID/notifications/slack" \
  -H "Authorization: Bearer $PILOTFOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"url\": \"$SLACK_WEBHOOK_URL\"}"

# Switch it off, keeping the URL:
curl -X PUT "https://pilotfox.app/api/v1/sites/SITE_ID/notifications/slack" \
  -H "Authorization: Bearer $PILOTFOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled": false}'

# Remove it:
curl -X DELETE "https://pilotfox.app/api/v1/sites/SITE_ID/notifications/slack" \
  -H "Authorization: Bearer $PILOTFOX_API_KEY"

Use /notifications/discord the same way. Each call answers 200 with the site's notification settings:

200 OK
{
  "data": {
    "recipients": [],
    "members": [{ "userId": "8c314866-bce3-411e-a51b-8e5258b540f5", "name": "Admin", "email": "admin@example.com" }],
    "webhooks": [
      { "kind": "slack", "connected": true, "enabled": true, "urlHint": "hooks.slack.com/…XXXX", "lastError": null, "lastSentAt": null },
      { "kind": "discord", "connected": false, "enabled": false, "urlHint": null, "lastError": null, "lastSentAt": null }
    ]
  }
}

The body takes url, enabled or both. Switching on ("enabled": true) or sending a url needs revenue:read; switching off and deleting need site:update only. Switching a webhook that was never connected is 400 ("Paste a Slack webhook URL first").

Who connected it

PilotFox records who set the webhook's URL, each time it is set: the signed-in member, or the API key used. Before each message, a test included, it checks that this member or key may still see the site's revenue:

  • A member must still belong to the organization, not be banned, and have revenue:read on the site.
  • An API key must still exist, be enabled and unexpired, hold revenue:read, and not be limited to other sites.

When the check fails, nothing is posted and the webhook shows as failing with the reason, so deleting an API key also stops the webhooks it connected. Switching the webhook off and on again does not change who connected it. To keep it, a member who has revenue:read pastes the URL again (Replace URL): the webhook is then theirs. Webhooks connected before PilotFox recorded this (before the 2026-10-30 update) send nothing until their URL is pasted again.

The message format

Each message has a bold headline, one line per sale when there are several, and a link to the site's Revenue page.

PilotFox posts JSON with a text field in Slack's markup:

*3 new sales: $147.00*
• $49.00 · from Hacker News (news.ycombinator.com) · Germany
• $49.00 · from Google (google.com) · France
• $49.00 · from Direct
<https://pilotfox.app/sites/SITE_ID/revenue|Revenue of Docs shop>

&, < and > in any text are escaped.

A single sale is one line: New sale: $49.00 · from Hacker News (news.ycombinator.com) · Germany. A test message starts with "Test:" and says that no sale was made.

When a delivery fails

Each sale is sent at most once. A delivery is retried once right away after a network error, a 5xx, or a 429 that asks to wait 5 seconds or less; otherwise the message is dropped and the webhook's lastError says why (it clears on the next successful delivery, which sets lastSentAt):

lastErrorCause
"Slack no longer accepts this webhook (it was removed or disabled). Paste a new one."401, 403, 404 or 410 from the service
"Slack refused the message."Any other 4xx, or a redirect
"Slack's rate limit was reached; the message was dropped."429
"Slack could not be reached."Network errors, timeouts (10 seconds) and 5xx
"The stored webhook URL can no longer be read (the server secret changed). Paste it again."A self-hosted instance's BETTER_AUTH_SECRET changed
"The person who connected this webhook can no longer see this site's revenue. Paste its URL again to keep it."The member who set the URL left the organization, lost revenue:read on the site, was banned, or their account was deleted (who connected it); nothing was posted
"The API key that connected this webhook was deleted or can no longer see this site's revenue. Paste its URL again to keep it."The API key that set the URL was deleted, disabled or expired, or no longer holds revenue:read on the site; nothing was posted
"This webhook was connected before PilotFox recorded who connects webhooks. Paste its URL again to keep it."The URL was set before the 2026-10-30 update; nothing was posted

(With "Discord" in place of "Slack" for a Discord webhook.) Redirects are never followed, and the service's own error text is never stored or shown.

On this page