Webhooks API
Receive real-time HTTP notifications when events occur in LinkTime.
Events
Subscribe to any of the following events when creating a webhook:
| Event | Description |
|---|---|
booking.created | A new booking has been confirmed |
booking.cancelled | A booking has been cancelled |
booking.rescheduled | A booking has been moved to a new time |
payment.received | Payment completed for a paid booking |
contact.created | A new contact has been added |
Payload Structure
All webhook payloads follow this structure:
{
"event": "booking.created",
"timestamp": "2026-02-07T14:30:00.000Z",
"data": {
// Event-specific data (see below)
}
}HTTP Headers
Each webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-LinkTime-Signature | HMAC-SHA256 signature for verification |
X-LinkTime-Event | The event type (e.g., booking.created) |
X-LinkTime-Timestamp | ISO 8601 timestamp of the event |
User-Agent | LinkTime-Webhook/1.0 |
Event Payloads
booking.created
{
"event": "booking.created",
"timestamp": "2026-02-07T14:30:00.000Z",
"data": {
"id": "clx1234567890",
"eventType": {
"id": "clx0987654321",
"name": "30 Minute Meeting",
"slug": "30-min",
"duration": 30
},
"startTime": "2026-02-08T10:00:00.000Z",
"endTime": "2026-02-08T10:30:00.000Z",
"status": "CONFIRMED",
"invitee": {
"name": "John Smith",
"email": "[email protected]",
"timezone": "America/New_York"
},
"host": {
"id": "user_abc123",
"name": "Jane Doe",
"email": "[email protected]"
},
"meetingLink": "https://meet.google.com/abc-defg-hij",
"location": "Google Meet",
"notes": "Looking forward to our call!",
"isRecurring": false,
"recurringGroupId": null
}
}booking.cancelled
Same as booking.created, plus:
{
"data": {
// ... all booking fields ...
"status": "CANCELLED",
"cancelReason": "Schedule conflict"
}
}booking.rescheduled
Same as booking.created, plus previous times:
{
"data": {
// ... all booking fields with NEW times ...
"startTime": "2026-02-09T14:00:00.000Z",
"endTime": "2026-02-09T14:30:00.000Z",
"previousStartTime": "2026-02-08T10:00:00.000Z",
"previousEndTime": "2026-02-08T10:30:00.000Z"
}
}payment.received
{
"event": "payment.received",
"timestamp": "2026-02-07T14:30:00.000Z",
"data": {
"bookingId": "clx1234567890",
"amount": 50.00,
"currency": "USD",
"paymentMethod": "stripe",
"transactionId": "pi_3ABC123DEF456"
}
}contact.created
{
"event": "contact.created",
"timestamp": "2026-02-07T14:30:00.000Z",
"data": {
"id": "clx1234567890",
"name": "John Smith",
"email": "[email protected]",
"phone": "+14155551234",
"company": "Acme Inc",
"notes": null,
"createdAt": "2026-02-07T14:30:00.000Z",
"source": "booking"
}
}Signature Verification
Each webhook includes an HMAC-SHA256 signature in the X-LinkTime-Signature header. Always verify this signature to ensure the webhook came from LinkTime.
Security: Never process webhooks without verifying the signature. Your webhook secret is shown once when you create the webhook - store it securely.
Verification Steps
- Get the raw request body (as a string, not parsed JSON)
- Compute HMAC-SHA256 of the body using your webhook secret
- Compare with the signature header (use timing-safe comparison)
Example: Node.js
import crypto from 'crypto';
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your webhook handler:
app.post('/webhooks/linktime', (req, res) => {
const signature = req.headers['x-linktime-signature'];
const isValid = verifyWebhook(req.rawBody, signature, process.env.LINKTIME_WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = req.body;
// Process the webhook...
res.status(200).send('OK');
});Example: Python
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# In your Flask handler:
@app.route('/webhooks/linktime', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-LinkTime-Signature')
is_valid = verify_webhook(request.data, signature, WEBHOOK_SECRET)
if not is_valid:
return 'Invalid signature', 401
event = request.json
# Process the webhook...
return 'OK', 200Retry Behavior
LinkTime automatically retries failed webhook deliveries to ensure reliability. We use a two-phase retry strategy:
Phase 1: Immediate Retries
During the initial request:
- • Attempt 1: Immediate
- • Attempt 2: After 10 seconds
- • Attempt 3: After 60 seconds
Phase 2: Background Retries
Via scheduled job:
- • Attempt 4: After 1 hour
- • Attempt 5: After 4 hours
- • Attempt 6: After 24 hours
What Gets Retried
| Response | Retried? | Reason |
|---|---|---|
| 2xx Success | No | Delivery successful |
| 4xx Client Error | No | Configuration issue - fix your endpoint |
| 429 Rate Limited | Yes | Temporary - will retry later |
| 5xx Server Error | Yes | Temporary - will retry later |
| Timeout (30s) | Yes | Endpoint slow - will retry later |
| Network Error | Yes | DNS/connection issue - will retry later |
Tip: Return a 2xx status code as quickly as possible. Process the webhook asynchronously if needed. This prevents timeouts and unnecessary retries.
Best Practices
Always verify signatures
Never trust webhooks without verifying the HMAC signature.
Return 200 quickly
Acknowledge receipt immediately, then process asynchronously. We timeout after 30 seconds.
Handle duplicates
Due to retries, you may receive the same webhook multiple times. Use the booking ID to deduplicate.
Use HTTPS
Always use HTTPS endpoints. We do not send webhooks to HTTP URLs in production.
Monitor delivery status
Check the webhook delivery history in Settings → Webhooks to debug issues.