Webhooks

When something changes, Daysli sends a signed POST to the addresses you added under a token with the webhooks scope. Each event carries identifiers only: fetch the object with your token, so you see exactly what the owner allows today.

Events

TypeNeedsSent when
request.createdrequests:readA request reaches the inbox (a blocked contact’s never does)
request.withdrawnrequests:readThe requester, or the token that made it, withdrew it
request.acceptedrequests:readThe owner accepted. Sent after the owner’s 5-second undo; never sent if they undo.
request.declinedrequests:readThe owner declined. Sent after the owner’s 5-second undo; never sent if they undo.
request.expiredrequests:readIts time passed unanswered
appointment.rescheduledrequests:readThe owner proposed a new time. Sent after the owner’s 5-second undo; never sent if they undo.
appointment.confirmedrequests:readThe requester confirmed the new time
appointment.cancelledrequests:readEither side cancelled. Sent after the owner’s 5-second undo; never sent if they undo.
appointment.completedrequests:readThe appointment ended
task.createdtasks:readA to-do was made
task.updatedtasks:readA to-do changed (reordering is not a change)
task.completedtasks:readA to-do was done
task.deletedtasks:readA to-do was deleted
event.createdevents:readAn event or appointment was added to the calendar
event.updatedevents:readIts title, time, place, repeat, calendar or Busy/Free changed
event.deletedevents:readIt was deleted
authorization.revokedalwaysThe token was revoked, expired or its account deleted — the last event it gets
pingalwaysWhen you add an endpoint, and when you ask for a test

Not events, by design: a request’s internal conflict state, a no-show (it reads as completed), and anything to do with the journal.

Payload

POST https://your.example.com/daysli
Content-Type: application/json
User-Agent: Daysli-Webhooks/1.0 (+https://daysli.com/developers/webhooks)
Daysli-Event-Id: 0192f0c4-7d3b-7c2a-9f11-3b0c9e2f5a10
Daysli-Event-Type: request.accepted
Daysli-Delivery-Attempt: 1
Daysli-Signature: t=1790056805,v1=5f2b…c9

{
  "id": "0192f0c4-7d3b-7c2a-9f11-3b0c9e2f5a10",
  "type": "request.accepted",
  "apiVersion": "v1",
  "occurredAt": "2026-10-07T06:00:00Z",
  "account": "acct_Q2p9…",
  "test": false,
  "data": { "object": "request", "id": "7c1e0a2b-…" }
}

authorization.revoked carries "data": { "object": "authorization", "reason": "owner" } (or expired, account_deleted); ping carries { "object": "ping" }. test is true for a sandbox token.

Checking the signature

Daysli-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256> over <t>.<raw body>, keyed with the endpoint’s signing secret (dsl_whsec_…, shown once when you add it). Use the raw body before any JSON parsing, compare in constant time, accept any matching v1 (there are two for 24 hours after you rotate the secret), and reject a time more than five minutes from yours.

TypeScript (Node 18+, Deno, Bun)

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

/** True when the delivery came from Daysli: any v1 matches and the time is within five minutes. */
export function verifyDaysliSignature(
  rawBody: string,
  header: string,
  secret: string,
  now: number = Date.now(),
): boolean {
  const items = header.split(',').map((part) => {
    const at = part.indexOf('=');
    return [part.slice(0, at), part.slice(at + 1)];
  });
  const t = Number(items.find(([key]) => key === 't')?.[1]);
  if (!Number.isFinite(t) || Math.abs(now / 1000 - t) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  return items.some(([key, value]) => {
    if (key !== 'v1') return false;
    const given = Buffer.from(value, 'hex');
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

Python 3

import hashlib
import hmac
import time


def verify_daysli_signature(raw_body: bytes, header: str, secret: str, now: float | None = None) -> bool:
    """True when the delivery came from Daysli: any v1 matches and the time is within five minutes."""
    items = [part.split("=", 1) for part in header.split(",") if "=" in part]
    stamps = [value for key, value in items if key == "t"]
    if not stamps or not stamps[0].isdigit():
        return False
    t = int(stamps[0])
    if abs((now if now is not None else time.time()) - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, value) for key, value in items if key == "v1")

These two are tested against Daysli’s signer on every build.

Delivery

Catch-up

GET /v1/webhook-events?since=… returns, oldest first, every event this token could have received in the last 30 days — the same payloads, paged by cursor. Run it after downtime, or instead of an endpoint if you would rather poll.

Addresses

An endpoint must be https on port 443, a domain name (not an IP address), without credentials in it, and resolve only to public addresses. Daysli checks this when you add it and again before every delivery.