
Organization-scoped, HMAC-signed events with stable IDs, exponential retries, delivery logs, and manual replay.
All events are live. Stable IDs across retries and manual replay.
| Event type | When it fires |
|---|---|
| professional.created | A provider is added to the network. |
| professional.updated | Provider profile or eligibility data changes. |
| provider_import.completed | A bulk import job finishes. |
| resolution.resolved | Resolution returns one or more options. |
| resolution.zero_results | No provider met all constraints. |
| resolution.partial | Some constraints produced unknown data. |
| option.held | An option is held pending confirmation. |
| option.released | A hold expires or is explicitly released. |
| booking.confirmed | A booking is atomically confirmed. |
| booking.rescheduled | A booking is moved to a new time. |
| booking.cancelled | A booking is cancelled. |
| booking.completed | A booking transitions to completed state. |
| calendar.connection_degraded | A calendar connection begins failing. |
| calendar.reconnect_required | OAuth token expired or access revoked. |
Every event shares this structure. The id is stable across retries and manual replay.
{
"id": "evt_01KXYZ...",
"type": "booking.confirmed",
"apiVersion": "2026-08-01",
"createdAt": "2026-08-01T14:02:04Z",
"organizationId": "org_01K...",
"attempt": 1,
"data": {
"bookingId": "bkg_01K...",
"resolutionId": "res_01K...",
"providerId": "pro_01K...",
"slot": {
"start": "2026-08-05T15:00:00-04:00",
"end": "2026-08-05T15:50:00-04:00",
"timezone": "America/New_York"
}
}
}Orita signs every delivery with HMAC-SHA256. Verify using the raw request body, X-Orita-Signature, and X-Orita-Timestamp headers.
Always reject events with a timestamp older than 5 minutes to prevent replay attacks.
Node.js
import { createHmac, timingSafeEqual } from "crypto";
export function verifyOritaWebhook(
rawBody: Buffer,
signature: string, // X-Orita-Signature header
timestamp: string, // X-Orita-Timestamp header
secret: string,
): boolean {
// Reject events older than 5 minutes
const age = Date.now() - Number(timestamp) * 1000;
if (age > 300_000) return false;
const payload = `${timestamp}.${rawBody.toString("utf8")}`;
const expected = createHmac("sha256", secret)
.update(payload)
.digest("hex");
return timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
);
}Python
import hmac, hashlib, time
def verify_orita_webhook(
raw_body: bytes,
signature: str, # X-Orita-Signature header
timestamp: str, # X-Orita-Timestamp header
secret: str,
) -> bool:
age = time.time() - int(timestamp)
if age > 300:
return False
payload = f"{timestamp}.{raw_body.decode('utf-8')}"
expected = "sha256=" + hmac.new(
secret.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)Orita retries failed deliveries with exponential backoff. The event ID is stable across all attempts.
| Attempt | Delay | Notes |
|---|---|---|
| 1 | Immediate | First delivery attempt after event creation |
| 2 | ~1 min | Exponential backoff begins |
| 3 | ~5 min | |
| 4 | ~30 min | |
| 5 | ~2 hours | Final automatic attempt |
| — | Dead-letter | Manual replay available via API or dashboard |
id is unchanged across retries and manual replay. Consumers must store processed event IDs and return a successful 2xxresponse for duplicates. Processing a duplicate event twice causes your system to diverge — not Orita's.Replay any delivery from the dashboard or API.
# Replay a specific delivery
curl -X POST https://orita.online/api/v2/webhooks/{deliveryId}/replay \
-H "Authorization: Bearer orita_***"
# → { deliveryId, status: "pending", attempt: 2 }