Know the moment a calendar changes
One signed webhook for every change on Google Calendar, Outlook and Zoom: an event moved in a calendar app, an invitation answered, a connection broken, a meeting started. Pyas keeps the provider subscriptions alive, retries for 41 hours and lets you replay what you missed.
{
"id": "evt_8f1c2b7a9d0e4f6a",
"type": "event.updated",
"createdAt": "2026-11-03T14:03:11Z",
"livemode": true,
"accountId": "fe8b3509…",
"subjectId": "user_42",
"provider": "google",
"data": {
"object": {
"id": "6v1q8b3k2m9t0c4p7r5s",
"calendarId": "primary",
"seriesId": null,
"isSeries": false
}
}
}What Google and Microsoft leave to you
Both providers can tell you when a calendar changes. Keeping that working for every user is the part that takes weeks.
Channels that expire
Google watch channels expire, a week by default, and there is no call to renew one. You open a new channel before the old one runs out, for every calendar.
Pyas: Pyas opens and renews the channel for every connected Google calendar.
Subscriptions that stop
Microsoft Graph subscriptions last seven days at most, one day if they carry the event's data, and stop without warning when they are not renewed. Creating one starts a validation handshake your endpoint must answer.
Pyas: Pyas creates, validates and renews the subscription for every Outlook calendar.
Notifications that say little
A Google notification says only that something in the calendar changed. You run a sync to find out what, and a stale sync token sends you back to a full sync.
Pyas: Each webhook names the event or calendar that changed, by its IDs.
Two formats
Google and Microsoft send different headers, different bodies and different ideas of what a change is. A recurring series changes in different ways on each.
Pyas: One format for both, with a whole series changed reported once.
Subscribe to what you use
Give an endpoint exact types, a family such as event.*, or * for everything, including types added later.
- event.*An event changed, through the API or in Google Calendar or Outlook.event.createdevent.updatedevent.deleted
- invitation.*Someone answered an invitation sent from a Pyas calendar.invitation.replied
- calendar.*A calendar was added, renamed or removed.calendar.createdcalendar.updatedcalendar.deleted
- connection.*A user connected, reconnected or lost an account.connection.createdconnection.reauthorizedconnection.brokenconnection.deleted
- meeting.*A Zoom meeting started or ended, or someone joined or left.meeting.startedmeeting.endedmeeting.updatedmeeting.deletedmeeting.participant_joinedmeeting.participant_left
- recording.*A Zoom recording is ready to fetch.recording.ready
Check the signature, then read the event
A webhook carries IDs, not the event itself. Your handler reads the event with one call and always gets it as it is now, even when two changes arrive close together. Pyas keeps no copies of your users' events to send.
When a whole recurring series changes, you get one webhook for the series, not one per occurrence.
import express from 'express'
import { Pyas, constructWebhookEvent } from '@pyas/sdk'
const pyas = new Pyas({ apiKey: process.env.PYAS_API_KEY })
const secret = process.env.PYAS_WEBHOOK_SECRET
const raw = express.raw({ type: 'application/json' })
app.post('/webhooks/pyas', raw, async (req, res) => {
let delivery
try {
const signature = req.header('pyas-signature')
delivery = await constructWebhookEvent(req.body, signature, secret)
} catch {
return res.status(400).end() // not signed by Pyas
}
res.status(200).end() // answer first, then do the work
if (delivery.type === 'event.updated') {
const { id, calendarId } = delivery.data.object
const event = await pyas.events.get({
accountId: delivery.accountId,
eventId: id,
calendarRef: calendarId,
})
await saveEvent(event)
}
})8 attempts over 41 hours
An endpoint that is down for a deploy, or for a weekend, still gets every change once it is back.
- NowFirst attempt
- 30 sRetry 1
- 2 minRetry 2
- 15 minRetry 3
- 1 hRetry 4
- 4 hRetry 5
- 12 hRetry 6
- 24 hRetry 7
- Signed
- HMAC-SHA256 over the timestamp and the raw body, in the Pyas-Signature header. Rotate a secret and both signatures are sent for 24 hours, so you can switch without missing a delivery.
- Retried
- Any answer other than 2xx, or no answer within 10 seconds, and Pyas tries again on the schedule above.
- Replayable
- Replay one delivery, or every failed delivery since a time you choose, up to 1,000 per request. Events are kept for 30 days.
- Safe to repeat
- Every retry of an event carries the same id, so your handler can skip one it has seen.
- Visible
- Every attempt is listed with its time, the status your endpoint returned and how long it took, in the API and the dashboard.
- Paused, not lost
- After 50 failed deliveries in a row, an endpoint is disabled and marked in the dashboard. Enable it again and Pyas sends a test delivery first.
Test webhooks before you deploy
Endpoints made with a test key get the sandbox's events, so you can watch real deliveries while you write the handler.
POST /v2/webhooks/{webhookId}/test
{ "type": "event.updated" }Questions
- Do Pyas webhooks include the event's details?
- No. A webhook carries the IDs of the event or calendar that changed, and your app reads it with one request. That way Pyas keeps no copies of your users' events, and you always read the event as it is now, not as it was when the webhook was sent.
- Do I still need Google push channels or Microsoft Graph subscriptions?
- No. Pyas opens a Google watch channel and a Microsoft Graph subscription for every connected calendar, renews them before they expire, and turns their notifications into one signed webhook format for both providers.
- How do I verify a Pyas webhook signature?
- Each delivery has a Pyas-Signature header in the form t=<unix seconds>,v1=<hex>. The v1 value is an HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with the endpoint's signing secret. With the SDK, constructWebhookEvent checks it for you. Reject signatures older than five minutes.
- What happens if my endpoint is down?
- Pyas retries a failed delivery 7 more times, after 30 seconds, 2 minutes, 15 minutes, 1 hour, 4 hours, 12 hours and 24 hours: 8 attempts over about 41 hours. Anything still missed can be replayed, one delivery at a time or every failed delivery since a time you choose. Events are kept for 30 days.
- Can I receive webhooks on localhost?
- Yes, while you develop. Run pyas webhooks listen --forward http://localhost:3000/webhooks and the CLI forwards your sandbox's events to your machine, signed the same way as in production.
- Will I get the same webhook twice?
- You can. If your endpoint takes longer than 10 seconds to answer, Pyas counts the attempt as failed and sends it again. Every retry of an event has the same id, so store the ids you have handled and skip repeats.