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.
The nudge on the overview
Alerts are not part of the guided setup, because nothing can move before positions exist. Once an app has two daily captures and your workspace has no webhook and no email alert, the app overview shows a card offering to add one: an email alert when SMTP is configured, a webhook otherwise. Not now hides the card in that browser, and adding any channel hides it everywhere.Subscribe to events
A new event never joins an existing subscription. To opt in, open Edit events on the channel in Settings, tick the events and save. A webhook keeps its url and its secret. The same dialog holds the confirm drops setting.
Choose the stars of new reviews
review.negative fires for reviews at or below ALERT_REVIEW_SCORE_MAX, one cutoff for the whole instance. review.new fires for every review AsoBeast stores for the first time, and each channel decides which stars it wants. In Settings, tick New review on a webhook or an email alert and a row of five stars appears. All five are ticked by default, which sends every review. Untick the stars you do not want, for example everything but 1 and 2 for a channel that only needs the complaints, or 4 and 5 for a channel that celebrates.
The filter belongs to the channel and applies to review.new only. Over the API it is reviewScores, an optional array of the stars 1 to 5 on a webhook or an email alert. An empty or omitted array means every star, the values are sorted and deduplicated, and a value outside 1 to 5 answers 400. A PATCH without reviewScores leaves it as it is.
A channel can hold both events. A review that its stars list under review.new arrives once, as a new review, and not again as a negative review. Stars it does not list still arrive as negative reviews when they fall at or below the cutoff. So review.new with 4 and 5 stars next to review.negative sends praise and complaints and nothing twice.
The same rules hold in both delivery modes. A new review never fires for a review that was only edited, and never for the reviews an import reads when it first adds an app. In instant delivery every matching review is one message, so a busy app on a channel that takes every star is noisy; batched delivery, the default, folds them into the daily report as a New reviews group.
Milestones, first rankings and overtakes
rank.dropped and rank.improved report moves of at least ALERT_RANK_DROP_THRESHOLD places. These three events report what a move means, whatever its size. Each compares today’s check with the previous check of the same app and keyword, so a keyword checked for the first time fires none of them, and adding keywords never floods a channel.
rank.dropped and rank.improved still fire alongside, so a move from 14 to 8 brings both rank.improved and rank.milestone. All three belong to your own app and arrive in the owned app report; rank.overtaken names the competitor inside it.
“Never ranked” looks back as far as position history is kept, RETENTION_RANKINGS_DAYS, 365 days by default. See what is retained.
A keyword that moves between 10 and 11 fires each time it crosses. In batched delivery, the default, every fact is deduplicated per keyword and day and folded into the daily report, so a noisy boundary costs a line in that report rather than a message per check. Instant delivery sends each check that crosses a boundary as it happens. To hear only about the drops that hold, see Report drops only when they hold.
Report drops only when they hold
By default every check is compared with the one before it. A keyword that slips from 9 to 11 for a day and then comes back firesrank.milestone twice, once for leaving the top 10 and once for returning. Near a boundary those one day wobbles bury the drops that stay.
Turn on Confirm drops before alerting on a webhook or an email alert, in the dialog that adds it or under Edit events, and that channel reports a drop only when the next check confirms it. The setting belongs to one channel, so other channels keep their current behaviour. In Settings it starts ticked for a new email alert and for a new webhook whose url is a Discord, Slack, Microsoft Teams, Telegram or Google Chat channel, because a person reads those, and unticked for a JSON webhook, which usually feeds a script that wants every check. A channel created through the API without the field, and every channel that existed before the setting, report every drop.
What the setting changes:
- It covers
rank.dropped, andrank.milestonewhen the app leaves the top 10, the top 3 or first place. Entering a tier,rank.first,rank.overtakenand every other event arrive as before. - A drop counts as confirmed when the check after it is still down against the check before the drop began: for
rank.dropped, at leastALERT_RANK_DROP_THRESHOLDplaces lower or not found, and for a milestone, still outside the tier. The alert shows that earlier position as the start and the second check as the end, so a drop that deepens reports the final position. - If the keyword recovers, nothing is sent. The alerts that would have undone the drop are skipped too: the matching “back in the top 10” milestone and the
rank.improvedthat follows it. Arank.improvedthat ends at leastALERT_RANK_DROP_THRESHOLDplaces above where the drop began is real news and is sent, and so is a recovery after a drop the channel was told about. - A keyword that keeps falling is reported once. Checks of 9, 11 and 18 report leaving the top 10, from 9 to 18, and no separate
rank.dropped, because the first step was smaller than the threshold. The next report needs a new drop that begins after a check that did not confirm one. - Only stored checks count. A day with no check neither confirms nor cancels a drop, so a keyword paused and resumed is judged on its first stored check after the pause.
- A drop needs two checks, so with daily checks it arrives a day later than it would without the setting. In batched delivery it is in the report that follows the second check.
"confirmed": true. A rank.improved or an entering rank.milestone that undoes a drop a confirming channel never heard about carries "undoesDrop": true; channels without the setting still receive it. Both fields are absent otherwise.
Chart moves
chart.entered, chart.left and chart.milestone report where your own app sits in the store charts, not in a search. Each compares today’s chart check with the previous stored check of the same app, chart kind (free, paid or grossing) and category, whatever the gap between them. The first check ever recorded for a chart fires nothing, so importing an app never floods a channel.
An app is in a chart when it sits within the depth the store answers: 100 on the App Store and 200 on Google Play. A move into a chart at position 7 fires
chart.entered and chart.milestone, the second for entering the top 10, and the daily report shows both as one row. A move from 14 to 8 inside a chart fires chart.milestone alone.
Three cases fire nothing. A chart for which the store listed no apps raises no event, because an empty answer does not show that the app left, and the next check compares with the check before that day. Competitor apps raise none, because the competitor report lists listing changes only and the chart page already marks them. An app that changes its price class or its category raises none either: the chart it leaves keeps its last check and the new chart starts with a first check.
In batched delivery the facts are deduplicated per app, chart and day, and each app’s card in the owned app report gets a Chart moves group with a link to the chart page. Instant delivery sends one notification per event, so one move can send two. See Read top charts for the chart page and Follow category ranks for the charts an app is checked in.
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.
Which endpoints a webhook may reach
A webhook url must behttp or https, must not embed credentials, and must resolve to a public address. Loopback, private, carrier grade NAT, link local and cloud metadata ranges are refused, in both their IPv4 and their IPv6 mapped forms. When the subscription is saved, a literal address in a refused range and a name that can only be local, such as one without a dot or one ending in .local or .internal, are refused at once. Every address a hostname resolves to is checked on the socket as it connects, so a hostname that resolves to an internal address, at save time or only later, is refused at delivery and recorded as not delivered. Redirects are never followed, which keeps the payload and its signature on the origin you registered.
A name that only resolves inside a network is refused the same way. That covers a bare hostname such as hooks or n8n, localhost, and any name ending in .local, .localhost, .internal or .home.arpa. The error names the host and says it resolves inside this network.
Set WEBHOOK_ALLOW_PRIVATE_TARGETS to true to deliver to your own network from a self hosted instance. The url may then name a host instead of an address, for example http://n8n:5678/webhook/asobeast for another service in the same Compose project, which keeps working when the container is recreated with a new address. Inside the api container localhost is the container itself, so name the receiver’s service instead. Credentials in the url and schemes other than http and https stay refused. See the configuration reference for what that changes.
A granular event looks like this.
rank.first carries position and depth instead of the move.
date is the UTC day of the chart check, depth the depth of that check, and from is null when the app was outside the chart.
app object of a granular event may also carry iconUrl, store and country, read from the app when the alert is sent. store and country name the app’s store and home storefront, and iconUrl is the address of the icon the store shows today, or null when no listing capture has recorded one. A consumer that ignores unknown fields needs no change.
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.
apps groups one owned app’s facts in arrays: rankDrops, rankImprovements, rankMilestones, firstRankings, overtakes, chartMoves, serpEntrants, changes, negativeReviews, newReviews, actions and competitors. An array is empty when nothing happened or when the channel does not list its event. The app of an entry, and of each entry of its competitors, carries id, name, store, country and iconUrl.
Branch on event and ignore values and fields you do not recognise. New events and fields arrive as additive changes within a major version.
link is present only when WEB_PUBLIC_URL is set. When it is unset the link is null rather than a broken localhost guess. The Compose stacks always set it, see Hosting behind TLS and a reverse proxy.
Discord, Slack, Microsoft Teams, Telegram and Google Chat urls receive a report in their own format instead of the JSON payload, and every other url, a Slack Workflow Builder url included, receives the payload above; see Connect a chat tool.
Email delivery
Email stays disabled until bothSMTP_HOST and SMTP_FROM are set. Neither one alone enables it.
Confirm a recipient
Adding an email alert sends one confirmation email to the recipient, naming the account that added the address. The alert shows Awaiting confirmation in Settings, Email alerts and receives nothing, not even a test email, until the recipient presses Confirm email address on the page the link opens. That page asks before it acts, so a mail scanner that opens the link confirms nothing. A recipient is confirmed at once only when the address is the verified account email of a member of the workspace. Account emails are verified only where billing and SMTP are both on, so on a self hosted instance every recipient, the owner included, confirms through the mailbox. The confirmation link points at the web app, so it needsWEB_PUBLIC_URL. Without it no confirmation email can be sent, the recipient stays Awaiting confirmation and receives nothing, and the API logs a warning at startup in production. When a confirmation email cannot be sent, for that reason or because the SMTP relay refused it, Settings says so when you add the recipient, and Resend confirmation tries again.
A confirmation link works for 7 days, for the address it was sent to and for the latest email sent for that alert. When you upgraded, alerts that were active counted as confirmed. Paused alerts did not, because a pause may have been an unsubscribe, so each needs Resend confirmation before it delivers again.
Three limits keep confirmation emails from being used to mail strangers:
Adding an alert, changing its address, resending a confirmation and sending a test are each limited to 10 requests a minute per workspace. A refused request answers
429 with a Retry-After header, or 403 for the two alert counts.
The recipient limit counts every workspace together on purpose: it protects the person receiving the email, however many workspaces ask. It counts one mailbox, not one spelling: addresses that reach the same mailbox share the allowance, whatever their capitalization, a +tag after the name, a domain written with accents or in its xn-- form, and for Gmail dots and googlemail.com. The One trial per mailbox section lists every rule. The plus tag is dropped on every domain, so on a provider that treats + as an ordinary character [email protected] and [email protected] also share one allowance. The email is still sent to the address you typed, and only the count is shared. A workspace that spends it delays confirmations to that mailbox from other workspaces until the next UTC day. A workspace past its own limit spends none of a recipient’s.
What an alert email looks like
Every alert email uses the AsoBeast layout: a header with the logo, the details of what happened, a button back into the app and a footer saying why the recipient gets it. The button opens the app page, the action foraction.opened, or the portfolio for the weekly digest. Instant delivery sends one email per event, and batched delivery sends the daily app update and the competitor watch. Subjects start with [asobeast] and never change, so mail filters keep working. Every email also has a plain text part.
The button, the logo, the settings link and the unsubscribe link are built from WEB_PUBLIC_URL. Without it the email shows text only and no link.
A daily update lists up to 20 rows per section. A longer section ends with a link to the page that lists every row, See all 32, or with the count of the rows left out when WEB_PUBLIC_URL is unset. When the report would grow past what Gmail shows before clipping, the HTML part shows fewer rows per section and fewer competitors per app, while the plain text part keeps 20. Listing changes name each field the way App Store Connect and the Play Console do, and an icon change reads Icon changed instead of printing the image address.
Each card opens with the app icon, the app name linking to its page in AsoBeast, and its store and storefront, so a competitor card reads Competitor · App Store · US. A rank move lines up as the keyword, the move and what it crossed. An icon change shows the previous and the new icon side by side, and the plain text part reads Icon changed. Dates read like 24 Oct 2026 in UTC, and an instant alert adds the minute, 24 Oct 2026, 03:10 UTC.
Unsubscribe from an email
Every alert email carries a one click unsubscribe that mail apps show as their own button, and an Unsubscribe link in its footer. Either one pauses that email alert only, while webhooks and other email alerts keep running. It also withdraws the recipient’s confirmation, so switching the alert back on under Settings, Email alerts sends nothing to that address until the address changes and the new one confirms. A link works for the address it was sent to, so changing an alert’s address retires the links in earlier emails. The one click headers needWEB_PUBLIC_URL on https. On plain http only the footer link is offered. See Email deliverability for the sender side.
Try it locally
Run a throwaway SMTP sink, and preview every template without sending anything. 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 followRETENTION_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_HOSTandSMTP_FROMare 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_FROMto an address on the domain the relay accepts. - Deep links are missing.
WEB_PUBLIC_URLis unset, solinkisnullby design. - The email shows no buttons or logo.
WEB_PUBLIC_URLis unset. Set it to the public web origin. - Mail apps show no unsubscribe button. The instance is not on
https, or the relay does not sign with DKIM. See Email deliverability. - A recipient stopped receiving alerts. They unsubscribed, and the email alert shows Unsubscribed in Settings. That alert does not ask the address to confirm again, so a recipient who wants alerts back is added as a new email alert and confirms it.
- A new recipient receives nothing. The alert shows Awaiting confirmation. Ask them to open the confirmation email, check the spam folder, or press Resend confirmation. Without
WEB_PUBLIC_URLno confirmation email can be sent at all. - Adding a recipient answers 429. The recipient’s mailbox already had 5 confirmation emails today under any spelling of its address, or the workspace had 20. The message says which, and the limit resets at midnight UTC.
- Resend confirmation answers 429. A confirmation email went to that alert less than an hour ago. The message says how long to wait.
- Reports arrive at an unpredictable hour. In batched mode the flush waits for every child job to settle, including retries.
CRON_DAILYsets the start, not the delivery time.
Related
Work the Action Center
Where
action.opened comes from.Compare a portfolio
Where the weekly digest comes from.