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:
| Platform | Features | Sync Type | API |
|---|---|---|---|
| OpenTable | Reservations, Guests, Availability | Real-time | REST API |
| Resy | Reservations, Guests, Waitlist | Real-time | REST API |
| Yelp Reservations | Reservations, Reviews | Real-time | Fusion API |
| Custom Webhook | Any platform | Webhook | Custom |
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 Status | OpenTable | Resy |
|---|---|---|
Pending | pending | pending |
Confirmed | confirmed | booked |
Seated | seated | arrived |
Completed | completed | finished |
Cancelled | cancelled | cancelled |
NoShow | no_show | no_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
| Event | OpenTable | Resy |
|---|---|---|
| New Reservation | reservation.created | reservation.new |
| Modified | reservation.modified | reservation.modified |
| Cancelled | reservation.cancelled | reservation.cancelled |
| No-Show | reservation.noshow | reservation.noshow |
| Seated | reservation.seated | reservation.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
| Code | Description | Resolution |
|---|---|---|
PLATFORM_UNAVAILABLE | Platform API down | Retry with backoff |
INVALID_CREDENTIALS | Auth failed | Re-configure integration |
RESERVATION_NOT_FOUND | External ID invalid | Check confirmation number |
TABLE_CONFLICT | Table already assigned | Use different table |
SYNC_CONFLICT | Data out of sync | Force full sync |
Related Documentation
- Table Management API - Floor plan and tables
- Reservations API - Reservation management
- Seating API - Seating and waitlist