Skip to main content

Webhooks

Receive real-time event notifications via HTTP callbacks.

Overview

Webhooks allow your application to receive notifications when events occur:

Event CategoryEventsUse Cases
OrdersCreated, Updated, CompletedOrder management, fulfillment
PaymentsProcessed, Refunded, FailedFinancial tracking
InventoryLow Stock, UpdatedStock management
MenuItem Updated, Price ChangedMenu sync
LocationsHours Changed, Status ChangedStore management

Configuration

Create Webhook

curl -X POST https://api.olympuscloud.ai/v1/webhooks \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/olympus",
"events": [
"order.created",
"order.updated",
"order.completed",
"payment.processed"
],
"secret": "your-webhook-secret"
}'

Response

{
"id": "wh_abc123",
"url": "https://your-app.com/webhooks/olympus",
"events": [
"order.created",
"order.updated",
"order.completed",
"payment.processed"
],
"status": "active",
"created_at": "2026-01-18T10:00:00Z"
}

Update Webhook

curl -X PATCH https://api.olympuscloud.ai/v1/webhooks/wh_abc123 \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
"order.created",
"order.completed"
]
}'

Delete Webhook

curl -X DELETE https://api.olympuscloud.ai/v1/webhooks/wh_abc123 \
-H "Authorization: Bearer $API_KEY"

Event Types

Order Events

EventDescriptionTrigger
order.createdNew order createdOrder submitted
order.updatedOrder modifiedItems added/removed
order.sentSent to kitchenOrder sent to KDS
order.readyReady for pickupAll items complete
order.completedOrder finishedOrder closed
order.cancelledOrder cancelledOrder voided

Payment Events

EventDescriptionTrigger
payment.processedPayment successfulCard charged
payment.failedPayment declinedCard rejected
payment.refundedRefund issuedRefund processed
payment.voidedPayment voidedPre-capture void

Inventory Events

EventDescriptionTrigger
inventory.low_stockStock below thresholdQuantity < reorder level
inventory.out_of_stockStock depletedQuantity = 0
inventory.updatedStock changedManual or sale adjustment
EventDescriptionTrigger
menu.item_updatedItem modifiedItem saved
menu.item_availabilityAvailability changed86'd/un-86'd
menu.price_changedPrice modifiedPrice updated

Location Events

EventDescriptionTrigger
location.hours_changedOperating hours updatedSchedule changed
location.status_changedStore opened/closedStatus toggle

Webhook Payload

Standard Format

{
"id": "evt_abc123",
"type": "order.created",
"api_version": "2026-01-01",
"created_at": "2026-01-18T10:30:00Z",
"tenant_id": "tenant_xyz",
"location_id": "loc_789",
"data": {
"id": "order_456",
"number": "1042",
"order_type": "dine_in",
"status": "open",
"items": [
{
"id": "li_001",
"menu_item_id": "item_burger",
"name": "Classic Burger",
"quantity": 2,
"unit_price": 14.99,
"total": 29.98
}
],
"subtotal": 29.98,
"tax": 2.55,
"total": 32.53,
"created_at": "2026-01-18T10:30:00Z"
}
}

Event-Specific Payloads

order.created

{
"type": "order.created",
"data": {
"id": "order_456",
"number": "1042",
"order_type": "dine_in",
"table": "Table 7",
"server": {
"id": "user_123",
"name": "Jane Smith"
},
"items": [...],
"subtotal": 45.99,
"tax": 3.91,
"total": 49.90
}
}

payment.processed

{
"type": "payment.processed",
"data": {
"id": "pay_789",
"order_id": "order_456",
"method": "card",
"amount": 49.90,
"tip": 10.00,
"total": 59.90,
"card": {
"brand": "visa",
"last4": "4242"
},
"transaction_id": "ch_abc123"
}
}

inventory.low_stock

{
"type": "inventory.low_stock",
"data": {
"id": "inv_123",
"item_id": "item_chicken",
"name": "Chicken Breast",
"current_quantity": 5,
"unit": "lb",
"reorder_level": 10,
"reorder_quantity": 50
}
}

Security

Signature Verification

All webhooks include a signature header for verification:

X-Olympus-Signature: t=1705574400,v1=abc123def456...

Verification Code

// Node.js verification
import crypto from 'crypto';

function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const parts = signature.split(',');
const timestamp = parts[0].split('=')[1];
const receivedSig = parts[1].split('=')[1];

// Check timestamp (reject if older than 5 minutes)
const eventTime = parseInt(timestamp) * 1000;
const now = Date.now();
if (now - eventTime > 300000) {
return false;
}

// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');

// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(receivedSig),
Buffer.from(expectedSig)
);
}
// Rust verification
use hmac::{Hmac, Mac};
use sha2::Sha256;

pub fn verify_signature(
payload: &str,
signature: &str,
secret: &str,
) -> Result<bool> {
let parts: Vec<&str> = signature.split(',').collect();
let timestamp = parts[0].strip_prefix("t=").ok_or(Error::InvalidSignature)?;
let received_sig = parts[1].strip_prefix("v1=").ok_or(Error::InvalidSignature)?;

// Check timestamp
let event_time: i64 = timestamp.parse()?;
let now = chrono::Utc::now().timestamp();
if now - event_time > 300 {
return Ok(false);
}

// Compute expected signature
let signed_payload = format!("{}.{}", timestamp, payload);
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())?;
mac.update(signed_payload.as_bytes());
let expected_sig = hex::encode(mac.finalize().into_bytes());

// Constant-time comparison
Ok(constant_time_eq(received_sig.as_bytes(), expected_sig.as_bytes()))
}

Handling Webhooks

Endpoint Implementation

// Express.js handler
import express from 'express';

const app = express();

app.post('/webhooks/olympus', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-olympus-signature'] as string;
const payload = req.body.toString();

// Verify signature
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send('Invalid signature');
}

const event = JSON.parse(payload);

// Handle event
switch (event.type) {
case 'order.created':
handleOrderCreated(event.data);
break;
case 'order.completed':
handleOrderCompleted(event.data);
break;
case 'payment.processed':
handlePaymentProcessed(event.data);
break;
default:
console.log('Unhandled event type:', event.type);
}

// Acknowledge receipt
res.status(200).json({ received: true });
});
// Axum handler
use axum::{
body::Bytes,
http::{HeaderMap, StatusCode},
Json,
};

pub async fn handle_webhook(
headers: HeaderMap,
body: Bytes,
) -> Result<StatusCode, StatusCode> {
let signature = headers
.get("x-olympus-signature")
.and_then(|h| h.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;

let payload = std::str::from_utf8(&body)
.map_err(|_| StatusCode::BAD_REQUEST)?;

// Verify signature
if !verify_signature(payload, signature, &config.webhook_secret)? {
return Err(StatusCode::UNAUTHORIZED);
}

let event: WebhookEvent = serde_json::from_str(payload)
.map_err(|_| StatusCode::BAD_REQUEST)?;

// Process asynchronously
tokio::spawn(async move {
if let Err(e) = process_event(event).await {
tracing::error!("Failed to process webhook: {:?}", e);
}
});

Ok(StatusCode::OK)
}

Retry Policy

Retry Schedule

AttemptDelayTotal Time
1Immediate0
21 minute1 min
35 minutes6 min
430 minutes36 min
52 hours~2.5 hours
68 hours~10.5 hours
724 hours~34.5 hours

Response Codes

CodeMeaningRetry
2xxSuccessNo
4xxClient errorNo (except 429)
429Rate limitedYes
5xxServer errorYes
TimeoutNo responseYes

Testing

Test Endpoint

# Send test webhook
curl -X POST https://api.olympuscloud.ai/v1/webhooks/wh_abc123/test \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "order.created"
}'

Webhook Logs

# List recent deliveries
curl https://api.olympuscloud.ai/v1/webhooks/wh_abc123/deliveries \
-H "Authorization: Bearer $API_KEY"

Response:

{
"data": [
{
"id": "del_xyz",
"event_id": "evt_abc123",
"event_type": "order.created",
"status": "success",
"response_code": 200,
"response_time_ms": 145,
"delivered_at": "2026-01-18T10:30:01Z"
},
{
"id": "del_abc",
"event_id": "evt_def456",
"event_type": "payment.processed",
"status": "failed",
"response_code": 500,
"response_time_ms": 2500,
"next_retry_at": "2026-01-18T10:35:00Z"
}
]
}

Local Testing

# Use ngrok for local development
ngrok http 3000

# Update webhook URL
curl -X PATCH https://api.olympuscloud.ai/v1/webhooks/wh_abc123 \
-H "Authorization: Bearer $API_KEY" \
-d '{"url": "https://abc123.ngrok.io/webhooks/olympus"}'

Best Practices

Idempotency

Handle duplicate events gracefully:

const processedEvents = new Set<string>();

async function handleEvent(event: WebhookEvent) {
// Check if already processed
if (processedEvents.has(event.id)) {
console.log('Event already processed:', event.id);
return;
}

// Or check database
const existing = await db.webhookEvents.findUnique({
where: { eventId: event.id }
});

if (existing) {
return;
}

// Process event
await processEvent(event);

// Mark as processed
await db.webhookEvents.create({
data: { eventId: event.id, processedAt: new Date() }
});

processedEvents.add(event.id);
}

Async Processing

Process webhooks asynchronously:

app.post('/webhooks/olympus', async (req, res) => {
// Verify signature...

// Acknowledge immediately
res.status(200).send('OK');

// Process in background
setImmediate(async () => {
try {
await processEvent(event);
} catch (error) {
console.error('Webhook processing failed:', error);
// Queue for retry or alert
}
});
});

Error Handling

async function processEvent(event: WebhookEvent) {
try {
switch (event.type) {
case 'order.created':
await handleOrderCreated(event.data);
break;
// ... other cases
}
} catch (error) {
// Log error details
console.error({
message: 'Webhook processing failed',
eventId: event.id,
eventType: event.type,
error: error.message,
});

// Re-throw for retry if applicable
throw error;
}
}