Skip to main content

Reservation System Integrations

Connect with reservation platforms for seamless table management.

Overview

Olympus Cloud integrates with major reservation systems to sync bookings, manage guest data, and coordinate table availability:

PlatformFeaturesSync TypeAPI
OpenTableReservations, Guests, AvailabilityReal-timeREST API
ResyReservations, Guests, WaitlistReal-timeREST API
Yelp ReservationsReservations, ReviewsReal-timeFusion API
Custom WebhookAny platformWebhookCustom

Architecture

┌─────────────────────────────────────────────────────────────────┐
│ Reservation Platforms │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ OpenTable│ │ Resy │ │ Yelp │ │ Custom │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└───────┼─────────────┼─────────────┼─────────────┼───────────────┘
│ │ │ │
└─────────────┴──────┬──────┴─────────────┘

┌──────────────▼───────────────┐
│ Reservation Integration Hub │
│ • Booking normalization │
│ • Guest profile merge │
│ • Availability sync │
└──────────────┬───────────────┘

┌──────────────▼───────────────┐
│ Table Management │
│ • Floor plan mapping │
│ • Table assignment │
│ • Turn time tracking │
└──────────────────────────────┘

OpenTable Integration

Setup

# config/integrations/opentable.yaml
opentable:
restaurant_id: "your-restaurant-id"
client_id: "your-client-id"
client_secret: "your-client-secret"
api_key: "your-api-key"
webhook_url: "https://api.olympuscloud.ai/webhooks/opentable"

Authentication

// src/integrations/opentable/auth.rs
pub struct OpenTableAuth {
client_id: String,
client_secret: String,
}

impl OpenTableAuth {
pub async fn get_access_token(&self) -> Result<String> {
let client = reqwest::Client::new();

let response = client
.post("https://oauth.opentable.com/api/v2/oauth/token")
.form(&[
("grant_type", "client_credentials"),
("client_id", &self.client_id),
("client_secret", &self.client_secret),
])
.send()
.await?;

let token_response: TokenResponse = response.json().await?;
Ok(token_response.access_token)
}
}

Fetch Reservations

// src/integrations/opentable/reservations.rs
pub async fn get_reservations(
ctx: &TenantContext,
date: NaiveDate,
) -> Result<Vec<Reservation>> {
let token = opentable_auth.get_access_token().await?;
let restaurant_id = get_opentable_restaurant_id(ctx).await?;
let client = reqwest::Client::new();

let response = client
.get(&format!(
"https://platform.opentable.com/sync/v2/reservations"
))
.query(&[
("rid", &restaurant_id),
("date", &date.format("%Y-%m-%d").to_string()),
])
.header("Authorization", format!("Bearer {}", token))
.send()
.await?;

let ot_reservations: OpenTableReservations = response.json().await?;

// Normalize to Olympus format
let reservations = ot_reservations.items.into_iter().map(|r| {
Reservation {
id: Uuid::new_v4().to_string(),
external_id: Some(r.confirmation_number),
source: ReservationSource::OpenTable,
guest: Guest {
name: format!("{} {}", r.first_name, r.last_name),
phone: r.phone,
email: r.email,
},
party_size: r.party_size,
date: date,
time: r.time,
table_id: None, // Assign later
status: map_status(&r.status),
notes: r.special_requests,
created_at: r.created_at,
}
}).collect();

Ok(reservations)
}

Update Availability

pub async fn update_availability(
ctx: &TenantContext,
date: NaiveDate,
time_slots: &[TimeSlot],
) -> Result<()> {
let token = opentable_auth.get_access_token().await?;
let restaurant_id = get_opentable_restaurant_id(ctx).await?;
let client = reqwest::Client::new();

let availability = time_slots.iter().map(|slot| {
OpenTableAvailability {
time: slot.time.format("%H:%M").to_string(),
party_sizes: slot.available_party_sizes.clone(),
status: if slot.available { "open" } else { "closed" },
}
}).collect::<Vec<_>>();

client
.put(&format!(
"https://platform.opentable.com/sync/v2/availability"
))
.query(&[
("rid", &restaurant_id),
("date", &date.format("%Y-%m-%d").to_string()),
])
.header("Authorization", format!("Bearer {}", token))
.json(&availability)
.send()
.await?;

Ok(())
}

Webhook Handler

// src/integrations/opentable/webhooks.rs
pub async fn handle_opentable_webhook(
payload: &str,
signature: &str,
) -> Result<()> {
// Verify signature
verify_webhook_signature(payload, signature, &WEBHOOK_SECRET)?;

let event: OpenTableEvent = serde_json::from_str(payload)?;

match event.event_type.as_str() {
"reservation.created" => {
let reservation = normalize_reservation(&event.data);
reservation_service.create(&reservation).await?;

// Notify host station
notification_service.notify_new_reservation(&reservation).await?;
}
"reservation.modified" => {
let reservation = normalize_reservation(&event.data);
reservation_service.update(&reservation).await?;
}
"reservation.cancelled" => {
let confirmation_number = event.data.confirmation_number;
reservation_service.cancel_by_external_id(
ReservationSource::OpenTable,
&confirmation_number
).await?;
}
_ => {}
}

Ok(())
}

Resy Integration

Setup

# config/integrations/resy.yaml
resy:
venue_id: "your-venue-id"
api_key: "your-api-key"
secret_key: "your-secret-key"
webhook_url: "https://api.olympuscloud.ai/webhooks/resy"

Authentication

// src/integrations/resy/auth.rs
pub fn create_auth_header(api_key: &str) -> String {
format!("ResyAPI api_key=\"{}\"", api_key)
}

pub async fn get_venue_details(
api_key: &str,
venue_id: &str,
) -> Result<ResyVenue> {
let client = reqwest::Client::new();

let response = client
.get(&format!(
"https://api.resy.com/3/venue/{}",
venue_id
))
.header("Authorization", create_auth_header(api_key))
.send()
.await?;

response.json().await
}

Fetch Reservations

// src/integrations/resy/reservations.rs
pub async fn get_reservations(
ctx: &TenantContext,
date: NaiveDate,
) -> Result<Vec<Reservation>> {
let config = get_resy_config(ctx).await?;
let client = reqwest::Client::new();

let response = client
.get("https://api.resy.com/3/venue/reservations")
.query(&[
("venue_id", &config.venue_id),
("date", &date.format("%Y-%m-%d").to_string()),
])
.header("Authorization", create_auth_header(&config.api_key))
.send()
.await?;

let resy_reservations: ResyReservations = response.json().await?;

let reservations = resy_reservations.reservations.into_iter().map(|r| {
Reservation {
id: Uuid::new_v4().to_string(),
external_id: Some(r.resy_token),
source: ReservationSource::Resy,
guest: Guest {
name: r.guest_name,
phone: r.phone_number,
email: r.email,
},
party_size: r.num_seats,
date: date,
time: r.slot.time,
table_id: r.table_id,
status: map_resy_status(&r.status),
notes: r.special_requests,
tags: r.tags,
created_at: r.created_at,
}
}).collect();

Ok(reservations)
}

Update Inventory

pub async fn update_slot_inventory(
ctx: &TenantContext,
date: NaiveDate,
slots: &[SlotInventory],
) -> Result<()> {
let config = get_resy_config(ctx).await?;
let client = reqwest::Client::new();

for slot in slots {
client
.patch("https://api.resy.com/3/venue/inventory")
.header("Authorization", create_auth_header(&config.api_key))
.json(&json!({
"venue_id": config.venue_id,
"date": date.format("%Y-%m-%d").to_string(),
"slot_id": slot.slot_id,
"quantity": slot.quantity,
"status": if slot.available { "open" } else { "closed" }
}))
.send()
.await?;
}

Ok(())
}

Guest Profile Sync

pub async fn sync_guest_profile(
ctx: &TenantContext,
guest_id: &str,
) -> Result<GuestProfile> {
let config = get_resy_config(ctx).await?;
let client = reqwest::Client::new();

let response = client
.get(&format!(
"https://api.resy.com/3/venue/{}/guest/{}",
config.venue_id, guest_id
))
.header("Authorization", create_auth_header(&config.api_key))
.send()
.await?;

let resy_guest: ResyGuest = response.json().await?;

// Merge with existing profile
let profile = GuestProfile {
id: guest_id.to_string(),
name: resy_guest.name,
email: resy_guest.email,
phone: resy_guest.phone,
total_visits: resy_guest.venue_stats.total_visits,
last_visit: resy_guest.venue_stats.last_visit,
average_spend: resy_guest.venue_stats.average_spend,
tags: resy_guest.tags,
preferences: resy_guest.preferences,
dietary_restrictions: resy_guest.dietary_restrictions,
notes: resy_guest.private_notes,
};

guest_service.upsert(ctx, &profile).await?;

Ok(profile)
}

Unified Reservation Model

Data Model

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Reservation {
pub id: String,
pub external_id: Option<String>,
pub source: ReservationSource,
pub guest: Guest,
pub party_size: u32,
pub date: NaiveDate,
pub time: NaiveTime,
pub duration_minutes: Option<u32>,
pub table_id: Option<String>,
pub status: ReservationStatus,
pub notes: Option<String>,
pub tags: Vec<String>,
pub created_at: DateTime<Utc>,
pub modified_at: DateTime<Utc>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ReservationSource {
OpenTable,
Resy,
Yelp,
Direct, // Booked directly in Olympus
Phone, // Phone reservation
WalkIn, // Walk-in converted to reservation
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ReservationStatus {
Pending, // Awaiting confirmation
Confirmed, // Confirmed by guest
Seated, // Guest has arrived and seated
Completed, // Dining completed
Cancelled, // Cancelled
NoShow, // Guest didn't show
}

Status Mapping

Olympus StatusOpenTableResy
Pendingpendingpending
Confirmedconfirmedbooked
Seatedseatedarrived
Completedcompletedfinished
Cancelledcancelledcancelled
NoShowno_showno_show

Table Assignment

Auto-Assignment Logic

pub async fn auto_assign_table(
ctx: &TenantContext,
reservation: &Reservation,
) -> Result<Option<String>> {
let floor_plan = floor_plan_service.get_active(ctx).await?;
let existing_assignments = table_service.get_assignments(
ctx,
reservation.date,
reservation.time,
reservation.time + Duration::minutes(90)
).await?;

// Find available tables that fit the party
let available_tables: Vec<_> = floor_plan.tables.iter()
.filter(|t| {
// Table can fit party
t.min_capacity <= reservation.party_size &&
t.max_capacity >= reservation.party_size &&
// Table is not already assigned
!existing_assignments.iter().any(|a| a.table_id == t.id)
})
.collect();

if available_tables.is_empty() {
return Ok(None);
}

// Score tables by fit (prefer tables that fit party size exactly)
let best_table = available_tables.into_iter()
.min_by_key(|t| {
(t.max_capacity - reservation.party_size).abs() as i32
})
.map(|t| t.id.clone());

Ok(best_table)
}

Table Sync

pub async fn sync_table_to_platform(
ctx: &TenantContext,
reservation: &Reservation,
table_id: &str,
) -> Result<()> {
match reservation.source {
ReservationSource::OpenTable => {
opentable.update_table_assignment(
&reservation.external_id.as_ref().unwrap(),
table_id
).await?;
}
ReservationSource::Resy => {
resy.update_table_assignment(
&reservation.external_id.as_ref().unwrap(),
table_id
).await?;
}
_ => {}
}

Ok(())
}

Availability Sync

Real-Time Availability

pub async fn calculate_availability(
ctx: &TenantContext,
date: NaiveDate,
) -> Result<Vec<TimeSlot>> {
let floor_plan = floor_plan_service.get_active(ctx).await?;
let reservations = reservation_service.get_for_date(ctx, date).await?;
let operating_hours = hours_service.get_for_date(ctx, date).await?;

let mut slots = Vec::new();

// Generate slots for each 15-minute interval
let mut current_time = operating_hours.open_time;
while current_time < operating_hours.close_time {
let conflicting = reservations.iter()
.filter(|r| {
let end_time = r.time + Duration::minutes(90);
r.time <= current_time && end_time > current_time
})
.count();

let available_tables = floor_plan.tables.len() - conflicting;

slots.push(TimeSlot {
time: current_time,
available: available_tables > 0,
available_party_sizes: calculate_party_sizes(
&floor_plan,
&reservations,
current_time
),
});

current_time = current_time + Duration::minutes(15);
}

Ok(slots)
}

pub async fn push_availability_to_platforms(
ctx: &TenantContext,
date: NaiveDate,
) -> Result<()> {
let availability = calculate_availability(ctx, date).await?;

// Push to all connected platforms
let integrations = integration_service.get_active(ctx).await?;

for integration in integrations {
match integration.platform {
Platform::OpenTable => {
opentable.update_availability(ctx, date, &availability).await?;
}
Platform::Resy => {
let slots = convert_to_resy_slots(&availability);
resy.update_slot_inventory(ctx, date, &slots).await?;
}
_ => {}
}
}

Ok(())
}

Guest Data Sync

Profile Merge

pub async fn merge_guest_profiles(
ctx: &TenantContext,
email: &str,
) -> Result<GuestProfile> {
let mut merged = GuestProfile::default();

// Get profiles from all platforms
if let Some(ot_profile) = opentable.get_guest_by_email(ctx, email).await? {
merged.merge(&ot_profile);
}

if let Some(resy_profile) = resy.get_guest_by_email(ctx, email).await? {
merged.merge(&resy_profile);
}

if let Some(local_profile) = guest_service.find_by_email(ctx, email).await? {
merged.merge(&local_profile);
}

// Save merged profile
guest_service.upsert(ctx, &merged).await?;

Ok(merged)
}

Webhook Configuration

Supported Events

EventOpenTableResy
New Reservationreservation.createdreservation.new
Modifiedreservation.modifiedreservation.modified
Cancelledreservation.cancelledreservation.cancelled
No-Showreservation.noshowreservation.noshow
Seatedreservation.seatedreservation.arrived

Webhook Security

pub fn verify_opentable_signature(
payload: &str,
signature: &str,
secret: &str,
) -> Result<()> {
let computed = hmac_sha256(secret.as_bytes(), payload.as_bytes());
let expected = hex::decode(signature)?;

if computed != expected {
return Err(Error::InvalidSignature);
}

Ok(())
}

Error Codes

CodeDescriptionResolution
PLATFORM_UNAVAILABLEPlatform API downRetry with backoff
INVALID_CREDENTIALSAuth failedRe-configure integration
RESERVATION_NOT_FOUNDExternal ID invalidCheck confirmation number
TABLE_CONFLICTTable already assignedUse different table
SYNC_CONFLICTData out of syncForce full sync