Saltar al contenido principal

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:

EndpointMethodPurpose
/healthGETPeriodic health check — 5s timeout, no auth sent, slower than 3s is recorded as degraded
Your webhook routePOSTSigned 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.

EndpointMethodPurpose
/capabilitiesGETWhat your integration supports
/ordersPOSTPush an order to the delivery platform
/orders/:idPUTUpdate platform-side order status
/orders/:id/cancelPOSTCancel on the platform
/menu/syncPOSTPush the menu to the platform
/webhooks/platformPOSTReceive platform events
The order flow, in one line

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:create in its supported_features
  • order_type can be DELIVERY, TAKEAWAY, PICKUP, or TAKEOUT (the last two normalize to TAKEAWAY). DINE_IN is rejected
  • external_order_id may also be sent as an X-Idempotency-Key header
  • Server computes totals from menu item prices — plugin-sent totals are ignored
  • external_order_id enables 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.

Changed 2026-09-28

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​

DoorDash​

Skip the Dishes​

  • OAuth: No (API Key)
  • Webhooks: Yes
  • Contact: Contact Skip for API access

Next Steps​

Support​