Skip to main content
Alerts fan out to two channel types, webhooks and email, and both carry the same events. Every attempt is recorded in a delivery log so a failing endpoint is visible instead of retrying silently.

Choose a delivery mode

ALERT_DELIVERY has two values and defaults to batched. In batched mode each subscribed channel receives the owned app report first and the competitor report second. Subscription filters apply to both scopes independently, so an owned only channel receives one report and a competitor only channel receives the other. An empty scope produces no notification at all. Batched delivery is durable. A flush claims a fixed snapshot of pending rows before sending, so a queue or process failure leaves that claim resumable with the same rows, timestamp and delivery identifiers. Later facts wait for a later claim rather than joining the retry. Re runs and multi market checks deduplicate the same fact before it is claimed, with the latest values winning. There is no separate alert schedule. CRON_DAILY controls when processing starts, while workload and retry duration decide when the report goes out.

Subscribe to events

Existing subscribers are unaffected by action.opened until they opt into it on the settings page.

Webhook delivery

A webhook receives a JSON POST with two headers, plus a signature header when the subscription has a secret. Verify the signature by computing an HMAC-SHA256 of the raw request body with your subscription secret and comparing it, in constant time, with the header value. Compute it over the bytes you received rather than over a re serialized object. A granular event looks like this.
A batched report wraps events in a scoped envelope. Branch on the top level scope, which is owned_apps or competitors. The flat events array stays granular and contains only events for that scope, so a consumer that already handles granular events needs one extra branch rather than a rewrite.
link is present only when WEB_PUBLIC_URL is set. That variable is used for nothing else. When it is unset the link is null rather than a broken localhost guess. Slack, Discord and ntfy all accept a plain webhook endpoint, and asobeast formats for the first two.

Email delivery

Email stays disabled until both SMTP_HOST and SMTP_FROM are set. Neither one alone enables it. To try it locally, run a throwaway SMTP sink. See Set up a development environment.

Read the delivery history

Every attempt on every channel is logged with its outcome and surfaced per channel on the settings page. Rows follow RETENTION_DELIVERIES_DAYS, which defaults to 30 days. Completed batch claims follow RETENTION_ALERT_EVENTS_DAYS, also 30 days by default. Claims that have not finished are retained regardless, so a stuck flush is never pruned out from under its retry.

Troubleshooting

  • The webhook endpoint returns a non 2xx status. The delivery is logged as failed with the status. Fix the endpoint and use flush now to make an early claim of the facts currently pending.
  • The signature does not verify. Compute the HMAC over the raw request body bytes. Re serializing the parsed JSON changes whitespace and key order, which changes the digest.
  • Email never sends. Check that both SMTP_HOST and SMTP_FROM are set. One without the other leaves email disabled.
  • The relay rejects the sender. Many relays require the envelope sender to match an authorized domain. Set SMTP_FROM to an address on the domain the relay accepts.
  • Deep links are missing. WEB_PUBLIC_URL is unset, so link is null by design.
  • Reports arrive at an unpredictable hour. In batched mode the flush waits for every child job to settle, including retries. CRON_DAILY sets the start, not the delivery time.

Work the Action Center

Where action.opened comes from.

Compare a portfolio

Where the weekly digest comes from.
Last modified on July 31, 2026