Create Custom Shop

Learn how to create custom HTTP-based shop integrations

Overview

Instead of using built-in shop integrations like Shopify, you can create custom HTTP endpoints that Openship will call to interact with your shop platform. This approach gives you complete flexibility to integrate with any system - whether it's a custom e-commerce platform, CRM, or even a spreadsheet.

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:

FieldTypeDescription
platform{ domain: string; accessToken: string }Platform credentials
searchEntrystringSearch query
after?stringPagination cursor

Response Format:

FieldTypeDescription
productsProduct[]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:

FieldTypeDescription
platform{ domain: string; accessToken: string }Platform credentials
productIdstringProduct identifier
variantId?stringVariant identifier

Response Format:

FieldTypeDescription
productProductSingle 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:

FieldTypeDescription
platform{ domain: string; accessToken: string }Platform credentials
searchEntrystringSearch query
after?stringPagination cursor

Response Format:

FieldTypeDescription
ordersOrder[]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:

FieldTypeDescription
platform{ domain: string; accessToken: string }Platform credentials
productIdstringProduct identifier
variantIdstringVariant identifier
inventory?numberNew inventory count
price?stringNew price

Response Format:

FieldTypeDescription
successbooleanWhether update succeeded
resultsobject[]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

  1. In your Openship dashboard, navigate to Platform > Shops
  2. Click "Add Platform"
  3. 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

  1. After creating the platform, click "Create Shop"
  2. In the "Create Shop" dialog:
    • Platform: Select "Demo Shop Platform"
    • Name: "Demo" (or any name you prefer)
    • Domain: "local"
    • Access Token: "demo_token"
  3. 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:

  1. Go to your Demo Shop Platform settings
  2. 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 on

This approach lets you learn how Openship works with our demo data first, then seamlessly transition to your own implementation.