Create Custom Channel

Learn how to create custom HTTP-based channel integrations

Overview

Instead of using built-in channel integrations like Shopify, you can create custom HTTP endpoints that Openship will call to interact with your fulfillment platform. This approach gives you complete flexibility to integrate with any system - whether it's a 3PL, supplier, or custom fulfillment service.

How Custom Integrations Work

When you create a channel 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-fulfillment-api.com/api/search-products"

Required HTTP Endpoints

Your custom channel integration must implement the following HTTP endpoints that match the function interface from the channel integration guide:

Product Search - /api/search-products

Purpose: Openship uses this endpoint to search for products available for purchase when setting up product matches, allowing users to find fulfillment options for their shop products

HTTP Method: POST

Request Body:

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

Response Format:

FieldTypeDescription
productsProduct[]Array of purchasable 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 catalog (suppliers, 3PL inventory, etc.)
  const allProducts = [
    {
      image: "https://via.placeholder.com/300x300/FF0000/FFFFFF?text=PIPE",
      title: "Hooli XYZ Compression Device",
      productId: "hooli001",
      variantId: "white",
      price: "299.99",
      availableForSale: true,
      inventory: 150,
      inventoryTracked: true,
      productLink: "https://hooli-supply.com/products/xyz-compression",
      cursor: "eyJpZCI6Imhvb2xpMDAxIn0="
    },
    {
      image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=STORAGE",
      title: "Electronics Storage Box",
      productId: "storage001", 
      variantId: "crypto-box",
      price: "2000.00",
      availableForSale: true,
      inventory: 8,
      inventoryTracked: true,
      productLink: "https://demo-supplier.com/products/crypto-device",
      cursor: "eyJpZCI6InNldGVjMDAxIn0="
    },
    {
      image: "https://via.placeholder.com/300x300/4B0082/FFFFFF?text=HACK", 
      title: "Wireless Mouse - Blue",
      productId: "phreaks001",
      variantId: "blue-box",
      price: "500.00",
      availableForSale: true,
      inventory: 12,
      inventoryTracked: true,
      productLink: "https://demo-store.com/products/wireless-mouse",
      cursor: "eyJpZCI6Im1vdXNlMDAxIn0="
    }
  ];

  let filteredProducts = allProducts;

  // Filter by search term if provided
  if (searchEntry) {
    filteredProducts = allProducts.filter(product =>
      product.title.toLowerCase().includes(searchEntry.toLowerCase())
    );
  }

  // Simple 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 from channel platforms when creating product matches, ensuring accurate pricing and availability data for automated purchasing

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 purchase 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 catalog
  const products = {
    "hooli001": {
      white: {
        image: "https://via.placeholder.com/300x300/FF0000/FFFFFF?text=PIPE",
        title: "Hooli XYZ Compression Device - White",
        productId: "hooli001",
        variantId: "white", 
        price: "299.99",
        availableForSale: true,
        inventory: 150,
        inventoryTracked: true,
        productLink: "https://hooli-supply.com/products/xyz-compression"
      }
    },
    "storage001": {
      "crypto-box": {
        image: "https://via.placeholder.com/300x300/008080/FFFFFF?text=STORAGE",
        title: "Electronics Storage Box - Crypto Box",
        productId: "storage001",
        variantId: "crypto-box",
        price: "2000.00",
        availableForSale: true,
        inventory: 8,
        inventoryTracked: true,
        productLink: "https://demo-supplier.com/products/crypto-device"
      }
    }
  };

  const product = products[productId]?.[variantId || 'default'];
  
  if (!product) {
    return res.status(404).json({ error: "Product not found" });
  }

  return res.status(200).json({ product });
}

Create Purchase - /api/create-purchase

Purpose: Openship uses this endpoint to automatically create purchases on channel platforms when orders are received from connected shops, enabling automated order fulfillment workflow

HTTP Method: POST

Request Body:

FieldTypeDescription
platform{ domain: string; accessToken: string }Platform credentials
cartItemsCartItem[]Items to purchase
shipping?ShippingAddressShipping address
notes?stringOrder notes

Response Format:

FieldTypeDescription
purchaseIdstringUnique identifier for the purchase
orderNumberstringHuman-readable order number
totalPricestringTotal cost of the purchase
invoiceUrlstringURL to the purchase invoice
lineItemsLineItem[]Items included in the purchase
statusstringCurrent status of the purchase
// POST /api/create-purchase
export default async function handler(req, res) {
  const { platform, cartItems, shipping, notes } = req.body;

  // Verify access token
  if (platform.accessToken !== process.env.ACCESS_TOKEN) {
    return res.status(403).json({ error: "Access denied" });
  }

  try {
    // Generate unique purchase ID (in real app, use proper ID generation)
    const purchaseId = `PO-${Date.now()}`;
    const orderNumber = `#${purchaseId}`;

    // Calculate total price
    const totalPrice = cartItems.reduce((sum, item) => {
      return sum + (parseFloat(item.price) * item.quantity);
    }, 0).toFixed(2);

    // In a real implementation, you would:
    // 1. Create the purchase in your fulfillment system
    // 2. Send order to your supplier/3PL
    // 3. Update inventory levels
    // 4. Generate invoices/receipts

    // For demo purposes, let's simulate different responses based on products
    const hasCryptoEquipment = cartItems.some(item => item.variantId?.includes('crypto') || item.variantId?.includes('setec'));
    
    if (hasCryptoEquipment) {
      // Special handling for crypto/security equipment
      return res.status(200).json({
        purchaseId,
        orderNumber,
        totalPrice,
        invoiceUrl: `https://demo-supplier.com/invoices/${purchaseId}`,
        lineItems: cartItems.map(item => ({
          id: `line_${Date.now()}_${Math.random()}`,
          title: item.name || `Product ${item.variantId}`,
          quantity: item.quantity,
          variantId: item.variantId
        })),
        status: "pending_security_clearance" // Crypto equipment needs clearance
      });
    }

    // Regular purchase flow
    const processedLineItems = cartItems.map(item => ({
      id: `line_${Date.now()}_${Math.random()}`,
      title: item.name || `Product ${item.variantId}`,
      quantity: item.quantity,
      variantId: item.variantId
    }));

    // Simulate email notification to supplier
    console.log(`📧 Purchase Order Created: ${orderNumber}`);
    console.log(`📦 Items:`, processedLineItems);
    console.log(`🚚 Ship to: ${shipping?.firstName} ${shipping?.lastName}`);
    console.log(`💰 Total: $${totalPrice}`);

    return res.status(200).json({
      purchaseId,
      orderNumber,
      totalPrice,
      invoiceUrl: `https://your-fulfillment-portal.com/invoices/${purchaseId}`,
      lineItems: processedLineItems,
      status: "processing"
    });

  } catch (error) {
    console.error('Purchase creation failed:', error);
    return res.status(500).json({
      error: "Purchase creation failed",
      details: error.message
    });
  }
}

Create Webhook - /api/create-webhook

Purpose: Openship uses this endpoint to set up real-time webhooks that notify the system when purchases are fulfilled or cancelled on channel platforms, enabling automatic tracking updates to customers

HTTP Method: POST

Request Body:

FieldTypeDescription
platform{ domain: string; accessToken: string }Platform credentials
endpointstringWebhook URL
eventsstring[]Event types to subscribe to

Response Format:

FieldTypeDescription
webhooksWebhook[]Array of created webhook objects
// POST /api/create-webhook
export default async function handler(req, res) {
  const { platform, endpoint, events } = req.body;

  // Verify access token
  if (platform.accessToken !== process.env.ACCESS_TOKEN) {
    return res.status(403).json({ error: "Access denied" });
  }

  try {
    const webhooks = [];

    // Map Openship events to your system's events
    const eventMapping = {
      "TRACKING_CREATED": "fulfillment.shipped",
      "ORDER_CANCELLED": "purchase.cancelled",
      "ORDER_CHARGEBACKED": "purchase.disputed"
    };

    for (const event of events) {
      const internalEvent = eventMapping[event] || event;
      
      // In a real implementation, you would register these webhooks 
      // with your fulfillment system/3PL API
      const webhookId = `webhook_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
      
      console.log(`🔗 Registering webhook: ${internalEvent} -> ${endpoint}`);
      
      // Simulate webhook registration
      webhooks.push({
        id: webhookId,
        endpoint: {
          callbackUrl: endpoint
        },
        events: [internalEvent],
        active: true,
        created_at: new Date().toISOString()
      });
    }

    return res.status(200).json({ webhooks });

  } catch (error) {
    console.error('Webhook creation failed:', error);
    return res.status(500).json({
      error: "Webhook creation failed",
      details: error.message
    });
  }
}

Webhook Event Handlers

Your custom channel should implement webhook handlers to notify Openship of fulfillment events:

Fulfillment Tracking Handler - /api/webhook/tracking-created

Purpose: Openship uses this webhook to receive tracking information when purchases are shipped from channel platforms, automatically forwarding tracking details to customers and updating order status

// POST /api/webhook/tracking-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 fulfillment event to Openship format
  const fulfillment = {
    id: event.fulfillment_id,
    orderId: event.order_id,
    status: "success",
    trackingCompany: event.shipping_carrier,
    trackingNumber: event.tracking_number,
    trackingUrl: event.tracking_url,
    purchaseId: event.purchase_id,
    lineItems: event.shipped_items.map(item => ({
      id: item.line_item_id,
      title: item.product_name,
      quantity: item.quantity_shipped,
      variantId: item.variant_id,
      productId: item.product_id
    })),
    createdAt: event.shipped_at,
    updatedAt: event.updated_at
  };

  return res.status(200).json({ fulfillment, type: "fulfillment_created" });
}

function verifyWebhookSignature(headers) {
  // Implement your webhook signature verification here
  // This could be HMAC, JWT, or other verification method
  return true; // Simplified for demo
}

Testing Your Custom Channel

We've built a demo channel integration right into this documentation that you can use to test the complete Openship workflow before implementing your own custom channel.

Step 1: Create a Demo Channel Platform

  1. In your Openship dashboard, navigate to Platform > Channels
  2. Click "Add Platform"
  3. In the "Create Channel Platform" dialog:
    • Select "Demo" from the Platform dropdown
    • The Name field will auto-fill with "Demo Channel"
    • Click "Create Platform"

This creates a platform that points to our demo channel endpoints at https://docs.openship.org/api/demo/channel/*

Step 2: Connect Your Demo Channel

  1. After creating the platform, click "Create Channel"
  2. In the "Create Channel" dialog:
    • Platform: Select "Demo Channel Platform"
    • Name: "Demo" (or any name you prefer)
    • Domain: "local"
    • Access Token: "demo_token"
  3. Click "Create Channel"

Step 3: Test the Demo Integration

Once connected, you can test the complete workflow:

  • Browse products available for purchase from our demo supplier
  • Test product matching between shops and channels
  • Create demo purchases and track their status
  • Experience automated order fulfillment

Step 4: Replace with Your Implementation

When you're ready to use your custom channel:

  1. Go to your Demo Channel Platform settings
  2. Replace the demo URLs with your actual implementation:
# Replace these demo URLs:
https://docs.openship.org/api/demo/channel/search-products
https://docs.openship.org/api/demo/channel/get-product  
https://docs.openship.org/api/demo/channel/create-purchase
https://docs.openship.org/api/demo/channel/create-webhook
# ... and other endpoints

# With your URLs:
https://your-fulfillment-api.com/api/search-products
https://your-fulfillment-api.com/api/get-product
https://your-fulfillment-api.com/api/create-purchase
https://your-fulfillment-api.com/api/create-webhook
# ... and so on

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