Your accountNotifications

Your account

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.

On this page

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.

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:

SalesWhat it sendsOn for a new channel
Every saleA quiet receipt as each one lands; a burst comes as one messageYes
First sale of the dayOne line when the day’s first sale landsNo
Refunds and disputesAs each one lands, with where to answer a disputeYes
Daily goal reachedOnce a day, when today passes its goalYes
When something’s offWhat it sendsOn for a new channel
Sales unusually quietAt most once a day, when sales are far below what’s normal by this hourYes
A site goes quietWhen a busy site stops getting visits, and again when they’re backYes
A source stops syncingAnd again when it’s backYes
TestsWhat it sendsOn for a new channel
ExperimentsWhen odo calls a test, ships a winner, or one needs youYes
SummariesWhat it sendsOn for a new channel
Daily summaryYesterday’s earnings and visitors, at 9:00No
Weekly summaryMondays at 9:00: the week, its best day, where a visitor was worth mostYes

On the Growth plan, the weekly summary also carries the headline of odo’s 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-EventWhat it is: the body’s type
Odo-DeliveryIts id, the same as the body’s id
Odo-TimestampWhen it was sent, in unix seconds
Odo-Signaturev1= 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:

JavaScript
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.

This page as markdownllms.txt