Orita fires webhooks when booking status changes. A well-built consumer handles them reliably: it verifies signatures before processing, uses stable event IDs for deduplication, handles retries without duplicate side effects, and can replay missed events on demand.
This guide covers all of it, with working code in JavaScript and Python.
The Five Event Types
Orita fires five booking lifecycle events:
| Event | When it fires |
|---|---|
| booking.created | A booking has been confirmed for the first time |
| booking.rescheduled | An existing booking's time or provider has changed |
| booking.cancelled | A booking has been cancelled by either party |
| booking.completed | A session has been marked as completed |
| booking.no_show | A session was not attended by the client |
Each event is delivered with the same structure:
{
"eventId": "evt_8f2a1b3c",
"eventType": "booking.created",
"occurredAt": "2026-07-29T15:04:23Z",
"data": {
"bookingId": "bk_18382",
"providerId": "prov_9a21b",
"serviceId": "svc_therapy_60min",
"status": "confirmed",
"slot": {
"start": "2026-08-06T15:00:00Z",
"end": "2026-08-06T16:00:00Z"
},
"client": {
"name": "Ana LΓ³pez",
"email": "ana@example.com"
}
}
}
Signature Verification
Every webhook delivery includes an X-Orita-Signature header. It's an HMAC-SHA256 signature of the raw request body, signed with your webhook secret.
Always verify the signature before processing the payload. An unverified webhook is an open endpoint.
JavaScript (Node.js)
import crypto from "crypto";
const WEBHOOK_SECRET = process.env.ORITA_WEBHOOK_SECRET;
/**
* Verify an Orita webhook signature.
* Call this before touching req.body.
*/
function verifyOritaWebhook(rawBody, signatureHeader) {
if (!signatureHeader) {
throw new Error("Missing X-Orita-Signature header");
}
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
const received = signatureHeader.replace(/^sha256=/, "");
// Use timingSafeEqual to prevent timing attacks
const expectedBuf = Buffer.from(expected, "hex");
const receivedBuf = Buffer.from(received, "hex");
if (expectedBuf.length !== receivedBuf.length) {
throw new Error("Webhook signature mismatch");
}
if (!crypto.timingSafeEqual(expectedBuf, receivedBuf)) {
throw new Error("Webhook signature mismatch");
}
}
// Express handler example
app.post("/webhooks/orita", express.raw({ type: "application/json" }), (req, res) => {
try {
verifyOritaWebhook(req.body, req.headers["x-orita-signature"]);
} catch (err) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body.toString());
handleOritaEvent(event);
res.status(200).json({ received: true });
});
Important: Use express.raw() or equivalent middleware to get the unparsed body. HMAC is computed over the exact bytes Orita sends β if your framework parses and re-serializes the JSON, the signature will not match.
Python (Flask)
import hashlib
import hmac
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["ORITA_WEBHOOK_SECRET"].encode()
def verify_orita_webhook(raw_body: bytes, signature_header: str) -> None:
"""
Verify X-Orita-Signature header.
Raises ValueError on mismatch.
"""
if not signature_header:
raise ValueError("Missing X-Orita-Signature header")
received = signature_header.replace("sha256=", "")
expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, received):
raise ValueError("Webhook signature mismatch")
@app.route("/webhooks/orita", methods=["POST"])
def handle_webhook():
raw_body = request.get_data()
signature = request.headers.get("X-Orita-Signature", "")
try:
verify_orita_webhook(raw_body, signature)
except ValueError as e:
return jsonify({"error": str(e)}), 401
import json
event = json.loads(raw_body)
process_orita_event(event)
return jsonify({"received": True}), 200
Retry Schedule
If your webhook endpoint doesn't return a 2xx response, Orita retries with exponential backoff:
| Attempt | Delay after previous | |---|---| | 1 (initial) | β | | 2 | 30 seconds | | 3 | 2 minutes | | 4 | 10 minutes | | 5 | 30 minutes | | 6 (final) | 2 hours |
After 6 failed attempts, the delivery is marked as failed. You can replay it manually.
The eventId is stable across all retries β the same event will always carry the same eventId regardless of which delivery attempt succeeds.
Idempotent Event Handling
Because Orita retries on failure, your handler may receive the same event more than once. Build your consumer to handle duplicates safely by deduplicating on eventId.
JavaScript: Idempotent handler with deduplication
// In-memory store for illustration β use a database in production
const processedEvents = new Set();
async function handleOritaEvent(event) {
const { eventId, eventType, data } = event;
// Deduplicate: if we've already processed this event, skip it
if (processedEvents.has(eventId)) {
console.log(`Duplicate event ${eventId} (${eventType}) β skipping`);
return;
}
// Process the event
switch (eventType) {
case "booking.created":
await onBookingCreated(data);
break;
case "booking.rescheduled":
await onBookingRescheduled(data);
break;
case "booking.cancelled":
await onBookingCancelled(data);
break;
case "booking.completed":
await onBookingCompleted(data);
break;
case "booking.no_show":
await onBookingNoShow(data);
break;
default:
console.warn(`Unknown event type: ${eventType}`);
}
// Mark as processed only after successful handling
processedEvents.add(eventId);
}
async function onBookingCreated(data) {
console.log(`New booking ${data.bookingId} confirmed for ${data.client.name}`);
// β send confirmation email, update CRM, notify provider, etc.
}
async function onBookingRescheduled(data) {
console.log(`Booking ${data.bookingId} rescheduled to ${data.slot.start}`);
// β update calendar, send rescheduling notification
}
async function onBookingCancelled(data) {
console.log(`Booking ${data.bookingId} cancelled`);
// β release slot, send cancellation notice, trigger waitlist logic
}
async function onBookingCompleted(data) {
console.log(`Session ${data.bookingId} completed`);
// β send follow-up, log to analytics, trigger next booking flow
}
async function onBookingNoShow(data) {
console.log(`Client no-show for ${data.bookingId}`);
// β mark no-show, apply no-show policy, notify provider
}
Python: Database-backed deduplication
import json
import os
from flask import Flask, request, jsonify
from sqlalchemy import create_engine, text
app = Flask(__name__)
engine = create_engine(os.environ["DATABASE_URL"])
WEBHOOK_SECRET = os.environ["ORITA_WEBHOOK_SECRET"].encode()
def is_already_processed(event_id: str) -> bool:
with engine.connect() as conn:
result = conn.execute(
text("SELECT 1 FROM processed_events WHERE event_id = :id"),
{"id": event_id},
)
return result.fetchone() is not None
def mark_as_processed(event_id: str, event_type: str) -> None:
with engine.begin() as conn:
conn.execute(
text(
"INSERT INTO processed_events (event_id, event_type, processed_at) "
"VALUES (:id, :type, NOW()) ON CONFLICT DO NOTHING"
),
{"id": event_id, "type": event_type},
)
def process_orita_event(event: dict) -> None:
event_id = event["eventId"]
event_type = event["eventType"]
data = event["data"]
if is_already_processed(event_id):
print(f"Duplicate event {event_id} ({event_type}) β skipping")
return
handlers = {
"booking.created": on_booking_created,
"booking.rescheduled": on_booking_rescheduled,
"booking.cancelled": on_booking_cancelled,
"booking.completed": on_booking_completed,
"booking.no_show": on_booking_no_show,
}
handler = handlers.get(event_type)
if handler:
handler(data)
else:
print(f"Unknown event type: {event_type}")
mark_as_processed(event_id, event_type)
def on_booking_created(data):
print(f"New booking {data['bookingId']} confirmed for {data['client']['name']}")
def on_booking_rescheduled(data):
print(f"Booking {data['bookingId']} rescheduled to {data['slot']['start']}")
def on_booking_cancelled(data):
print(f"Booking {data['bookingId']} cancelled")
def on_booking_completed(data):
print(f"Session {data['bookingId']} completed")
def on_booking_no_show(data):
print(f"No-show for booking {data['bookingId']}")
Manual Replay for Missed Events
If your endpoint was down during a delivery window and all retry attempts failed, you can replay any delivery:
POST /api/v1/webhooks/deliveries/{deliveryId}/replay
Authorization: Bearer <your_api_key>
// Replay a specific delivery
const response = await fetch(
`https://api.orita.online/api/v1/webhooks/deliveries/${deliveryId}/replay`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ORITA_API_KEY}`,
},
}
);
const result = await response.json();
console.log(result);
// { "status": "requeued", "deliveryId": "dlv_8f2a1b" }
The replayed delivery carries the same eventId as the original. Your idempotency logic will handle it correctly β if you already processed the event somehow, it'll be a no-op. If you didn't, it'll be processed now.
Delivery Log
You can inspect recent webhook deliveries and their status:
GET /api/v1/webhooks/deliveries?eventType=booking.cancelled&status=failed
This is useful for auditing failed deliveries, identifying patterns in failures, and queuing replays.
Deployment Considerations
Return 200 fast, process async. Orita's delivery timeout is short. If your processing logic is slow (database writes, downstream API calls), acknowledge the webhook immediately and process in a background job.
app.post("/webhooks/orita", express.raw({ type: "application/json" }), async (req, res) => {
try {
verifyOritaWebhook(req.body, req.headers["x-orita-signature"]);
} catch {
return res.status(401).json({ error: "Invalid signature" });
}
// Acknowledge immediately
res.status(200).json({ received: true });
// Process in background
const event = JSON.parse(req.body.toString());
setImmediate(() => handleOritaEvent(event).catch(console.error));
});
Don't skip signature verification in development. Use a test webhook secret locally β but keep the verification code path active. It's easy to comment out for development and forget to uncomment in production.
Use a database for processed event tracking. In-memory deduplication (like the Set example above) doesn't survive restarts or work across multiple instances. Use a database table with a unique index on event_id.
Summary
A production-grade Orita webhook consumer needs four things:
- Signature verification β HMAC-SHA256 on the raw body before any processing
- Idempotency β deduplication on
eventIdso retries are safe - Fast acknowledgment β return 200 immediately, process async
- Replay capability β
/deliveries/:id/replayfor missed events
The retry schedule (30s β 2min β 10min β 30min β 2h) gives your endpoint multiple recovery windows. With idempotency on eventId, retries are safe to receive. With manual replay, no event is permanently lost.
Read the full Webhooks reference β Β· Configure your webhook endpoint β
