Webhooks
Receive real-time event notifications via HTTP callbacks.
Overview
Webhooks allow your application to receive notifications when events occur:
| Event Category | Events | Use Cases |
|---|---|---|
| Orders | Created, Updated, Completed | Order management, fulfillment |
| Payments | Processed, Refunded, Failed | Financial tracking |
| Inventory | Low Stock, Updated | Stock management |
| Menu | Item Updated, Price Changed | Menu sync |
| Locations | Hours Changed, Status Changed | Store 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
| Event | Description | Trigger |
|---|---|---|
order.created | New order created | Order submitted |
order.updated | Order modified | Items added/removed |
order.sent | Sent to kitchen | Order sent to KDS |
order.ready | Ready for pickup | All items complete |
order.completed | Order finished | Order closed |
order.cancelled | Order cancelled | Order voided |
Payment Events
| Event | Description | Trigger |
|---|---|---|
payment.processed | Payment successful | Card charged |
payment.failed | Payment declined | Card rejected |
payment.refunded | Refund issued | Refund processed |
payment.voided | Payment voided | Pre-capture void |
Inventory Events
| Event | Description | Trigger |
|---|---|---|
inventory.low_stock | Stock below threshold | Quantity < reorder level |
inventory.out_of_stock | Stock depleted | Quantity = 0 |
inventory.updated | Stock changed | Manual or sale adjustment |
Menu Events
| Event | Description | Trigger |
|---|---|---|
menu.item_updated | Item modified | Item saved |
menu.item_availability | Availability changed | 86'd/un-86'd |
menu.price_changed | Price modified | Price updated |
Location Events
| Event | Description | Trigger |
|---|---|---|
location.hours_changed | Operating hours updated | Schedule changed |
location.status_changed | Store opened/closed | Status 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
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 min |
| 3 | 5 minutes | 6 min |
| 4 | 30 minutes | 36 min |
| 5 | 2 hours | ~2.5 hours |
| 6 | 8 hours | ~10.5 hours |
| 7 | 24 hours | ~34.5 hours |
Response Codes
| Code | Meaning | Retry |
|---|---|---|
| 2xx | Success | No |
| 4xx | Client error | No (except 429) |
| 429 | Rate limited | Yes |
| 5xx | Server error | Yes |
| Timeout | No response | Yes |
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;
}
}
Related Documentation
- Events - Real-time events
- Authentication - API authentication
- API Reference - Full API documentation