Create Custom Shop
Learn how to create custom HTTP-based shop integrations
Overview
Instead of a compiled Shopify/Openfront shop adapter, a ShopPlatform can select custom HTTP endpoints that Openship calls for source catalog and order operations. The endpoint must implement the documented request/response contract and remain authoritative for its own products and orders.
Openship executes database-selected URLs.
How Custom Integrations Work
When you create a shop platform in Openship, instead of setting function values to built-in slugs like "shopify", you can provide HTTP URLs. Openship will make POST requests to these endpoints with the required parameters and expect specific response formats.
For example:
- Built-in:
searchProductsFunction: "shopify" - Custom:
searchProductsFunction: "https://your-app.com/api/search-products"
Required HTTP Endpoints
Your custom shop integration must implement the following HTTP endpoints that match the function interface from the shop integration guide:
Product Search - /api/search-products
Purpose: Openship uses this endpoint to allow users to search and browse products from your shop when creating product matches or exploring available inventory
HTTP Method: POST
Request Body:
| Field | Type | Description |
|---|---|---|
platform | { domain: string; accessToken: string } | Platform credentials |
searchEntry | string | Search query |
after? | string | Pagination cursor |
Response Format:
| Field | Type | Description |
|---|---|---|
products | Product[] | Array of product objects |
pageInfo | { hasNextPage: boolean; endCursor: string } | Pagination information |
// POST /api/search-products
export default async function handler(req, res) {
const { platform, searchEntry, after } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Your custom product data (could come from database, API, etc.)
const allProducts = [
{
image: "https://via.placeholder.com/300x300/000000/FFFFFF?text=HEADPHONES",
title: "Wireless Bluetooth Headphones",
productId: "headphones001",
variantId: "black",
price: "1337.00",
availableForSale: true,
inventory: 42,
inventoryTracked: true,
productLink: "https://demo-store.com/products/bluetooth-headphones",
cursor: "eyJpZCI6ImdpYnNvbjAwMSJ9"
},
{
image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=CHARGER",
title: "Portable Phone Charger Module",
productId: "charger001",
variantId: "black-box",
price: "2000.00",
availableForSale: true,
inventory: 5,
inventoryTracked: true,
productLink: "https://demo-store.com/products/phone-charger",
cursor: "eyJpZCI6InNldGVjMDAxIn0="
},
{
image: "https://via.placeholder.com/300x300/0000FF/FFFFFF?text=WAR",
title: "Smart Watch Series 5",
productId: "watch001",
variantId: "default",
price: "25000.00",
availableForSale: false,
inventory: 0,
inventoryTracked: true,
productLink: "https://demo-store.com/products/smart-watch",
cursor: "eyJpZCI6IndvcHIwMDEifQ=="
}
];
let filteredProducts = allProducts;
// Filter by search term if provided
if (searchEntry) {
filteredProducts = allProducts.filter(product =>
product.title.toLowerCase().includes(searchEntry.toLowerCase())
);
}
// Simple pagination (in real app, use proper pagination)
let startIndex = 0;
if (after) {
const decodedCursor = JSON.parse(Buffer.from(after, 'base64').toString());
startIndex = allProducts.findIndex(p => p.productId === decodedCursor.id) + 1;
}
const paginatedProducts = filteredProducts.slice(startIndex, startIndex + 10);
const hasNextPage = startIndex + 10 < filteredProducts.length;
const endCursor = paginatedProducts.length > 0
? paginatedProducts[paginatedProducts.length - 1].cursor
: null;
return res.status(200).json({
products: paginatedProducts,
pageInfo: {
hasNextPage,
endCursor
}
});
}Get Single Product - /api/get-product
Purpose: Openship uses this endpoint to fetch detailed product information when users select specific products for matching or viewing current inventory levels
HTTP Method: POST
Request Body:
| Field | Type | Description |
|---|---|---|
platform | { domain: string; accessToken: string } | Platform credentials |
productId | string | Product identifier |
variantId? | string | Variant identifier |
Response Format:
| Field | Type | Description |
|---|---|---|
product | Product | Single product object with full details |
// POST /api/get-product
export default async function handler(req, res) {
const { platform, productId, variantId } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Your product database (could be real database, API, etc.)
const products = {
"headphones001": {
black: {
image: "https://via.placeholder.com/300x300/000000/FFFFFF?text=HEADPHONES",
title: "Wireless Bluetooth Headphones - Midnight Black",
productId: "headphones001",
variantId: "black",
price: "1337.00",
availableForSale: true,
inventory: 42,
inventoryTracked: true,
productLink: "https://demo-store.com/products/bluetooth-headphones"
}
},
"charger001": {
"black-box": {
image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=CHARGER",
title: "Portable Phone Charger Module - Black Box",
productId: "charger001",
variantId: "black-box",
price: "2000.00",
availableForSale: true,
inventory: 5,
inventoryTracked: true,
productLink: "https://demo-store.com/products/phone-charger"
}
}
};
const product = products[productId]?.[variantId || 'default'];
if (!product) {
return res.status(404).json({ error: "Product not found" });
}
return res.status(200).json({ product });
}Search Orders - /api/search-orders
Purpose: Openship uses this endpoint to retrieve orders from your shop, allowing the system to automatically process incoming orders and trigger corresponding purchases on matched channel platforms
HTTP Method: POST
Request Body:
| Field | Type | Description |
|---|---|---|
platform | { domain: string; accessToken: string } | Platform credentials |
searchEntry | string | Search query |
after? | string | Pagination cursor |
Response Format:
| Field | Type | Description |
|---|---|---|
orders | Order[] | Array of order objects |
pageInfo | { hasNextPage: boolean; endCursor: string } | Pagination information |
// POST /api/search-orders
export default async function handler(req, res) {
const { platform, searchEntry, after } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Your orders data source
const allOrders = [
{
orderId: "order001",
orderName: "#ORDER-001",
link: "https://demo-store.com/orders/order001",
date: "09/15/1995",
firstName: "John",
lastName: "Smith",
streetAddress1: "123 Main Street",
streetAddress2: "Apt 404",
city: "New York",
state: "NY",
zip: "10001",
country: "United States",
email: "customer@demo-store.com",
fulfillmentStatus: "unfulfilled",
financialStatus: "paid",
totalPrice: "1337.00",
currency: "USD",
lineItems: [
{
lineItemId: "order001item001",
name: "Wireless Bluetooth Headphones",
quantity: 1,
image: "https://via.placeholder.com/300x300/000000/FFFFFF?text=HEADPHONES",
price: "1337.00",
variantId: "black",
productId: "headphones001"
}
],
cursor: "eyJpZCI6ImhhY2sxOTk1MDAxIn0="
},
{
orderId: "sneakers1992001",
orderName: "#ORDER-002",
link: "https://demo-store.com/orders/sneakers1992001",
date: "09/09/1992",
firstName: "Jane",
lastName: "Doe",
streetAddress1: "12 Maple Dr",
streetAddress2: "",
city: "San Francisco",
state: "CA",
zip: "94102",
country: "United States",
email: "cosmo@demo-store.com",
fulfillmentStatus: "unfulfilled",
financialStatus: "paid",
totalPrice: "2000.00",
currency: "USD",
lineItems: [
{
lineItemId: "charger001item001",
name: "Portable Phone Charger Module",
quantity: 1,
image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=CHARGER",
price: "2000.00",
variantId: "black-box",
productId: "charger001"
}
],
cursor: "eyJpZCI6InNuZWFrZXJzMTk5MjAwMSJ9"
}
];
let filteredOrders = allOrders;
// Filter by search term if provided
if (searchEntry) {
filteredOrders = allOrders.filter(order =>
order.orderName.toLowerCase().includes(searchEntry.toLowerCase()) ||
order.firstName.toLowerCase().includes(searchEntry.toLowerCase()) ||
order.lastName.toLowerCase().includes(searchEntry.toLowerCase())
);
}
return res.status(200).json({
orders: filteredOrders,
pageInfo: {
hasNextPage: false,
endCursor: null
}
});
}Update Product - /api/update-product
Purpose: Openship uses this endpoint to automatically sync inventory levels and pricing between matched products across different platforms
HTTP Method: POST
Request Body:
| Field | Type | Description |
|---|---|---|
platform | { domain: string; accessToken: string } | Platform credentials |
productId | string | Product identifier |
variantId | string | Variant identifier |
inventory? | number | New inventory count |
price? | string | New price |
Response Format:
| Field | Type | Description |
|---|---|---|
success | boolean | Whether update succeeded |
results | object[] | Update operation results |
// POST /api/update-product
export default async function handler(req, res) {
const { platform, productId, variantId, inventory, price } = req.body;
// Verify access token
if (platform.accessToken !== process.env.ACCESS_TOKEN) {
return res.status(403).json({ error: "Access denied" });
}
// Simulate updating your product database
const results = [];
if (price !== undefined) {
// Update price in your system
console.log(`Updating price for ${productId}:${variantId} to ${price}`);
results.push({
operation: "price_update",
productId,
variantId,
oldPrice: "1337.00", // You'd get this from your database
newPrice: price,
success: true
});
}
if (inventory !== undefined) {
// Update inventory in your system
console.log(`Updating inventory for ${productId}:${variantId} to ${inventory}`);
results.push({
operation: "inventory_update",
productId,
variantId,
oldInventory: 42, // You'd get this from your database
newInventory: inventory,
success: true
});
}
return res.status(200).json({
success: true,
results
});
}Webhook Event Handlers
Your custom shop can also implement webhook handlers to notify Openship of events in real-time:
Order Creation Handler - /api/webhook/order-created
Purpose: Openship uses this webhook to instantly receive and process new orders from your shop platform, automatically triggering the workflow to create corresponding purchases on matched channel platforms
// POST /api/webhook/order-created
export default async function handler(req, res) {
const { event, headers } = req.body;
// Verify webhook authenticity (implement your own verification)
if (!verifyWebhookSignature(headers)) {
return res.status(403).json({ error: "Invalid signature" });
}
// Transform your webhook event to Openship format
const order = {
id: event.order_id,
name: event.order_number,
email: event.customer_email,
financialStatus: event.payment_status,
fulfillmentStatus: "unfulfilled",
totalPrice: event.total_amount,
currency: event.currency,
lineItems: event.items.map(item => ({
id: item.line_item_id,
title: item.product_name,
quantity: item.quantity,
price: item.unit_price,
variantId: item.variant_id,
productId: item.product_id
})),
shippingAddress: {
firstName: event.shipping_address.first_name,
lastName: event.shipping_address.last_name,
address1: event.shipping_address.street,
city: event.shipping_address.city,
province: event.shipping_address.state,
country: event.shipping_address.country,
zip: event.shipping_address.postal_code
},
createdAt: event.created_at,
updatedAt: event.updated_at
};
return res.status(200).json({ order, type: "order_created" });
}Testing Your Custom Shop
We've built a demo shop integration right into this documentation that you can use to test the complete Openship workflow before implementing your own custom shop.
Step 1: Create a Demo Shop Platform
- In your Openship dashboard, navigate to Platform > Shops
- Click "Add Platform"
- In the "Create Shop Platform" dialog:
- Select "Demo" from the Platform dropdown
- The Name field will auto-fill with "Demo Shop"
- Click "Create Platform"
This creates a platform that points to our demo shop endpoints at https://docs.openship.org/api/demo/shop/*
Step 2: Connect Your Demo Shop
- After creating the platform, click "Create Shop"
- In the "Create Shop" dialog:
- Platform: Select "Demo Shop Platform"
- Name: "Demo" (or any name you prefer)
- Domain: "local"
- Access Token: "demo_token"
- Click "Create Shop"
Step 3: Test the Demo Integration
Once connected, you can test the complete workflow:
- Browse products using our demo product catalog
- View sample orders in the system
- Test product matching between shops and channels
- Experience the full order fulfillment process
Step 4: Replace with Your Implementation
When you're ready to use your custom shop:
- Go to your Demo Shop Platform settings
- Replace the demo URLs with your actual implementation:
# Replace these demo URLs:
https://docs.openship.org/api/demo/shop/search-products
https://docs.openship.org/api/demo/shop/get-product
https://docs.openship.org/api/demo/shop/search-orders
https://docs.openship.org/api/demo/shop/update-product
# ... and other endpoints
# With your URLs:
https://your-app.com/api/search-products
https://your-app.com/api/get-product
https://your-app.com/api/search-orders
https://your-app.com/api/update-product
# ... and so onThis approach lets you learn how Openship works with our demo data first, then seamlessly transition to your own implementation.