Skip to content

API

The API your app calls over its binding to Chaski, and the user routes its frontend calls.

ChaskiEntrypoint

Your app calls Chaski through its service binding to ChaskiEntrypoint, as Connect your app sets it up. A binding exposes methods only, so the groups are methods: env.CHASKI.feed().list(query) is still a single round trip.

CallReturns
ready(){ channels, paused, problems, environment, templates }Checks the setup and the applied migrations, lists the channels that are on, the paused ones and the saved settings that stopped working (problems: { channel, message }[]), names the environment from CHASKI_ENVIRONMENT (null without one) and counts the templates, changing nothing; throws the config error a send would.
send(request)SendResultSends now; see Sending.
schedule(request)ScheduleResultSends at scheduledAt; see Scheduling.
schedules().list({ recipientId? })ScheduledItem[]Pending schedules, soonest first.
schedules().cancel(query){ cancelled }Cancels by id, idempotencyKey or groupKey.
feed().list(query)FeedPageA recipient’s feed, newest first, a page at a time.
feed().unreadCount(recipientId){ unread }
feed().markRead(input){ marked }Marks ids, everything before an instant, or the whole feed.
preferences().get(recipientId)ChaskiPreferences
preferences().set(recipientId, update)ChaskiPreferencesMerges channels; mutedTypes overwrites the list.
preferences().topics(){ topic, types }[]Template types grouped by topic.
push().register(request){ id }Registers a web push subscription, an iOS or macOS device token, an Android FCM token or a Windows channel URI.
push().unregister(query){ unregistered }By recipientId, endpoint or deviceToken.
push().vapidPublicKey(){ publicKey }The key for pushManager.subscribe(), or null.
templates().import(file, { preview? })TemplateImportResultCreates or updates templates by type, and their translations, from a template file; preview only reports. Takes version 1 and 2 files.
templates().export()TemplateFileEvery template with its translations, as a version 2 file templates().import() takes.

ready() suits a health route of your app: it asks the database each time, so a database that doesn’t answer fails it too. templates counts the templates outside the trash; at 0, every send is refused as an unknown type, as on a fresh database or after a deploy that forgot to load them. paused names the channels someone paused in the dashboard, whose deliveries wait as held, and problems the channels whose deliveries fail until someone saves their settings again.

TypeScript
app.get('/health', async (c) => {
  try {
    const status = await c.env.CHASKI.ready();
    const healthy = status.templates > 0 && status.problems.length === 0;
    return c.json({ chaski: status }, healthy ? 200 : 503);
  } catch (error) {
    return c.json({ chaski: String(error) }, 503);
  }
});

Requests

TypeScript
type SendRequest = {
  type: string;
  recipient: Recipient;
  payload?: Record<string, unknown>;
  channels?: Array<'email' | 'in_app' | 'push'>;
  transactional?: boolean;
  email?: { fromName?: string; replyTo?: string; attachments?: AttachmentInput[] };
  // silent shows the push without a sound, on Apple devices, on Windows and in browsers;
  // collapseId replaces a push already shown with the same id and becomes its tag.
  push?: { silent?: boolean; collapseId?: string };
  // For your clients, as is: the feed item and every push carry it. Strings only, since FCM
  // data and WNS launch arguments hold nothing else; up to 20 keys.
  data?: Record<string, string>;
  // The language of this send alone, ahead of recipient.locale; a BCP 47 tag. One that isn't
  // counts as absent.
  locale?: string;
  idempotencyKey?: string;
  // Delivers the one transactional email before send() returns; see Sending.
  inline?: boolean;
};

type ScheduleRequest = Omit<SendRequest, 'inline'> & {
  // ISO 8601 with an explicit offset: Z or ±hh:mm.
  scheduledAt: string;
  groupKey?: string;
};

type Recipient = {
  id: string;
  email?: string;
  name?: string;
  phone?: string;
  timezone?: string;
  locale?: string;
};

type AttachmentInput = {
  filename: string;
  contentType: string;
  // Exactly one of content (base64) or url.
  content?: string;
  url?: string;
  disposition?: 'attachment' | 'inline';
  contentId?: string;
};

Feed items

TypeScript
type FeedItem = {
  id: string;
  // The notification it came from; its pushes carry the same id as their tag, unless the send
  // named a collapseId.
  notificationId: string;
  type: string;
  title: string;
  body: string;
  href?: string;
  // The send's data, or null.
  data: Record<string, string> | null;
  readAt: string | null;
  createdAt: string;
};

type FeedPage = { items: FeedItem[]; nextCursor: string | null };

feed.list() takes a recipientId, and optionally a cursor from the last page, a limit up to 100 and unreadOnly.

User routes

userRoutes() answers these paths, relative to where your app mounts it, for the user identify returns, and forwards each one to Chaski. chaskijs/client calls them for you.

Method and path
GET /feedThe feed; cursor, limit and unreadOnly as query parameters.
GET /feed/unread-count{ unread }
POST /feed/readMarks items read: { ids?, before? }, or {} for the whole feed.
GET /feed/socketA WebSocket that announces new items, whole, and reads made in other tabs and devices. ?device=<id> names a device the client registered, whose pushes the feed takes over while the socket is open.
GET /preferencesThe user’s preferences.
PATCH /preferencesUpdates them.
GET /preferences/topicsTemplate types grouped by topic.
GET /push/vapid-public-key{ publicKey }
POST /push/subscriptionsRegisters this browser’s subscription, an iOS or macOS device token, an Android FCM token or a Windows channel URI; answers { id }.
DELETE /push/subscriptionsDrops one, by endpoint or deviceToken.

A path the mount doesn’t serve, including those of a group left out of groups, reaches your app’s own 404, and identify doesn’t run for it. So a second mount can sit under the first one’s path (/me/notifications/team under /me/notifications) and both answer, in whichever order you register them.

Writes and socket upgrades from an origin other than the request’s own or one in allowedOrigins answer 403. The mount also serves GET /t/o/:deliveryId, the open-tracking pixel of Chaski’s emails, to anyone. Errors answer as { error, message }, with the codes in Errors.

Package entries

EntryRuns in
chaskijs/workerThe Chaski WorkerThe Worker’s handlers, ChaskiEntrypoint, the Durable Objects and the Workflow, which a Chaski project’s main module re-exports.
chaskijsYour app’s WorkeruserRoutes(), the errors, type ChaskiEntrypoint and every type of the API, in a few kilobytes.
chaskijs/clientThe browsercreateChaskiClient, chaskiQueries, ChaskiRequestError.
chaskijs/swYour service workerhandlePushEvents.
chaskijs/reactThe browser, with React 18 or 19ChaskiProvider and the hooks.

Updated Oct 30, 2026

MIT License · chaski.sh