cURL
curl --request POST \
--url https://api.novacal.io/v1/webhooks \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://example.com/hooks/novacal",
"events": [
"event.created",
"event.canceled"
],
"is_active": true
}
'import requests
url = "https://api.novacal.io/v1/webhooks"
payload = {
"url": "https://example.com/hooks/novacal",
"events": ["event.created", "event.canceled"],
"is_active": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: 'https://example.com/hooks/novacal',
events: ['event.created', 'event.canceled'],
is_active: true
})
};
fetch('https://api.novacal.io/v1/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.novacal.io/v1/webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/hooks/novacal',
'events' => [
'event.created',
'event.canceled'
],
'is_active' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.novacal.io/v1/webhooks"
payload := strings.NewReader("{\n \"url\": \"https://example.com/hooks/novacal\",\n \"events\": [\n \"event.created\",\n \"event.canceled\"\n ],\n \"is_active\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.novacal.io/v1/webhooks")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://example.com/hooks/novacal\",\n \"events\": [\n \"event.created\",\n \"event.canceled\"\n ],\n \"is_active\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.novacal.io/v1/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://example.com/hooks/novacal\",\n \"events\": [\n \"event.created\",\n \"event.canceled\"\n ],\n \"is_active\": true\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"id": 1,
"url": "https://example.com/hooks/novacal",
"events": [
"event.created",
"event.canceled"
],
"secret": "novacal_wh_YOUR_SIGNING_SECRET",
"is_active": true,
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z"
}
}Webhooks
Create Webhook
Register a Novacal webhook to receive real-time notifications when events are booked, rescheduled, or canceled, with a signing secret to verify deliveries.
POST
/
v1
/
webhooks
cURL
curl --request POST \
--url https://api.novacal.io/v1/webhooks \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://example.com/hooks/novacal",
"events": [
"event.created",
"event.canceled"
],
"is_active": true
}
'import requests
url = "https://api.novacal.io/v1/webhooks"
payload = {
"url": "https://example.com/hooks/novacal",
"events": ["event.created", "event.canceled"],
"is_active": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: 'https://example.com/hooks/novacal',
events: ['event.created', 'event.canceled'],
is_active: true
})
};
fetch('https://api.novacal.io/v1/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.novacal.io/v1/webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://example.com/hooks/novacal',
'events' => [
'event.created',
'event.canceled'
],
'is_active' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.novacal.io/v1/webhooks"
payload := strings.NewReader("{\n \"url\": \"https://example.com/hooks/novacal\",\n \"events\": [\n \"event.created\",\n \"event.canceled\"\n ],\n \"is_active\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.novacal.io/v1/webhooks")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://example.com/hooks/novacal\",\n \"events\": [\n \"event.created\",\n \"event.canceled\"\n ],\n \"is_active\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.novacal.io/v1/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://example.com/hooks/novacal\",\n \"events\": [\n \"event.created\",\n \"event.canceled\"\n ],\n \"is_active\": true\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"id": 1,
"url": "https://example.com/hooks/novacal",
"events": [
"event.created",
"event.canceled"
],
"secret": "novacal_wh_YOUR_SIGNING_SECRET",
"is_active": true,
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z"
}
}What this endpoint is for
Register a URL that Novacal calls whenever something happens to an event you host or organize. Webhooks let you react to bookings in real time instead of polling the events endpoint.Available events
| Event | Sent when |
|---|---|
event.created | A new event is booked |
event.rescheduled | An existing event moves to a different time |
event.canceled | An event is canceled |
event.started | An event reaches its start time |
event.ended | An event reaches its end time |
event.started and event.ended are driven by the clock rather than by an action someone takes, so they are checked once a minute. Expect a delivery within a minute of the event’s start or end time, not at the exact second. Canceled events never fire either one, and each fires at most once per event.Delivery format
Novacal sends aPOST request with a JSON body:
{
"event": "event.created",
"created_at": "2026-07-14T10:32:11+00:00",
"data": {
"id": "9b1f...",
"event_type_id": 42,
"start": "2026-07-20T09:00:00.000000Z",
"end": "2026-07-20T09:30:00.000000Z",
"status": "scheduled",
"organizer": {},
"booker": {},
"guests": []
}
}
data object is the same event payload returned by the Get Event endpoint.
Verifying deliveries
Every delivery carries two headers:| Header | Description |
|---|---|
X-Novacal-Signature | HMAC SHA-256 of the raw request body, keyed with your webhook secret |
X-Novacal-Event | The event name, for example event.created |
import crypto from "crypto";
const expected = crypto
.createHmac("sha256", process.env.NOVACAL_WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
const valid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(request.headers["x-novacal-signature"])
);
Retries
Respond with a2xx status code to acknowledge a delivery. Novacal retries a failed delivery up to three times with a short backoff, so your endpoint should be idempotent.Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
application/json
Webhook payload
The HTTPS URL that Novacal sends deliveries to
Example:
"https://example.com/hooks/novacal"
The events to subscribe to. At least one is required.
Minimum array length:
1Available options:
event.created, event.rescheduled, event.canceled, event.started, event.ended Example:
["event.created", "event.canceled"]
Whether the webhook starts active. Defaults to true.
Example:
true