Delivery Platform Integration
Build delivery platform plugins to connect restaurants with services like Uber Eats, DoorDash, Skip the Dishes, and more.
Overview
Delivery platform integrations enable restaurants to:
- Receive orders from delivery platforms
- Update order status and estimated times
- Sync menu items and availability
- Handle order modifications and cancellations
- Track delivery status
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ SSP Backend │────────>│ Your Plugin │────────>│ Uber Eats │
│ │ HTTPS │ Bridge Service │ OAuth │ DoorDash │
└─────────────┘ └──────────────────┘ └─────────────┘
↑ ↑
│ │
└─────────<───────────────┘
Webhooks (order updates)
What SSP actually calls on your service
For a marketplace plugin, SSP makes exactly two kinds of call to you:
| Endpoint | Method | Purpose |
|---|---|---|
/health | GET | Periodic health check — 5s timeout, no auth sent, slower than 3s is recorded as degraded |
| Your webhook route | POST | Signed event delivery — verify X-SSP-Signature |
Everything else in a delivery integration is your plugin calling SSP with
your pik_ key, or your plugin calling the delivery platform.
A suggested internal structure
The routes below are a useful shape for your own service. They are your design, not an SSP contract — SSP never calls them.
| Endpoint | Method | Purpose |
|---|---|---|
/capabilities | GET | What your integration supports |
/orders | POST | Push an order to the delivery platform |
/orders/:id | PUT | Update platform-side order status |
/orders/:id/cancel | POST | Cancel on the platform |
/menu/sync | POST | Push the menu to the platform |
/webhooks/platform | POST | Receive platform events |
Platform order arrives → you POST /orders to SSP → you subscribe to
order.status_changed → you push kitchen progress back to the platform.
Implementation
1. Create Order
Receive order from SSP and create in delivery platform:
app.post('/orders', authenticate, async (req, res) => {
try {
const {
order_id,
items,
customer,
delivery_address,
special_instructions,
provider_config
} = req.body;
// Initialize platform client
const client = new UberEatsClient({
client_id: provider_config.uber_eats_client_id,
client_secret: provider_config.uber_eats_client_secret,
store_id: provider_config.uber_eats_store_id
});
// Create order on platform
const platformOrder = await client.orders.create({
external_reference_id: order_id,
items: items.map(item => ({
id: item.external_id,
name: item.name,
quantity: item.quantity,
price: item.price,
modifiers: item.modifiers
})),
customer: {
name: customer.name,
phone: customer.phone,
email: customer.email
},
delivery_address: {
street: delivery_address.street,
city: delivery_address.city,
state: delivery_address.state,
postal_code: delivery_address.postal_code,
country: delivery_address.country,
latitude: delivery_address.latitude,
longitude: delivery_address.longitude
},
special_instructions: special_instructions
});
res.json({
success: true,
external_order_id: platformOrder.id,
status: 'accepted',
estimated_pickup_time: platformOrder.estimated_pickup_time,
estimated_delivery_time: platformOrder.estimated_delivery_time,
tracking_url: platformOrder.tracking_url
});
} catch (error) {
console.error('Order creation failed:', error);
res.status(400).json({
error: true,
message: error.message,
code: 'ORDER_CREATION_FAILED'
});
}
});
2. Update Order Status
Update order status in delivery platform:
app.put('/orders/:id', authenticate, async (req, res) => {
try {
const { id } = req.params;
const { status, estimated_ready_time, provider_config } = req.body;
const client = new UberEatsClient(provider_config);
await client.orders.updateStatus(id, {
status: mapStatusToProvider(status),
estimated_ready_time: estimated_ready_time
});
res.json({
success: true,
order_id: id,
status: status,
updated_at: new Date().toISOString()
});
} catch (error) {
res.status(400).json({
error: true,
message: error.message,
code: 'STATUS_UPDATE_FAILED'
});
}
});
function mapStatusToProvider(status) {
const statusMap = {
'accepted': 'accepted',
'preparing': 'preparing',
'ready_for_pickup': 'ready',
'picked_up': 'picked_up',
'cancelled': 'cancelled'
};
return statusMap[status] || status;
}
3. Menu Sync
Sync menu items to delivery platform:
app.post('/menu/sync', authenticate, async (req, res) => {
try {
const { categories, provider_config } = req.body;
const client = new UberEatsClient(provider_config);
// Transform menu format
const platformMenu = {
categories: categories.map(category => ({
title: category.name,
items: category.items.map(item => ({
title: item.name,
description: item.description,
price: item.price,
image_url: item.image_url,
available: item.available,
modifiers: item.modifiers?.map(mod => ({
title: mod.name,
options: mod.options.map(opt => ({
title: opt.name,
price: opt.price
}))
}))
}))
}))
};
await client.menu.update(platformMenu);
res.json({
success: true,
synced_categories: categories.length,
synced_items: categories.reduce((sum, cat) => sum + cat.items.length, 0),
synced_at: new Date().toISOString()
});
} catch (error) {
res.status(400).json({
error: true,
message: error.message,
code: 'MENU_SYNC_FAILED'
});
}
});
4. Handle Platform Webhooks
Receive and process webhooks from delivery platform:
app.post('/webhooks/ubereats', async (req, res) => {
try {
// 1. Verify webhook signature
const signature = req.headers['x-uber-signature'];
if (!verifyUberEatsSignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}
// 2. Process event
const event = req.body;
console.log('Webhook received:', event.event_type);
// 3. Forward to SSP
await forwardToSSP({
provider: 'uber-eats',
event_type: event.event_type,
event_id: event.event_id,
payload: transformEvent(event),
timestamp: new Date().toISOString()
});
// 4. Respond immediately
res.json({ status: 'ok' });
} catch (error) {
console.error('Webhook processing failed:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
function transformEvent(event) {
switch (event.event_type) {
case 'orders.notification':
return {
order_id: event.meta.resource_id,
status: event.meta.status,
estimated_delivery_time: event.meta.estimated_delivery_time
};
case 'orders.cancel':
return {
order_id: event.meta.resource_id,
cancellation_reason: event.meta.cancellation_reason
};
default:
return event;
}
}
Creating Orders in SSP (Delivery Origination)
When a customer places an order on Uber Eats, DoorDash, or your delivery platform, push the order into SSP so the kitchen can prepare it:
curl -X POST "https://api.ssppos.com/api/plugin/v1/orders" \
-H "X-Plugin-Api-Key: pik_..." \
-H "Content-Type: application/json" \
-d '{
"location_id": 1,
"order_type": "DELIVERY",
"external_order_id": "UE-ABC123",
"customer": {
"email": "customer@example.com",
"first_name": "Jane",
"delivery_address": "456 Oak Ave, Toronto"
},
"items": [
{ "menu_item_id": 42, "quantity": 2 },
{ "menu_item_id": 15, "quantity": 1 }
],
"tip_amount": 3.00,
"metadata": { "delivery_platform": "uber_eats", "platform_order_id": "UE-ABC123" }
}'
Requirements:
- Your plugin must have
orders:createin itssupported_features order_typecan beDELIVERY,TAKEAWAY,PICKUP, orTAKEOUT(the last two normalize toTAKEAWAY).DINE_INis rejectedexternal_order_idmay also be sent as anX-Idempotency-Keyheader- Server computes totals from menu item prices — plugin-sent totals are ignored
external_order_idenables idempotency — resending returns the original order- Customer is optional (some platforms like Uber Eats hold the customer relationship)
The order reaches the kitchen as it is created. Its lines appear on the
restaurant's kitchen display straight away, and the ticket is labelled with your
plugin's display_name, so staff can tell which platform the order came from.
The order therefore starts in_progress, and the create response says so:
{
"data": {
"id": 123,
"unique_orderid": "ORD-2026-001",
"order_status": "in_progress",
"order_type": "DELIVERY"
}
}
After creating the order, drive it through the rest of the
state machine using PUT /orders/{id}:
in_progress → ready → completed → paid
↘ cancelled (from in_progress or ready)
There is no open → in_progress step to send. A PUT to the state the order
is already in (for example in_progress right after creating it) is a no-op
200.
If you subscribe to order.*, each order you create produces order.created
followed by order.status_changed from CREATED to COOKING (webhooks carry
internal status names) as it is sent to the kitchen.
Orders created through the API used to start open. A PUT with open on
such an order is still accepted as a no-op, with a deprecation signal, until
2027-03-31. See the Changelog for what to
change.
See the full Order Creation reference for customization support, customer matching, and error codes.
Order Status Flow
┌──────────┐ ┌──────────┐ ┌───────────────┐ ┌───────────┐ ┌───────────┐
│ received │───>│ accepted │───>│ ready_for_ │───>│ picked_up │───>│ delivered │
│ │ │ │ │ pickup │ │ │ │ │
└──────────┘ └──────────┘ └───────────────┘ └───────────┘ └───────────┘
│ │
│ │
▼ ▼
┌───────────┐ ┌──────────┐
│ cancelled │ │ failed │
└───────────┘ └──────────┘
OAuth 2.0 Flow
Many delivery platforms use OAuth for authentication:
// Step 1: Redirect user to authorize
app.get('/oauth/authorize', authenticate, (req, res) => {
const state = generateState();
const authUrl = `https://login.uber.com/oauth/v2/authorize?` +
`client_id=${process.env.UBER_EATS_CLIENT_ID}&` +
`response_type=code&` +
`redirect_uri=${process.env.REDIRECT_URI}&` +
`scope=eats.store&` +
`state=${state}`;
res.redirect(authUrl);
});
// Step 2: Handle callback
app.get('/oauth/callback', async (req, res) => {
try {
const { code, state } = req.query;
// Verify state
if (!verifyState(state)) {
throw new Error('Invalid state parameter');
}
// Exchange code for tokens
const tokens = await exchangeCodeForTokens(code);
// Store tokens securely
await storeTokens({
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
expires_at: Date.now() + tokens.expires_in * 1000
});
res.redirect('https://manager.ssppos.com/marketplace/plugins/' + plugin_id + '?oauth=success');
} catch (error) {
res.redirect('https://manager.ssppos.com/marketplace/plugins/' + plugin_id + '?oauth=error');
}
});
async function exchangeCodeForTokens(code) {
const response = await axios.post('https://login.uber.com/oauth/v2/token', {
client_id: process.env.UBER_EATS_CLIENT_ID,
client_secret: process.env.UBER_EATS_CLIENT_SECRET,
grant_type: 'authorization_code',
code: code,
redirect_uri: process.env.REDIRECT_URI
});
return response.data;
}
Best Practices
1. Handle Rate Limits
const rateLimit = require('express-rate-limit');
const orderLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 60, // 60 orders per minute
message: { error: true, message: 'Too many orders' }
});
app.post('/orders', authenticate, orderLimiter, async (req, res) => {
// Handle order
});
2. Token Refresh
async function getValidAccessToken(provider_config) {
const { access_token, refresh_token, expires_at } = provider_config;
// Check if token is expired
if (Date.now() >= expires_at - 5 * 60 * 1000) { // 5 min buffer
const newTokens = await refreshAccessToken(refresh_token);
await updateStoredTokens(newTokens);
return newTokens.access_token;
}
return access_token;
}
3. Order Deduplication
const processedOrders = new Set();
app.post('/orders', authenticate, async (req, res) => {
const orderId = req.body.order_id;
if (processedOrders.has(orderId)) {
return res.json({ success: true, message: 'Already processed' });
}
// Process order
const result = await createOrder(req.body);
processedOrders.add(orderId);
res.json(result);
});
4. Error Recovery
async function createOrderWithRetry(orderData, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await createOrder(orderData);
} catch (error) {
if (attempt === maxRetries) throw error;
// Exponential backoff
await sleep(Math.pow(2, attempt) * 1000);
}
}
}
Testing
Mock Webhooks
// Test webhook endpoint locally
curl -X POST http://localhost:3000/webhooks/ubereats \
-H "Content-Type: application/json" \
-H "X-Uber-Signature: test_signature" \
-d '{
"event_id": "test_123",
"event_type": "orders.notification",
"meta": {
"resource_id": "order_789",
"status": "preparing"
}
}'
Sandbox Environment
Most platforms provide sandbox/test environments:
const baseUrl = process.env.NODE_ENV === 'production'
? 'https://api.uber.com'
: 'https://sandbox-api.uber.com';
Common Providers
Uber Eats
- OAuth: Yes
- Webhooks: Yes
- Docs: developer.uber.com/docs/eats
DoorDash
- OAuth: Yes
- Webhooks: Yes
- Docs: developer.doordash.com
Skip the Dishes
- OAuth: No (API Key)
- Webhooks: Yes
- Contact: Contact Skip for API access
Next Steps
- Webhooks Guide - Handle real-time events
- Security Best Practices - Secure your integration
- Plugin Data API - Order creation and menu sync
Support
- Email: developers@ssppos.com
- Examples: github.com/ssppos/examples