# Notifications

> A quiet receipt of each sale in Slack, Discord, Telegram, email, a signed webhook or the phone app, alerts when sales or visits go quiet, and weekly summaries.

odo sends a quiet receipt of each sale where you already are, and says when something looks wrong before you’d notice. For example: *$49.00 from Stripe · Verified · traced to a visit from Google in Berlin.*

## Channels

Add one in **Settings → Notifications**. Each covers one project, or Global.

- **Slack:** an incoming webhook’s address.
- **Discord:** a channel webhook’s address.
- **Telegram:** your own bot. Paste its token from @BotFather, message the bot once, then **Find chat**.
- **Email:** any address.
- **Webhook:** any https address of yours, signed (below).
- **The phone app** adds itself when you sign in to it: see [the phone app](https://odo.gg/docs/phone-app).

Owners and admins manage channels. Addresses and tokens are encrypted like source keys and never shown again. Slack, Discord and Telegram are only ever reached at their official addresses.

## What each channel sends

Each channel switches on what it wants:

| Sales | What it sends | On for a new channel |
| --- | --- | --- |
| Every sale | A quiet receipt as each one lands; a burst comes as one message | Yes |
| First sale of the day | One line when the day’s first sale lands | No |
| Refunds and disputes | As each one lands, with where to answer a dispute | Yes |
| Daily goal reached | Once a day, when today passes its goal | Yes |

| When something’s off | What it sends | On for a new channel |
| --- | --- | --- |
| Sales unusually quiet | At most once a day, when sales are far below what’s normal by this hour | Yes |
| A site goes quiet | When a busy site stops getting visits, and again when they’re back | Yes |
| A source stops syncing | And again when it’s back | Yes |

| Tests | What it sends | On for a new channel |
| --- | --- | --- |
| Experiments | When odo calls a test, ships a winner, or one needs you | Yes |

| Summaries | What it sends | On for a new channel |
| --- | --- | --- |
| Daily summary | Yesterday’s earnings and visitors, at 9:00 | No |
| Weekly summary | Mondays at 9:00: the week, its best day, where a visitor was worth most | Yes |

On the [Growth plan](https://odo.gg/docs/plans), the weekly summary also carries the headline of [odo’s review](https://odo.gg/docs/analysis#the-weekly-review) and its first thing to do. A review finished after the summary went out follows on its own, as `review.weekly`.

Bursts are gathered: a sale or refund waits a few seconds for others, never more than a minute, and messages to one channel are at least 20 seconds apart and at most 30 an hour. Fifty sales in a minute arrive as one message.

## When something looks wrong

- **Sales unusually quiet**, at most once a day per project: *Sales are quiet. $40.00 so far today, usually about $210 by 15:00.* Today is held against the last 28 days at the same time (and against the same weekday alone, whichever is lower). It needs at least 14 of those days with a sale and 5 sales usual by now, today at 30% of usual or less, and a chance of 1 in 1,000 or less of a day that slow. It also notices a dry spell after a normal morning.
- **A site goes quiet**, once per silence, and again when visits are back: *odo hasn’t seen a visit on example.com since 14:05.* It needs two weeks of history, half an hour of silence at least, and every one of the last 14 days with 10 or more visits in the same hours.

Both count only sources that report sales one by one and are syncing, never demo data, and nothing from before odo was running.

## The signed webhook

Each delivery is a JSON `POST` with these headers:

| Header | |
| --- | --- |
| `Odo-Event` | What it is: the body’s `type` |
| `Odo-Delivery` | Its id, the same as the body’s `id` |
| `Odo-Timestamp` | When it was sent, in unix seconds |
| `Odo-Signature` | `v1=` and the hex HMAC-SHA256 of `<timestamp>.<raw body>`, keyed with the channel’s signing secret |

The signing secret is shown once, when you make the channel. Recompute the signature over the raw body, compare it in constant time, and refuse a timestamp more than five minutes off, so a captured delivery can’t be replayed:

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

function fromOdo(rawBody, headers, secret) {
  const ts = headers['odo-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = Buffer.from(`v1=${createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex')}`);
  const given = Buffer.from(headers['odo-signature'] ?? '');
  return given.length === expected.length && timingSafeEqual(given, expected);
}
```

The body is `{ id, type, createdAt, scope, title, lines, fields, text, url, data }`. `type` is one of `sale`, `sales`, `first_sale`, `refund`, `dispute`, `refunds`, `goal`, `sales.quiet`, `site.quiet`, `site.recovered`, `source.error`, `source.recovered`, `digest.daily`, `digest.weekly`, `review.weekly` or `test`, and `data` holds its facts (a refund’s `amount` is negative). New types may be added: ignore the ones you don’t know.
