[{"data":1,"prerenderedAt":1844},["ShallowReactive",2],{"navigation":3,"page:\u002Fwebhooks":45},{"title":4,"links":5},"API reference",[6,9,12,15,18,21,24,27,30,33,36,39,42],{"title":7,"href":8},"Accounts","\u002Freference\u002Faccounts",{"title":10,"href":11},"Connect","\u002Freference\u002Fconnect",{"title":13,"href":14},"Calendars","\u002Freference\u002Fcalendars",{"title":16,"href":17},"Events","\u002Freference\u002Fevents",{"title":19,"href":20},"Availability","\u002Freference\u002Favailability",{"title":22,"href":23},"Meetings","\u002Freference\u002Fmeetings",{"title":25,"href":26},"Invitations","\u002Freference\u002Finvitations",{"title":28,"href":29},"Webhooks","\u002Freference\u002Fwebhooks",{"title":31,"href":32},"Metadata","\u002Freference\u002Fmetadata",{"title":34,"href":35},"Keys","\u002Freference\u002Fkeys",{"title":37,"href":38},"Sandbox","\u002Freference\u002Fsandbox",{"title":40,"href":41},"Usage","\u002Freference\u002Fusage",{"title":43,"href":44},"Logs","\u002Freference\u002Flogs",{"kind":46,"title":28,"description":47,"sections":48,"doc":73},"guide","Receive a signed notification for every change as it happens. Deliveries are retried for 41 hours and can be replayed, and you can test them before you write your handler.",[49,52,55,58,61,64,67,70],{"id":50,"title":51},"add-an-endpoint","Add an endpoint",{"id":53,"title":54},"verify-every-delivery","Verify every delivery",{"id":56,"title":57},"what-a-delivery-looks-like","What a delivery looks like",{"id":59,"title":60},"event-types","Event types",{"id":62,"title":63},"retries","Retries",{"id":65,"title":66},"replay","Replay",{"id":68,"title":69},"test-before-you-build","Test before you build",{"id":71,"title":72},"endpoints-on-v1-payloads","Endpoints on v1 payloads",{"id":74,"title":28,"body":75,"description":47,"extension":1835,"hero":1836,"meta":1837,"navigation":357,"path":1838,"rawbody":1839,"sections":1840,"seo":1841,"stem":81,"v1":1842,"__hash__":1843},"docs\u002Fwebhooks.md",{"type":76,"value":77,"toc":1820},"minimark",[78,82,89,92,95,256,273,284,299,301,320,924,927,940,946,948,1174,1234,1243,1312,1321,1326,1413,1416,1439,1441,1582,1585,1597,1599,1606,1609,1611,1614,1626,1632,1644,1647,1659,1661,1664,1679,1691,1697,1703,1728,1731,1733,1736,1740,1743,1753,1766,1769,1775,1779,1782,1806,1809,1816],[79,80,28],"h1",{"id":81},"webhooks",[83,84,85],"lead",{},[86,87,88],"p",{},"Pyas sends an HTTPS request to your endpoint when something changes, for example when an event is created or moved in a calendar app, when a connection breaks, or when a meeting starts. Every delivery is signed, and a delivery that fails is retried for up to 41 hours. Pyas also stores each delivery so you can replay it.",[90,91,51],"h2",{"id":50},[86,93,94],{},"To receive webhooks, create an endpoint with your handler's URL and the event types you want to receive:",[96,97,98,203],"code-group",{},[99,100,106],"pre",{"className":101,"code":102,"filename":103,"language":104,"meta":105,"style":105},"language-js shiki shiki-themes css-variables","const { endpoint, signingSecret } = await pyas.webhooks.create({\n  url: 'https:\u002F\u002Fyourapp.com\u002Fwebhooks\u002Fpyas',\n  events: ['event.*', 'connection.broken'],\n})\n","Node.js","js","",[107,108,109,157,173,197],"code",{"__ignoreMap":105},[110,111,114,118,122,126,130,133,136,139,142,145,149,151,154],"span",{"class":112,"line":113},"line",1,[110,115,117],{"class":116},"sag7P","const",[110,119,121],{"class":120},"sqSWz"," { ",[110,123,125],{"class":124},"sXULy","endpoint",[110,127,129],{"class":128},"sEEt9",",",[110,131,132],{"class":124}," signingSecret",[110,134,135],{"class":120}," } ",[110,137,138],{"class":116},"=",[110,140,141],{"class":116}," await",[110,143,144],{"class":124}," pyas",[110,146,148],{"class":147},"s1IGz",".",[110,150,81],{"class":124},[110,152,153],{"class":147},".create",[110,155,156],{"class":120},"({\n",[110,158,160,163,166,170],{"class":112,"line":159},2,[110,161,162],{"class":120},"  url",[110,164,165],{"class":116},":",[110,167,169],{"class":168},"sk1Au"," 'https:\u002F\u002Fyourapp.com\u002Fwebhooks\u002Fpyas'",[110,171,172],{"class":128},",\n",[110,174,176,179,181,184,187,189,192,195],{"class":112,"line":175},3,[110,177,178],{"class":120},"  events",[110,180,165],{"class":116},[110,182,183],{"class":120}," [",[110,185,186],{"class":168},"'event.*'",[110,188,129],{"class":128},[110,190,191],{"class":168}," 'connection.broken'",[110,193,194],{"class":120},"]",[110,196,172],{"class":128},[110,198,200],{"class":112,"line":199},4,[110,201,202],{"class":120},"})\n",[99,204,209],{"className":205,"code":206,"filename":207,"language":208,"meta":105,"style":105},"language-bash shiki shiki-themes css-variables","curl -X POST https:\u002F\u002Fapi.pyas.io\u002Fv2\u002Fwebhooks \\\n  -H \"x-api-key: $PYAS_API_KEY\" \\\n  -H \"content-type: application\u002Fjson\" \\\n  -d '{ \"url\": \"https:\u002F\u002Fyourapp.com\u002Fwebhooks\u002Fpyas\", \"events\": [\"event.*\", \"connection.broken\"] }'\n","cURL","bash",[107,210,211,229,239,248],{"__ignoreMap":105},[110,212,213,216,220,223,226],{"class":112,"line":113},[110,214,215],{"class":147},"curl",[110,217,219],{"class":218},"sTB4W"," -X",[110,221,222],{"class":218}," POST",[110,224,225],{"class":218}," https:\u002F\u002Fapi.pyas.io\u002Fv2\u002Fwebhooks",[110,227,228],{"class":120}," \\\n",[110,230,231,234,237],{"class":112,"line":159},[110,232,233],{"class":218},"  -H",[110,235,236],{"class":168}," \"x-api-key: $PYAS_API_KEY\"",[110,238,228],{"class":120},[110,240,241,243,246],{"class":112,"line":175},[110,242,233],{"class":218},[110,244,245],{"class":168}," \"content-type: application\u002Fjson\"",[110,247,228],{"class":120},[110,249,250,253],{"class":112,"line":199},[110,251,252],{"class":218},"  -d",[110,254,255],{"class":168}," '{ \"url\": \"https:\u002F\u002Fyourapp.com\u002Fwebhooks\u002Fpyas\", \"events\": [\"event.*\", \"connection.broken\"] }'\n",[86,257,258,261,262,265,266,269,270,272],{},[107,259,260],{},"events"," accepts exact event types, families such as ",[107,263,264],{},"event.*",", or ",[107,267,268],{},"*"," for every type, including types added later. The default is ",[107,271,268],{},". An endpoint created with a test key receives events from the sandbox, and an endpoint created with a live key receives your users' events. You can also manage endpoints in the dashboard.",[86,274,275,276,279,280,283],{},"To verify deliveries, you need the endpoint's signing secret (",[107,277,278],{},"whsec_...","). The response that creates the endpoint includes it as ",[107,281,282],{},"signingSecret",". To get it again later, call:",[99,285,289],{"className":286,"code":287,"language":288,"meta":105,"style":105},"language-http shiki shiki-themes css-variables","GET \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Fsecret\n","http",[107,290,291],{"__ignoreMap":105},[110,292,293,296],{"class":112,"line":113},[110,294,295],{"class":116},"GET",[110,297,298],{"class":120}," \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Fsecret\n",[90,300,54],{"id":53},[86,302,303,304,307,308,311,312,315,316,319],{},"Each delivery includes a ",[107,305,306],{},"Pyas-Signature"," header in the format ",[107,309,310],{},"t=\u003Cunix seconds>,v1=\u003Chex>",". The ",[107,313,314],{},"v1"," value is an HMAC-SHA256 of ",[107,317,318],{},"\u003Ct>.\u003Craw body>",", computed with the endpoint's secret. Verify the raw body exactly as you received it, before you parse it. Reject signatures that are older than 5 minutes.",[96,321,322,566],{},[99,323,325],{"className":101,"code":324,"filename":103,"language":104,"meta":105,"style":105},"import express from 'express'\nimport { constructWebhookEvent } from '@pyas\u002Fsdk'\n\napp.post('\u002Fwebhooks\u002Fpyas', express.raw({ type: 'application\u002Fjson' }), async (req, res) => {\n  let event\n  try {\n    event = await constructWebhookEvent(req.body, req.header('pyas-signature'), process.env.PYAS_WEBHOOK_SECRET)\n  } catch {\n    return res.status(400).end()\n  }\n  res.status(200).end()          \u002F\u002F respond first, then process the event\n  await handle(event)\n})\n",[107,326,327,341,353,359,411,420,428,483,494,519,525,549,561],{"__ignoreMap":105},[110,328,329,332,335,338],{"class":112,"line":113},[110,330,331],{"class":116},"import",[110,333,334],{"class":120}," express ",[110,336,337],{"class":116},"from",[110,339,340],{"class":168}," 'express'\n",[110,342,343,345,348,350],{"class":112,"line":159},[110,344,331],{"class":116},[110,346,347],{"class":120}," { constructWebhookEvent } ",[110,349,337],{"class":116},[110,351,352],{"class":168}," '@pyas\u002Fsdk'\n",[110,354,355],{"class":112,"line":175},[110,356,358],{"emptyLinePlaceholder":357},true,"\n",[110,360,361,364,367,370,373,375,378,381,384,386,389,392,394,397,400,402,405,408],{"class":112,"line":199},[110,362,363],{"class":124},"app",[110,365,366],{"class":147},".post",[110,368,369],{"class":120},"(",[110,371,372],{"class":168},"'\u002Fwebhooks\u002Fpyas'",[110,374,129],{"class":128},[110,376,377],{"class":124}," express",[110,379,380],{"class":147},".raw",[110,382,383],{"class":120},"({ type",[110,385,165],{"class":116},[110,387,388],{"class":168}," 'application\u002Fjson'",[110,390,391],{"class":120}," })",[110,393,129],{"class":128},[110,395,396],{"class":116}," async",[110,398,399],{"class":120}," (req",[110,401,129],{"class":128},[110,403,404],{"class":120}," res) ",[110,406,407],{"class":116},"=>",[110,409,410],{"class":120}," {\n",[110,412,414,417],{"class":112,"line":413},5,[110,415,416],{"class":116},"  let",[110,418,419],{"class":120}," event\n",[110,421,423,426],{"class":112,"line":422},6,[110,424,425],{"class":116},"  try",[110,427,410],{"class":120},[110,429,431,434,436,438,441,443,446,449,451,454,457,459,462,465,467,470,472,475,477,480],{"class":112,"line":430},7,[110,432,433],{"class":120},"    event ",[110,435,138],{"class":116},[110,437,141],{"class":116},[110,439,440],{"class":147}," constructWebhookEvent",[110,442,369],{"class":120},[110,444,445],{"class":124},"req",[110,447,448],{"class":120},".body",[110,450,129],{"class":128},[110,452,453],{"class":124}," req",[110,455,456],{"class":147},".header",[110,458,369],{"class":120},[110,460,461],{"class":168},"'pyas-signature'",[110,463,464],{"class":120},")",[110,466,129],{"class":128},[110,468,469],{"class":124}," process",[110,471,148],{"class":120},[110,473,474],{"class":124},"env",[110,476,148],{"class":120},[110,478,479],{"class":124},"PYAS_WEBHOOK_SECRET",[110,481,482],{"class":120},")\n",[110,484,486,489,492],{"class":112,"line":485},8,[110,487,488],{"class":120},"  } ",[110,490,491],{"class":116},"catch",[110,493,410],{"class":120},[110,495,497,500,503,506,508,511,513,516],{"class":112,"line":496},9,[110,498,499],{"class":116},"    return",[110,501,502],{"class":124}," res",[110,504,505],{"class":147},".status",[110,507,369],{"class":120},[110,509,510],{"class":124},"400",[110,512,464],{"class":120},[110,514,515],{"class":147},".end",[110,517,518],{"class":120},"()\n",[110,520,522],{"class":112,"line":521},10,[110,523,524],{"class":120},"  }\n",[110,526,528,531,533,535,538,540,542,545],{"class":112,"line":527},11,[110,529,530],{"class":124},"  res",[110,532,505],{"class":147},[110,534,369],{"class":120},[110,536,537],{"class":124},"200",[110,539,464],{"class":120},[110,541,515],{"class":147},[110,543,544],{"class":120},"()          ",[110,546,548],{"class":547},"ssgCD","\u002F\u002F respond first, then process the event\n",[110,550,552,555,558],{"class":112,"line":551},12,[110,553,554],{"class":116},"  await",[110,556,557],{"class":147}," handle",[110,559,560],{"class":120},"(event)\n",[110,562,564],{"class":112,"line":563},13,[110,565,202],{"class":120},[99,567,570],{"className":101,"code":568,"filename":569,"language":104,"meta":105,"style":105},"import { createHmac, timingSafeEqual } from 'node:crypto'\n\nfunction verify(header, rawBody, secret) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')))\n  if (Math.abs(Date.now() \u002F 1000 - Number(parts.t)) > 300) return false\n  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')\n  return header.split(',').some((p) => {\n    const given = Buffer.from(p.trim().slice(3))\n    return p.trim().startsWith('v1=') && given.length === expected.length && timingSafeEqual(given, Buffer.from(expected))\n  })\n}\n","Node.js (any framework)",[107,571,572,589,593,614,673,733,797,822,857,914,919],{"__ignoreMap":105},[110,573,574,576,579,581,584,586],{"class":112,"line":113},[110,575,331],{"class":116},[110,577,578],{"class":120}," { createHmac",[110,580,129],{"class":128},[110,582,583],{"class":120}," timingSafeEqual } ",[110,585,337],{"class":116},[110,587,588],{"class":168}," 'node:crypto'\n",[110,590,591],{"class":112,"line":159},[110,592,358],{"emptyLinePlaceholder":357},[110,594,595,598,601,604,606,609,611],{"class":112,"line":175},[110,596,597],{"class":116},"function",[110,599,600],{"class":147}," verify",[110,602,603],{"class":120},"(header",[110,605,129],{"class":128},[110,607,608],{"class":120}," rawBody",[110,610,129],{"class":128},[110,612,613],{"class":120}," secret) {\n",[110,615,616,619,622,625,628,631,633,636,639,641,644,646,649,652,654,657,660,663,665,667,670],{"class":112,"line":199},[110,617,618],{"class":116},"  const",[110,620,621],{"class":124}," parts",[110,623,624],{"class":116}," =",[110,626,627],{"class":124}," Object",[110,629,630],{"class":147},".fromEntries",[110,632,369],{"class":120},[110,634,635],{"class":124},"header",[110,637,638],{"class":147},".split",[110,640,369],{"class":120},[110,642,643],{"class":168},"','",[110,645,464],{"class":120},[110,647,648],{"class":147},".map",[110,650,651],{"class":120},"((p) ",[110,653,407],{"class":116},[110,655,656],{"class":124}," p",[110,658,659],{"class":147},".trim",[110,661,662],{"class":120},"()",[110,664,638],{"class":147},[110,666,369],{"class":120},[110,668,669],{"class":168},"'='",[110,671,672],{"class":120},")))\n",[110,674,675,678,681,684,687,689,692,695,698,701,704,707,710,712,715,718,721,724,727,730],{"class":112,"line":413},[110,676,677],{"class":116},"  if",[110,679,680],{"class":120}," (",[110,682,683],{"class":124},"Math",[110,685,686],{"class":147},".abs",[110,688,369],{"class":120},[110,690,691],{"class":124},"Date",[110,693,694],{"class":147},".now",[110,696,697],{"class":120},"() ",[110,699,700],{"class":116},"\u002F",[110,702,703],{"class":124}," 1000",[110,705,706],{"class":116}," -",[110,708,709],{"class":147}," Number",[110,711,369],{"class":120},[110,713,714],{"class":124},"parts",[110,716,717],{"class":120},".t)) ",[110,719,720],{"class":116},">",[110,722,723],{"class":124}," 300",[110,725,726],{"class":120},") ",[110,728,729],{"class":116},"return",[110,731,732],{"class":124}," false\n",[110,734,735,737,740,742,745,747,750,752,755,758,760,763,766,768,771,774,776,778,781,783,785,787,790,792,795],{"class":112,"line":422},[110,736,618],{"class":116},[110,738,739],{"class":124}," expected",[110,741,624],{"class":116},[110,743,744],{"class":147}," createHmac",[110,746,369],{"class":120},[110,748,749],{"class":168},"'sha256'",[110,751,129],{"class":128},[110,753,754],{"class":120}," secret)",[110,756,757],{"class":147},".update",[110,759,369],{"class":120},[110,761,762],{"class":168},"`",[110,764,765],{"class":116},"${",[110,767,714],{"class":124},[110,769,770],{"class":120},".t",[110,772,773],{"class":116},"}",[110,775,148],{"class":168},[110,777,765],{"class":116},[110,779,780],{"class":120},"rawBody",[110,782,773],{"class":116},[110,784,762],{"class":168},[110,786,464],{"class":120},[110,788,789],{"class":147},".digest",[110,791,369],{"class":120},[110,793,794],{"class":168},"'hex'",[110,796,482],{"class":120},[110,798,799,802,805,807,809,811,813,816,818,820],{"class":112,"line":430},[110,800,801],{"class":116},"  return",[110,803,804],{"class":124}," header",[110,806,638],{"class":147},[110,808,369],{"class":120},[110,810,643],{"class":168},[110,812,464],{"class":120},[110,814,815],{"class":147},".some",[110,817,651],{"class":120},[110,819,407],{"class":116},[110,821,410],{"class":120},[110,823,824,827,830,832,835,838,840,842,844,846,849,851,854],{"class":112,"line":485},[110,825,826],{"class":116},"    const",[110,828,829],{"class":124}," given",[110,831,624],{"class":116},[110,833,834],{"class":124}," Buffer",[110,836,837],{"class":147},".from",[110,839,369],{"class":120},[110,841,86],{"class":124},[110,843,659],{"class":147},[110,845,662],{"class":120},[110,847,848],{"class":147},".slice",[110,850,369],{"class":120},[110,852,853],{"class":124},"3",[110,855,856],{"class":120},"))\n",[110,858,859,861,863,865,867,870,872,875,877,880,882,884,887,890,892,894,896,899,902,905,907,909,911],{"class":112,"line":496},[110,860,499],{"class":116},[110,862,656],{"class":124},[110,864,659],{"class":147},[110,866,662],{"class":120},[110,868,869],{"class":147},".startsWith",[110,871,369],{"class":120},[110,873,874],{"class":168},"'v1='",[110,876,726],{"class":120},[110,878,879],{"class":116},"&&",[110,881,829],{"class":124},[110,883,148],{"class":120},[110,885,886],{"class":124},"length",[110,888,889],{"class":116}," ===",[110,891,739],{"class":124},[110,893,148],{"class":120},[110,895,886],{"class":124},[110,897,898],{"class":116}," &&",[110,900,901],{"class":147}," timingSafeEqual",[110,903,904],{"class":120},"(given",[110,906,129],{"class":128},[110,908,834],{"class":124},[110,910,837],{"class":147},[110,912,913],{"class":120},"(expected))\n",[110,915,916],{"class":112,"line":521},[110,917,918],{"class":120},"  })\n",[110,920,921],{"class":112,"line":527},[110,922,923],{"class":120},"}\n",[86,925,926],{},"To rotate an endpoint's signing secret, call:",[99,928,930],{"className":286,"code":929,"language":288,"meta":105,"style":105},"POST \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Fsecret\u002Frotate\n",[107,931,932],{"__ignoreMap":105},[110,933,934,937],{"class":112,"line":113},[110,935,936],{"class":116},"POST",[110,938,939],{"class":120}," \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Fsecret\u002Frotate\n",[86,941,942,943,945],{},"Rotating starts a grace period, which is 24 hours by default. During the grace period, each delivery includes one ",[107,944,314],{}," signature for each secret, and either signature verifies the delivery.",[90,947,57],{"id":56},[99,949,953],{"className":950,"code":951,"language":952,"meta":105,"style":105},"language-json shiki shiki-themes css-variables","{\n  \"id\": \"evt_Q3kx9TzLmP2wVb7Rn4Ya\",\n  \"type\": \"event.updated\",\n  \"apiVersion\": \"2\",\n  \"createdAt\": \"2026-10-20T14:03:11Z\",\n  \"livemode\": true,\n  \"appId\": \"5c1e9a47-2d3b-4f86-a0e9-7b4c2d18f365\",\n  \"accountId\": \"6b0e3f1a-8c2d-4e5f-9a7b-1c3d5e7f9a2b\",\n  \"subjectId\": \"user_42\",\n  \"provider\": \"google\",\n  \"data\": {\n    \"object\": {\n      \"id\": \"6v1q8b3k2m9t0c4p7r5s\",\n      \"calendarId\": \"primary\",\n      \"seriesId\": null,\n      \"isSeries\": false\n    }\n  },\n  \"request\": { \"traceId\": \"trc_...\" }\n}\n","json",[107,954,955,960,972,984,996,1008,1020,1032,1044,1056,1068,1077,1086,1098,1111,1124,1134,1140,1148,1169],{"__ignoreMap":105},[110,956,957],{"class":112,"line":113},[110,958,959],{"class":120},"{\n",[110,961,962,965,967,970],{"class":112,"line":159},[110,963,964],{"class":116},"  \"id\"",[110,966,165],{"class":128},[110,968,969],{"class":168}," \"evt_Q3kx9TzLmP2wVb7Rn4Ya\"",[110,971,172],{"class":128},[110,973,974,977,979,982],{"class":112,"line":175},[110,975,976],{"class":116},"  \"type\"",[110,978,165],{"class":128},[110,980,981],{"class":168}," \"event.updated\"",[110,983,172],{"class":128},[110,985,986,989,991,994],{"class":112,"line":199},[110,987,988],{"class":116},"  \"apiVersion\"",[110,990,165],{"class":128},[110,992,993],{"class":168}," \"2\"",[110,995,172],{"class":128},[110,997,998,1001,1003,1006],{"class":112,"line":413},[110,999,1000],{"class":116},"  \"createdAt\"",[110,1002,165],{"class":128},[110,1004,1005],{"class":168}," \"2026-10-20T14:03:11Z\"",[110,1007,172],{"class":128},[110,1009,1010,1013,1015,1018],{"class":112,"line":422},[110,1011,1012],{"class":116},"  \"livemode\"",[110,1014,165],{"class":128},[110,1016,1017],{"class":124}," true",[110,1019,172],{"class":128},[110,1021,1022,1025,1027,1030],{"class":112,"line":430},[110,1023,1024],{"class":116},"  \"appId\"",[110,1026,165],{"class":128},[110,1028,1029],{"class":168}," \"5c1e9a47-2d3b-4f86-a0e9-7b4c2d18f365\"",[110,1031,172],{"class":128},[110,1033,1034,1037,1039,1042],{"class":112,"line":485},[110,1035,1036],{"class":116},"  \"accountId\"",[110,1038,165],{"class":128},[110,1040,1041],{"class":168}," \"6b0e3f1a-8c2d-4e5f-9a7b-1c3d5e7f9a2b\"",[110,1043,172],{"class":128},[110,1045,1046,1049,1051,1054],{"class":112,"line":496},[110,1047,1048],{"class":116},"  \"subjectId\"",[110,1050,165],{"class":128},[110,1052,1053],{"class":168}," \"user_42\"",[110,1055,172],{"class":128},[110,1057,1058,1061,1063,1066],{"class":112,"line":521},[110,1059,1060],{"class":116},"  \"provider\"",[110,1062,165],{"class":128},[110,1064,1065],{"class":168}," \"google\"",[110,1067,172],{"class":128},[110,1069,1070,1073,1075],{"class":112,"line":527},[110,1071,1072],{"class":116},"  \"data\"",[110,1074,165],{"class":128},[110,1076,410],{"class":120},[110,1078,1079,1082,1084],{"class":112,"line":551},[110,1080,1081],{"class":116},"    \"object\"",[110,1083,165],{"class":128},[110,1085,410],{"class":120},[110,1087,1088,1091,1093,1096],{"class":112,"line":563},[110,1089,1090],{"class":116},"      \"id\"",[110,1092,165],{"class":128},[110,1094,1095],{"class":168}," \"6v1q8b3k2m9t0c4p7r5s\"",[110,1097,172],{"class":128},[110,1099,1101,1104,1106,1109],{"class":112,"line":1100},14,[110,1102,1103],{"class":116},"      \"calendarId\"",[110,1105,165],{"class":128},[110,1107,1108],{"class":168}," \"primary\"",[110,1110,172],{"class":128},[110,1112,1114,1117,1119,1122],{"class":112,"line":1113},15,[110,1115,1116],{"class":116},"      \"seriesId\"",[110,1118,165],{"class":128},[110,1120,1121],{"class":124}," null",[110,1123,172],{"class":128},[110,1125,1127,1130,1132],{"class":112,"line":1126},16,[110,1128,1129],{"class":116},"      \"isSeries\"",[110,1131,165],{"class":128},[110,1133,732],{"class":124},[110,1135,1137],{"class":112,"line":1136},17,[110,1138,1139],{"class":120},"    }\n",[110,1141,1143,1146],{"class":112,"line":1142},18,[110,1144,1145],{"class":120},"  }",[110,1147,172],{"class":128},[110,1149,1151,1154,1156,1158,1161,1163,1166],{"class":112,"line":1150},19,[110,1152,1153],{"class":116},"  \"request\"",[110,1155,165],{"class":128},[110,1157,121],{"class":120},[110,1159,1160],{"class":116},"\"traceId\"",[110,1162,165],{"class":128},[110,1164,1165],{"class":168}," \"trc_...\"",[110,1167,1168],{"class":120}," }\n",[110,1170,1172],{"class":112,"line":1171},20,[110,1173,923],{"class":120},[1175,1176,1177,1211,1228],"ul",{},[1178,1179,1180,1181,1183,1184,1187,1188,1183,1191,1194,1195,1183,1198,1187,1201,1183,1204,1206,1207,148],"li",{},"For ",[107,1182,264],{},", ",[107,1185,1186],{},"calendar.*"," and ",[107,1189,1190],{},"invitation.replied",[107,1192,1193],{},"data.object"," tells you which event or calendar changed. It holds IDs only, never titles, times or attendees. Read the event or calendar to get its details. For ",[107,1196,1197],{},"connection.*",[107,1199,1200],{},"api_key.*",[107,1202,1203],{},"meeting.*",[107,1205,1193],{}," is the account, the key or the meeting itself. See ",[1208,1209,60],"a",{"href":1210},"#event-types",[1178,1212,1213,1216,1217,1220,1221,1224,1225,148],{},[107,1214,1215],{},"request.traceId"," identifies the API request that caused the change. When no API request caused it, for example a change made in a calendar app, ",[107,1218,1219],{},"request"," is ",[107,1222,1223],{},"null",", so check it before you read ",[107,1226,1227],{},"traceId",[1178,1229,1230,1233],{},[107,1231,1232],{},"id"," is the same on every retry of an event, so you can skip any id that you've already handled.",[86,1235,1180,1236,1187,1238,1183,1240,1242],{},[107,1237,264],{},[107,1239,1190],{},[107,1241,1193],{}," has:",[1244,1245,1246,1259],"table",{},[1247,1248,1249],"thead",{},[1250,1251,1252,1256],"tr",{},[1253,1254,1255],"th",{},"Field",[1253,1257,1258],{},"What it is",[1260,1261,1262,1279,1289,1301],"tbody",{},[1250,1263,1264,1269],{},[1265,1266,1267],"td",{},[107,1268,1232],{},[1265,1270,1271,1272,1220,1275,1278],{},"The event's ID. When ",[107,1273,1274],{},"isSeries",[107,1276,1277],{},"true",", the ID of the recurring series.",[1250,1280,1281,1286],{},[1265,1282,1283],{},[107,1284,1285],{},"calendarId",[1265,1287,1288],{},"The calendar the event is on.",[1250,1290,1291,1296],{},[1265,1292,1293],{},[107,1294,1295],{},"seriesId",[1265,1297,1298,1299,148],{},"For one occurrence of a recurring series that changed on its own, the series' ID. Otherwise ",[107,1300,1223],{},[1250,1302,1303,1307],{},[1265,1304,1305],{},[107,1306,1274],{},[1265,1308,1309,1311],{},[107,1310,1277],{}," when the change was to a whole recurring series.",[86,1313,1180,1314,1183,1316,1318,1319,148],{},[107,1315,1186],{},[107,1317,1193],{}," has the calendar's ",[107,1320,1232],{},[1322,1323,1325],"h3",{"id":1324},"get-the-details-of-what-changed","Get the details of what changed",[1327,1328,1329,1378,1392],"ol",{},[1178,1330,1180,1331,1334,1335,1338,1339,1342,1343,165,1345,1357,1360,1361,1220,1363,1365,1366],{},[107,1332,1333],{},"event.created"," or ",[107,1336,1337],{},"event.updated",", read the event with the delivery's ",[107,1340,1341],{},"accountId"," and the object's ",[107,1344,1285],{},[99,1346,1348],{"className":286,"code":1347,"language":288,"meta":105,"style":105},"GET \u002Fv2\u002Fevents\u002F6v1q8b3k2m9t0c4p7r5s?accountId={accountId}&calendarRef=primary\n",[107,1349,1350],{"__ignoreMap":105},[110,1351,1352,1354],{"class":112,"line":113},[110,1353,295],{"class":116},[110,1355,1356],{"class":120}," \u002Fv2\u002Fevents\u002F6v1q8b3k2m9t0c4p7r5s?accountId={accountId}&calendarRef=primary\n",[1358,1359],"br",{},"When ",[107,1362,1274],{},[107,1364,1277],{},", the whole series changed. To refresh its occurrences, list them:",[99,1367,1369],{"className":286,"code":1368,"language":288,"meta":105,"style":105},"GET \u002Fv2\u002Fevents\u002F6v1q8b3k2m9t0c4p7r5s\u002Finstances?accountId={accountId}&calendarRef=primary&from=2026-10-01T00:00:00Z&to=2026-12-31T00:00:00Z\n",[107,1370,1371],{"__ignoreMap":105},[110,1372,1373,1375],{"class":112,"line":113},[110,1374,295],{"class":116},[110,1376,1377],{"class":120}," \u002Fv2\u002Fevents\u002F6v1q8b3k2m9t0c4p7r5s\u002Finstances?accountId={accountId}&calendarRef=primary&from=2026-10-01T00:00:00Z&to=2026-12-31T00:00:00Z\n",[1178,1379,1180,1380,1383,1384,1386,1387,1220,1389,1391],{},[107,1381,1382],{},"event.deleted",", remove the event with that ",[107,1385,1232],{}," from your records. When ",[107,1388,1274],{},[107,1390,1277],{},", remove the series and its occurrences.",[1178,1393,1180,1394,1334,1397,1400,1401],{},[107,1395,1396],{},"calendar.created",[107,1398,1399],{},"calendar.updated",", read the calendar:",[99,1402,1404],{"className":286,"code":1403,"language":288,"meta":105,"style":105},"GET \u002Fv2\u002Fcalendars\u002F{calendarId}?accountId={accountId}\n",[107,1405,1406],{"__ignoreMap":105},[110,1407,1408,1410],{"class":112,"line":113},[110,1409,295],{"class":116},[110,1411,1412],{"class":120}," \u002Fv2\u002Fcalendars\u002F{calendarId}?accountId={accountId}\n",[86,1414,1415],{},"If you need to know what an event was before a change, keep your own copy of the events you sync and compare it with what you read.",[86,1417,1418,1419,1183,1422,1183,1425,1183,1428,1431,1432,1434,1435,1438],{},"Each delivery includes the ",[107,1420,1421],{},"Pyas-Event-Id",[107,1423,1424],{},"Pyas-Event-Type",[107,1426,1427],{},"Pyas-Delivery-Id",[107,1429,1430],{},"Pyas-Timestamp",", and ",[107,1433,306],{}," headers. If you set a credential on the endpoint with ",[107,1436,1437],{},"auth"," (a header or a bearer token), each delivery also includes that credential.",[90,1440,60],{"id":59},[1244,1442,1443,1453],{},[1247,1444,1445],{},[1250,1446,1447,1450],{},[1253,1448,1449],{},"Type",[1253,1451,1452],{},"When",[1260,1454,1455,1468,1481,1495,1508,1517,1536,1549,1559,1572],{},[1250,1456,1457,1465],{},[1265,1458,1459,1183,1462],{},[107,1460,1461],{},"connection.created",[107,1463,1464],{},"connection.reauthorized",[1265,1466,1467],{},"A user connected an account, or reconnected a broken connection",[1250,1469,1470,1478],{},[1265,1471,1472,1183,1475],{},[107,1473,1474],{},"connection.broken",[107,1476,1477],{},"connection.deleted",[1265,1479,1480],{},"A connected account can no longer sync, or was deleted",[1250,1482,1483,1492],{},[1265,1484,1485,1183,1487,1183,1489],{},[107,1486,1396],{},[107,1488,1399],{},[107,1490,1491],{},"calendar.deleted",[1265,1493,1494],{},"A calendar was created, updated, or deleted",[1250,1496,1497,1505],{},[1265,1498,1499,1183,1501,1183,1503],{},[107,1500,1333],{},[107,1502,1337],{},[107,1504,1382],{},[1265,1506,1507],{},"An event was created, updated, or deleted, through the API or in a calendar app",[1250,1509,1510,1514],{},[1265,1511,1512],{},[107,1513,1190],{},[1265,1515,1516],{},"Someone responded to an invitation sent from a Pyas calendar",[1250,1518,1519,1533],{},[1265,1520,1521,1183,1524,1183,1527,1183,1530],{},[107,1522,1523],{},"meeting.started",[107,1525,1526],{},"meeting.ended",[107,1528,1529],{},"meeting.updated",[107,1531,1532],{},"meeting.deleted",[1265,1534,1535],{},"A Zoom meeting started, ended, was updated, or was deleted",[1250,1537,1538,1546],{},[1265,1539,1540,1183,1543],{},[107,1541,1542],{},"meeting.participant_joined",[107,1544,1545],{},"meeting.participant_left",[1265,1547,1548],{},"Someone joined or left a meeting",[1250,1550,1551,1556],{},[1265,1552,1553],{},[107,1554,1555],{},"recording.ready",[1265,1557,1558],{},"A meeting's recording is ready to fetch",[1250,1560,1561,1569],{},[1265,1562,1563,1183,1566],{},[107,1564,1565],{},"api_key.expiring",[107,1567,1568],{},"api_key.revoked",[1265,1570,1571],{},"An API key is about to expire, or was revoked",[1250,1573,1574,1579],{},[1265,1575,1576],{},[107,1577,1578],{},"webhook.ping",[1265,1580,1581],{},"A test delivery",[86,1583,1584],{},"To get every event type with a sample payload for each, call:",[99,1586,1588],{"className":286,"code":1587,"language":288,"meta":105,"style":105},"GET \u002Fv2\u002Fwebhooks\u002Fevent-types\n",[107,1589,1590],{"__ignoreMap":105},[110,1591,1592,1594],{"class":112,"line":113},[110,1593,295],{"class":116},[110,1595,1596],{"class":120}," \u002Fv2\u002Fwebhooks\u002Fevent-types\n",[90,1598,63],{"id":62},[86,1600,1601,1602,1605],{},"A delivery succeeds when your endpoint responds with any ",[107,1603,1604],{},"2xx"," status. If your endpoint responds with any other status, or doesn't respond within 10 seconds, Pyas retries the delivery after 30 seconds, 2 minutes, 15 minutes, 1 hour, 4 hours, 12 hours, and 24 hours. That's 8 attempts over about 41 hours. Respond quickly, and process the event after you respond.",[86,1607,1608],{},"If an endpoint fails 50 deliveries in a row, Pyas disables it and marks it as disabled in the dashboard. When you enable the endpoint again, Pyas sends a test delivery first. Events are stored for 30 days.",[90,1610,66],{"id":65},[86,1612,1613],{},"To resend a single delivery, call:",[99,1615,1617],{"className":286,"code":1616,"language":288,"meta":105,"style":105},"POST \u002Fv2\u002Fwebhooks\u002Fdeliveries\u002F{deliveryId}\u002Freplay\n",[107,1618,1619],{"__ignoreMap":105},[110,1620,1621,1623],{"class":112,"line":113},[110,1622,936],{"class":116},[110,1624,1625],{"class":120}," \u002Fv2\u002Fwebhooks\u002Fdeliveries\u002F{deliveryId}\u002Freplay\n",[86,1627,1628,1629,165],{},"To recover after an outage on your side, replay the deliveries that failed since a given time. Each request replays up to 1,000 failed deliveries. Send a body such as ",[107,1630,1631],{},"{ \"since\": \"2026-10-19T00:00:00Z\" }",[99,1633,1635],{"className":286,"code":1634,"language":288,"meta":105,"style":105},"POST \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Freplay\n",[107,1636,1637],{"__ignoreMap":105},[110,1638,1639,1641],{"class":112,"line":113},[110,1640,936],{"class":116},[110,1642,1643],{"class":120}," \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Freplay\n",[86,1645,1646],{},"To list every delivery attempt, including when it was made, the status code your endpoint returned and how long it took, call:",[99,1648,1650],{"className":286,"code":1649,"language":288,"meta":105,"style":105},"GET \u002Fv2\u002Fwebhooks\u002Fdeliveries\n",[107,1651,1652],{"__ignoreMap":105},[110,1653,1654,1656],{"class":112,"line":113},[110,1655,295],{"class":116},[110,1657,1658],{"class":120}," \u002Fv2\u002Fwebhooks\u002Fdeliveries\n",[90,1660,69],{"id":68},[86,1662,1663],{},"There are three ways to test webhooks while you build.",[86,1665,1666,1670,1671,1674,1675,1678],{},[1667,1668,1669],"strong",{},"Send a test delivery."," Send a request with a body such as ",[107,1672,1673],{},"{ \"type\": \"event.updated\" }"," to the endpoint below. Pyas immediately sends a signed sample of that event type to your endpoint. Test deliveries are marked ",[107,1676,1677],{},"\"test\": true"," and aren't retried.",[99,1680,1682],{"className":286,"code":1681,"language":288,"meta":105,"style":105},"POST \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Ftest\n",[107,1683,1684],{"__ignoreMap":105},[110,1685,1686,1688],{"class":112,"line":113},[110,1687,936],{"class":116},[110,1689,1690],{"class":120}," \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Ftest\n",[86,1692,1693,1696],{},[1667,1694,1695],{},"Use the sandbox inbox."," Every sandbox includes an inbox hosted by Pyas that receives every sandbox event, so you can inspect real deliveries before your endpoint exists.",[86,1698,1699,1702],{},[1667,1700,1701],{},"Forward events to your machine."," During development, the CLI forwards your sandbox's events to your local machine, signed the same way as in production:",[99,1704,1706],{"className":205,"code":1705,"language":208,"meta":105,"style":105},"npx @pyas\u002Fcli webhooks listen --forward http:\u002F\u002Flocalhost:3000\u002Fwebhooks\u002Fpyas\n",[107,1707,1708],{"__ignoreMap":105},[110,1709,1710,1713,1716,1719,1722,1725],{"class":112,"line":113},[110,1711,1712],{"class":147},"npx",[110,1714,1715],{"class":218}," @pyas\u002Fcli",[110,1717,1718],{"class":218}," webhooks",[110,1720,1721],{"class":218}," listen",[110,1723,1724],{"class":218}," --forward",[110,1726,1727],{"class":218}," http:\u002F\u002Flocalhost:3000\u002Fwebhooks\u002Fpyas\n",[86,1729,1730],{},"The CLI prints a signing secret for the forwarding session. Use that secret to verify deliveries while you develop.",[90,1732,72],{"id":71},[86,1734,1735],{},"Endpoints registered on v1 continue to receive v1 payloads, so your integration keeps working while you migrate. v1 deliveries aren't signed, and each one is sent once. A v1 delivery that fails isn't retried and can't be replayed.",[1322,1737,1739],{"id":1738},"move-an-endpoint-to-v2","Move an endpoint to v2",[86,1741,1742],{},"You can move an endpoint in either of two ways, and both can keep the URL you already have.",[86,1744,1745,1748,1749,1752],{},[1667,1746,1747],{},"Switch the endpoint."," When your handler is ready, send the body ",[107,1750,1751],{},"{ \"payloadVersion\": \"v2\" }"," to:",[99,1754,1756],{"className":286,"code":1755,"language":288,"meta":105,"style":105},"PATCH \u002Fv2\u002Fwebhooks\u002F{webhookId}\n",[107,1757,1758],{"__ignoreMap":105},[110,1759,1760,1763],{"class":112,"line":113},[110,1761,1762],{"class":116},"PATCH",[110,1764,1765],{"class":120}," \u002Fv2\u002Fwebhooks\u002F{webhookId}\n",[86,1767,1768],{},"From then on, the endpoint receives signed, retried v2 deliveries for the event types that correspond to its v1 triggers, and it stops receiving v1 payloads. The response includes the endpoint's new signing secret.",[86,1770,1771,1774],{},[1667,1772,1773],{},"Add a v2 endpoint beside it."," Create a v2 endpoint, at the same URL or a new one, and keep the v1 endpoint until your v2 handler is ready. While both exist, each change arrives twice, once in each format, so act on only one of them. Delete the v1 endpoint when you finish. Each endpoint counts toward your plan's endpoint limit.",[1322,1776,1778],{"id":1777},"accept-both-formats-at-one-url","Accept both formats at one URL",[86,1780,1781],{},"If one URL receives both formats while you migrate, check each request before you parse it:",[1175,1783,1784,1799],{},[1178,1785,1786,1787,1789,1790,1793,1794,1798],{},"A v2 delivery has a ",[107,1788,306],{}," header, and its body has ",[107,1791,1792],{},"\"apiVersion\": \"2\"",". ",[1208,1795,1797],{"href":1796},"#verify-every-delivery","Verify the signature"," before you act on it.",[1178,1800,1801,1802,1805],{},"A v1 delivery has an ",[107,1803,1804],{},"x-pyas-webhook-event"," header that names its v1 trigger, and no signature.",[86,1807,1808],{},"In the dashboard, an endpoint's page lists its deliveries in the format the endpoint receives now. If its URL also received the other format in the last 30 days, a switch above the list shows those deliveries too. v1 deliveries show their response status, and you can't open or replay them.",[86,1810,1811,1812,148],{},"See ",[1208,1813,1815],{"href":1814},"\u002Fmigrating-from-v1","Migrating from v1",[1817,1818,1819],"style",{},"html pre.shiki code .sag7P, html code.shiki .sag7P{--shiki-default:var(--shiki-token-keyword)}html pre.shiki code .sqSWz, html code.shiki .sqSWz{--shiki-default:var(--shiki-foreground)}html pre.shiki code .sXULy, html code.shiki .sXULy{--shiki-default:var(--shiki-token-constant)}html pre.shiki code .sEEt9, html code.shiki .sEEt9{--shiki-default:var(--shiki-token-punctuation)}html pre.shiki code .s1IGz, html code.shiki .s1IGz{--shiki-default:var(--shiki-token-function)}html pre.shiki code .sk1Au, html code.shiki .sk1Au{--shiki-default:var(--shiki-token-string-expression)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sTB4W, html code.shiki .sTB4W{--shiki-default:var(--shiki-token-string)}html pre.shiki code .ssgCD, html code.shiki .ssgCD{--shiki-default:var(--shiki-token-comment)}",{"title":105,"searchDepth":175,"depth":175,"links":1821},[1822,1823,1824,1827,1828,1829,1830,1831],{"id":50,"depth":159,"text":51},{"id":53,"depth":159,"text":54},{"id":56,"depth":159,"text":57,"children":1825},[1826],{"id":1324,"depth":175,"text":1325},{"id":59,"depth":159,"text":60},{"id":62,"depth":159,"text":63},{"id":65,"depth":159,"text":66},{"id":68,"depth":159,"text":69},{"id":71,"depth":159,"text":72,"children":1832},[1833,1834],{"id":1738,"depth":175,"text":1739},{"id":1777,"depth":175,"text":1778},"md",false,{},"\u002Fwebhooks","---\ntitle: Webhooks\ndescription: Receive a signed notification for every change as it happens. Deliveries are retried for 41 hours and can be replayed, and you can test them before you write your handler.\nv1: \u002Ffundamentals\u002Fgetting-set-up\u002Fcreate-webhooks\n---\n\n# Webhooks\n\n::lead\nPyas sends an HTTPS request to your endpoint when something changes, for example when an event is created or moved in a calendar app, when a connection breaks, or when a meeting starts. Every delivery is signed, and a delivery that fails is retried for up to 41 hours. Pyas also stores each delivery so you can replay it.\n::\n\n## Add an endpoint\n\nTo receive webhooks, create an endpoint with your handler's URL and the event types you want to receive:\n\n::code-group\n```js [Node.js]\nconst { endpoint, signingSecret } = await pyas.webhooks.create({\n  url: 'https:\u002F\u002Fyourapp.com\u002Fwebhooks\u002Fpyas',\n  events: ['event.*', 'connection.broken'],\n})\n```\n\n```bash [cURL]\ncurl -X POST https:\u002F\u002Fapi.pyas.io\u002Fv2\u002Fwebhooks \\\n  -H \"x-api-key: $PYAS_API_KEY\" \\\n  -H \"content-type: application\u002Fjson\" \\\n  -d '{ \"url\": \"https:\u002F\u002Fyourapp.com\u002Fwebhooks\u002Fpyas\", \"events\": [\"event.*\", \"connection.broken\"] }'\n```\n::\n\n`events` accepts exact event types, families such as `event.*`, or `*` for every type, including types added later. The default is `*`. An endpoint created with a test key receives events from the sandbox, and an endpoint created with a live key receives your users' events. You can also manage endpoints in the dashboard.\n\nTo verify deliveries, you need the endpoint's signing secret (`whsec_...`). The response that creates the endpoint includes it as `signingSecret`. To get it again later, call:\n\n```http\nGET \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Fsecret\n```\n\n## Verify every delivery\n\nEach delivery includes a `Pyas-Signature` header in the format `t=\u003Cunix seconds>,v1=\u003Chex>`. The `v1` value is an HMAC-SHA256 of `\u003Ct>.\u003Craw body>`, computed with the endpoint's secret. Verify the raw body exactly as you received it, before you parse it. Reject signatures that are older than 5 minutes.\n\n::code-group\n```js [Node.js]\nimport express from 'express'\nimport { constructWebhookEvent } from '@pyas\u002Fsdk'\n\napp.post('\u002Fwebhooks\u002Fpyas', express.raw({ type: 'application\u002Fjson' }), async (req, res) => {\n  let event\n  try {\n    event = await constructWebhookEvent(req.body, req.header('pyas-signature'), process.env.PYAS_WEBHOOK_SECRET)\n  } catch {\n    return res.status(400).end()\n  }\n  res.status(200).end()          \u002F\u002F respond first, then process the event\n  await handle(event)\n})\n```\n\n```js [Node.js (any framework)]\nimport { createHmac, timingSafeEqual } from 'node:crypto'\n\nfunction verify(header, rawBody, secret) {\n  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')))\n  if (Math.abs(Date.now() \u002F 1000 - Number(parts.t)) > 300) return false\n  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex')\n  return header.split(',').some((p) => {\n    const given = Buffer.from(p.trim().slice(3))\n    return p.trim().startsWith('v1=') && given.length === expected.length && timingSafeEqual(given, Buffer.from(expected))\n  })\n}\n```\n::\n\nTo rotate an endpoint's signing secret, call:\n\n```http\nPOST \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Fsecret\u002Frotate\n```\n\nRotating starts a grace period, which is 24 hours by default. During the grace period, each delivery includes one `v1` signature for each secret, and either signature verifies the delivery.\n\n## What a delivery looks like\n\n```json\n{\n  \"id\": \"evt_Q3kx9TzLmP2wVb7Rn4Ya\",\n  \"type\": \"event.updated\",\n  \"apiVersion\": \"2\",\n  \"createdAt\": \"2026-10-20T14:03:11Z\",\n  \"livemode\": true,\n  \"appId\": \"5c1e9a47-2d3b-4f86-a0e9-7b4c2d18f365\",\n  \"accountId\": \"6b0e3f1a-8c2d-4e5f-9a7b-1c3d5e7f9a2b\",\n  \"subjectId\": \"user_42\",\n  \"provider\": \"google\",\n  \"data\": {\n    \"object\": {\n      \"id\": \"6v1q8b3k2m9t0c4p7r5s\",\n      \"calendarId\": \"primary\",\n      \"seriesId\": null,\n      \"isSeries\": false\n    }\n  },\n  \"request\": { \"traceId\": \"trc_...\" }\n}\n```\n\n- For `event.*`, `calendar.*` and `invitation.replied`, `data.object` tells you which event or calendar changed. It holds IDs only, never titles, times or attendees. Read the event or calendar to get its details. For `connection.*`, `api_key.*` and `meeting.*`, `data.object` is the account, the key or the meeting itself. See [Event types](#event-types).\n- `request.traceId` identifies the API request that caused the change. When no API request caused it, for example a change made in a calendar app, `request` is `null`, so check it before you read `traceId`.\n- `id` is the same on every retry of an event, so you can skip any id that you've already handled.\n\nFor `event.*` and `invitation.replied`, `data.object` has:\n\n| Field | What it is |\n|---|---|\n| `id` | The event's ID. When `isSeries` is `true`, the ID of the recurring series. |\n| `calendarId` | The calendar the event is on. |\n| `seriesId` | For one occurrence of a recurring series that changed on its own, the series' ID. Otherwise `null`. |\n| `isSeries` | `true` when the change was to a whole recurring series. |\n\nFor `calendar.*`, `data.object` has the calendar's `id`.\n\n### Get the details of what changed\n\n1. For `event.created` or `event.updated`, read the event with the delivery's `accountId` and the object's `calendarId`:\n\n   ```http\n   GET \u002Fv2\u002Fevents\u002F6v1q8b3k2m9t0c4p7r5s?accountId={accountId}&calendarRef=primary\n   ```\n\n   When `isSeries` is `true`, the whole series changed. To refresh its occurrences, list them:\n\n   ```http\n   GET \u002Fv2\u002Fevents\u002F6v1q8b3k2m9t0c4p7r5s\u002Finstances?accountId={accountId}&calendarRef=primary&from=2026-10-01T00:00:00Z&to=2026-12-31T00:00:00Z\n   ```\n\n2. For `event.deleted`, remove the event with that `id` from your records. When `isSeries` is `true`, remove the series and its occurrences.\n3. For `calendar.created` or `calendar.updated`, read the calendar:\n\n   ```http\n   GET \u002Fv2\u002Fcalendars\u002F{calendarId}?accountId={accountId}\n   ```\n\nIf you need to know what an event was before a change, keep your own copy of the events you sync and compare it with what you read.\n\nEach delivery includes the `Pyas-Event-Id`, `Pyas-Event-Type`, `Pyas-Delivery-Id`, `Pyas-Timestamp`, and `Pyas-Signature` headers. If you set a credential on the endpoint with `auth` (a header or a bearer token), each delivery also includes that credential.\n\n## Event types\n\n| Type | When |\n|---|---|\n| `connection.created`, `connection.reauthorized` | A user connected an account, or reconnected a broken connection |\n| `connection.broken`, `connection.deleted` | A connected account can no longer sync, or was deleted |\n| `calendar.created`, `calendar.updated`, `calendar.deleted` | A calendar was created, updated, or deleted |\n| `event.created`, `event.updated`, `event.deleted` | An event was created, updated, or deleted, through the API or in a calendar app |\n| `invitation.replied` | Someone responded to an invitation sent from a Pyas calendar |\n| `meeting.started`, `meeting.ended`, `meeting.updated`, `meeting.deleted` | A Zoom meeting started, ended, was updated, or was deleted |\n| `meeting.participant_joined`, `meeting.participant_left` | Someone joined or left a meeting |\n| `recording.ready` | A meeting's recording is ready to fetch |\n| `api_key.expiring`, `api_key.revoked` | An API key is about to expire, or was revoked |\n| `webhook.ping` | A test delivery |\n\nTo get every event type with a sample payload for each, call:\n\n```http\nGET \u002Fv2\u002Fwebhooks\u002Fevent-types\n```\n\n## Retries\n\nA delivery succeeds when your endpoint responds with any `2xx` status. If your endpoint responds with any other status, or doesn't respond within 10 seconds, Pyas retries the delivery after 30 seconds, 2 minutes, 15 minutes, 1 hour, 4 hours, 12 hours, and 24 hours. That's 8 attempts over about 41 hours. Respond quickly, and process the event after you respond.\n\nIf an endpoint fails 50 deliveries in a row, Pyas disables it and marks it as disabled in the dashboard. When you enable the endpoint again, Pyas sends a test delivery first. Events are stored for 30 days.\n\n## Replay\n\nTo resend a single delivery, call:\n\n```http\nPOST \u002Fv2\u002Fwebhooks\u002Fdeliveries\u002F{deliveryId}\u002Freplay\n```\n\nTo recover after an outage on your side, replay the deliveries that failed since a given time. Each request replays up to 1,000 failed deliveries. Send a body such as `{ \"since\": \"2026-10-19T00:00:00Z\" }`:\n\n```http\nPOST \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Freplay\n```\n\nTo list every delivery attempt, including when it was made, the status code your endpoint returned and how long it took, call:\n\n```http\nGET \u002Fv2\u002Fwebhooks\u002Fdeliveries\n```\n\n## Test before you build\n\nThere are three ways to test webhooks while you build.\n\n**Send a test delivery.** Send a request with a body such as `{ \"type\": \"event.updated\" }` to the endpoint below. Pyas immediately sends a signed sample of that event type to your endpoint. Test deliveries are marked `\"test\": true` and aren't retried.\n\n```http\nPOST \u002Fv2\u002Fwebhooks\u002F{webhookId}\u002Ftest\n```\n\n**Use the sandbox inbox.** Every sandbox includes an inbox hosted by Pyas that receives every sandbox event, so you can inspect real deliveries before your endpoint exists.\n\n**Forward events to your machine.** During development, the CLI forwards your sandbox's events to your local machine, signed the same way as in production:\n\n```bash\nnpx @pyas\u002Fcli webhooks listen --forward http:\u002F\u002Flocalhost:3000\u002Fwebhooks\u002Fpyas\n```\n\nThe CLI prints a signing secret for the forwarding session. Use that secret to verify deliveries while you develop.\n\n## Endpoints on v1 payloads\n\nEndpoints registered on v1 continue to receive v1 payloads, so your integration keeps working while you migrate. v1 deliveries aren't signed, and each one is sent once. A v1 delivery that fails isn't retried and can't be replayed.\n\n### Move an endpoint to v2\n\nYou can move an endpoint in either of two ways, and both can keep the URL you already have.\n\n**Switch the endpoint.** When your handler is ready, send the body `{ \"payloadVersion\": \"v2\" }` to:\n\n```http\nPATCH \u002Fv2\u002Fwebhooks\u002F{webhookId}\n```\n\nFrom then on, the endpoint receives signed, retried v2 deliveries for the event types that correspond to its v1 triggers, and it stops receiving v1 payloads. The response includes the endpoint's new signing secret.\n\n**Add a v2 endpoint beside it.** Create a v2 endpoint, at the same URL or a new one, and keep the v1 endpoint until your v2 handler is ready. While both exist, each change arrives twice, once in each format, so act on only one of them. Delete the v1 endpoint when you finish. Each endpoint counts toward your plan's endpoint limit.\n\n### Accept both formats at one URL\n\nIf one URL receives both formats while you migrate, check each request before you parse it:\n\n- A v2 delivery has a `Pyas-Signature` header, and its body has `\"apiVersion\": \"2\"`. [Verify the signature](#verify-every-delivery) before you act on it.\n- A v1 delivery has an `x-pyas-webhook-event` header that names its v1 trigger, and no signature.\n\nIn the dashboard, an endpoint's page lists its deliveries in the format the endpoint receives now. If its URL also received the other format in the last 30 days, a switch above the list shows those deliveries too. v1 deliveries show their response status, and you can't open or replay them.\n\nSee [Migrating from v1](\u002Fmigrating-from-v1).\n",null,{"title":28,"description":47},"\u002Ffundamentals\u002Fgetting-set-up\u002Fcreate-webhooks","-mL9ErYo6nLMXeaKNTKxjZRbZ_xnBDxipP_NWmxvUMQ",1794697271301]