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
| Type | Needs | Sent when |
|---|---|---|
request.created | requests:read | A request reaches the inbox (a blocked contact’s never does) |
request.withdrawn | requests:read | The requester, or the token that made it, withdrew it |
request.accepted | requests:read | The owner accepted. Sent after the owner’s 5-second undo; never sent if they undo. |
request.declined | requests:read | The owner declined. Sent after the owner’s 5-second undo; never sent if they undo. |
request.expired | requests:read | Its time passed unanswered |
appointment.rescheduled | requests:read | The owner proposed a new time. Sent after the owner’s 5-second undo; never sent if they undo. |
appointment.confirmed | requests:read | The requester confirmed the new time |
appointment.cancelled | requests:read | Either side cancelled. Sent after the owner’s 5-second undo; never sent if they undo. |
appointment.completed | requests:read | The appointment ended |
task.created | tasks:read | A to-do was made |
task.updated | tasks:read | A to-do changed (reordering is not a change) |
task.completed | tasks:read | A to-do was done |
task.deleted | tasks:read | A to-do was deleted |
event.created | events:read | An event or appointment was added to the calendar |
event.updated | events:read | Its title, time, place, repeat, calendar or Busy/Free changed |
event.deleted | events:read | It was deleted |
authorization.revoked | always | The token was revoked, expired or its account deleted — the last event it gets |
ping | always | When 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")
Delivery
- At least once, not in order. Deduplicate on
Daysli-Event-Id, and fetch the object to learn its current state. - Answer any
2xxwithin ten seconds. Redirects are not followed; the body you answer with is read up to 1 KB and discarded. - A failure is retried after 1 minute, 5 minutes, 30 minutes, 2, 6, 12 and 24 hours (±10%), then given up. Given-up deliveries can be sent again from Settings → Developer.
- An endpoint that has failed for three days is turned off and the owner told. Turning it back on does not replay what was missed — use catch-up.
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.