Create Custom Channel

Learn how to create custom HTTP-based channel integrations

Overview

Instead of a compiled Shopify/Openfront channel adapter, a ChannelPlatform can select custom HTTP endpoints that Openship calls for fulfillment operations. The endpoint must implement the documented request/response contract and remain responsible for its own provider-specific behavior.

Openship executes database-selected URLs. The docs endpoints are POST-only in-memory examples, not a supplier or 3PL.

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 purchase state
// 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.

On this page