Create Channel Integration

Learn how to create custom channel integrations for purchasing and fulfillment

Overview

Channels are fulfillment platforms where Openship can automatically purchase products to fulfill orders. Unlike shops (which sell products), channels provide products for purchase when orders need to be fulfilled. Openship has a built-in integration for Shopify so new integrations can be built by following the same pattern.

Shopify Channel Integration Reference

The Shopify channel integration (/features/integrations/channel/shopify.ts) demonstrates all required functions for a complete channel integration. This guide will explain how each function works and how to implement it for your own channel integration. Each function below has 3 parts, what Openship sends to the function, how the function works, and what Openship expects to be returned.

Function Reference

Product Search - searchProductsFunction

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

Request Body:

PropTypeDefault
platform{ domain: string; accessToken: string }-
searchEntrystring-
after?string-

Response Format:

FieldTypeDescription
productsProduct[]Array of purchasable product objects
pageInfo{ hasNextPage: boolean; endCursor: string }Pagination information
export async function searchProductsFunction({ 
  platform, 
  searchEntry, 
  after 
}: { 
  platform: { domain: string; accessToken: string }; 
  searchEntry: string; 
  after?: string; 
}) {
  const shopifyClient = new GraphQLClient(
    `https://${platform.domain}/admin/api/graphql.json`,
    {
      headers: {
        "X-Shopify-Access-Token": platform.accessToken,
      },
    }
  );

  const gqlQuery = gql`
    query SearchProducts($query: String, $after: String) {
      productVariants(first: 15, query: $query, after: $after) {
        edges {
          node {
            id
            availableForSale
            image { originalSrc }
            price
            title
            product {
              id
              handle
              title
              images(first: 1) {
                edges {
                  node { originalSrc }
                }
              }
            }
            inventoryQuantity
            inventoryPolicy
          }
          cursor
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  `;

  const { productVariants } = await shopifyClient.request(gqlQuery, {
    query: searchEntry,
    after,
  });

  if (productVariants.edges.length < 1) {
    throw new Error("No products found from Shopify channel");
  }

  const products = productVariants.edges.map(({ node, cursor }) => ({
    image: node.image?.originalSrc || node.product.images.edges[0]?.node.originalSrc,
    title: `${node.product.title} - ${node.title}`,
    productId: node.product.id.split("/").pop(),
    variantId: node.id.split("/").pop(),
    price: node.price,
    availableForSale: node.availableForSale,
    inventory: node.inventoryQuantity,
    inventoryTracked: node.inventoryPolicy !== "deny",
    productLink: `https://${platform.domain}/products/${node.product.handle}`,
    cursor,
  }));

  return { 
    products, 
    pageInfo: productVariants.pageInfo 
  };
}

Get Single Product - getProductFunction

Purpose: Openship uses this function to fetch detailed product information from channel platforms when creating product matches, ensuring accurate pricing and availability data for automated purchasing

Request Body:

PropTypeDefault
platform{ domain: string; accessToken: string }-
productIdstring-
variantId?string-

Response Format:

FieldTypeDescription
productProductSingle product object with purchase details
export async function getProductFunction({
  platform,
  productId,
  variantId,
}: {
  platform: { domain: string; accessToken: string };
  productId: string;
  variantId?: string;
}) {
  const shopifyClient = new GraphQLClient(
    `https://${platform.domain}/admin/api/graphql.json`,
    {
      headers: {
        "X-Shopify-Access-Token": platform.accessToken,
      },
    }
  );

  const gqlQuery = gql`
    query GetProduct($variantId: ID!) {
      productVariant(id: $variantId) {
        id
        availableForSale
        image { originalSrc }
        price
        title
        product {
          id
          handle
          title
          images(first: 1) {
            edges {
              node { originalSrc }
            }
          }
        }
        inventoryQuantity
        inventoryPolicy
      }
    }
  `;

  const fullVariantId = `gid://shopify/ProductVariant/${variantId}`;
  const { productVariant } = await shopifyClient.request(gqlQuery, {
    variantId: fullVariantId,
  });

  if (!productVariant) {
    throw new Error("Product not found from Shopify channel");
  }

  const product = {
    image: productVariant.image?.originalSrc || 
           productVariant.product.images.edges[0]?.node.originalSrc,
    title: `${productVariant.product.title} - ${productVariant.title}`,
    productId: productVariant.product.id.split("/").pop(),
    variantId: productVariant.id.split("/").pop(),
    price: productVariant.price,
    availableForSale: productVariant.availableForSale,
    inventory: productVariant.inventoryQuantity,
    inventoryTracked: productVariant.inventoryPolicy !== "deny",
    productLink: `https://${platform.domain}/products/${productVariant.product.handle}`,
  };

  return { product };
}

Create Purchase - createPurchaseFunction

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

Request Body:

PropTypeDefault
platform{ domain: string; accessToken: string }-
cartItemsCartItem[]-
shipping?ShippingAddress-
notes?string-

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
export async function createPurchaseFunction({
  platform,
  cartItems,
  shipping,
  notes,
}: {
  platform: { domain: string; accessToken: string };
  cartItems: Array<{
    variantId: string;
    quantity: number;
    price?: string;
  }>;
  shipping?: {
    firstName: string;
    lastName: string;
    address1: string;
    address2?: string;
    city: string;
    province: string;
    country: string;
    zip: string;
    phone?: string;
  };
  notes?: string;
}) {
  const shopifyClient = new GraphQLClient(
    `https://${platform.domain}/admin/api/graphql.json`,
    {
      headers: {
        "X-Shopify-Access-Token": platform.accessToken,
      },
    }
  );

  // Create draft order
  const mutation = gql`
    mutation CreateDraftOrder($input: DraftOrderInput!) {
      draftOrderCreate(input: $input) {
        draftOrder {
          id
          name
          invoiceUrl
          totalPrice
          lineItems(first: 50) {
            edges {
              node {
                id
                title
                quantity
                originalUnitPrice
                variant {
                  id
                  title
                  product {
                    id
                    title
                  }
                }
              }
            }
          }
        }
        userErrors {
          field
          message
        }
      }
    }
  `;

  const lineItems = cartItems.map(item => ({
    variantId: `gid://shopify/ProductVariant/${item.variantId}`,
    quantity: item.quantity,
    originalUnitPrice: item.price,
  }));

  const input: any = {
    lineItems,
    note: notes,
  };

  if (shipping) {
    input.shippingAddress = {
      firstName: shipping.firstName,
      lastName: shipping.lastName,
      address1: shipping.address1,
      address2: shipping.address2,
      city: shipping.city,
      province: shipping.province,
      country: shipping.country,
      zip: shipping.zip,
      phone: shipping.phone,
    };
  }

  const result = await shopifyClient.request(mutation, { input });

  if (result.draftOrderCreate.userErrors.length > 0) {
    throw new Error(`Failed to create purchase: ${result.draftOrderCreate.userErrors.map(e => e.message).join(', ')}`);
  }

  const draftOrder = result.draftOrderCreate.draftOrder;

  // Complete the draft order
  const completeMutation = gql`
    mutation CompleteDraftOrder($id: ID!) {
      draftOrderComplete(id: $id) {
        draftOrder {
          order {
            id
            name
            totalPrice
            lineItems(first: 50) {
              edges {
                node {
                  id
                  title
                  quantity
                  variant { id }
                }
              }
            }
          }
        }
        userErrors {
          field
          message
        }
      }
    }
  `;

  const completeResult = await shopifyClient.request(completeMutation, {
    id: draftOrder.id,
  });

  if (completeResult.draftOrderComplete.userErrors.length > 0) {
    throw new Error(`Failed to complete purchase: ${completeResult.draftOrderComplete.userErrors.map(e => e.message).join(', ')}`);
  }

  const order = completeResult.draftOrderComplete.draftOrder.order;

  return {
    purchaseId: order.id.split("/").pop(),
    orderNumber: order.name,
    totalPrice: order.totalPrice,
    invoiceUrl: draftOrder.invoiceUrl,
    lineItems: order.lineItems.edges.map(({ node }) => ({
      id: node.id.split("/").pop(),
      title: node.title,
      quantity: node.quantity,
      variantId: node.variant.id.split("/").pop(),
    })),
    status: "pending",
  };
}

Create Webhook

Purpose: Openship uses this function 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

Request Body:

PropTypeDefault
platform{ domain: string; accessToken: string }-
endpointstring-
eventsstring[]-

Response Format:

FieldTypeDescription
webhooksWebhook[]Array of created webhook objects
export async function createWebhookFunction({
  platform,
  endpoint,
  events,
}: {
  platform: { domain: string; accessToken: string };
  endpoint: string;
  events: string[];
}) {
  const mapTopic = {
    ORDER_CREATED: "ORDERS_CREATE",
    ORDER_CANCELLED: "ORDERS_CANCELLED",
    ORDER_CHARGEBACKED: "DISPUTES_CREATE",
    TRACKING_CREATED: "FULFILLMENTS_CREATE",
  };

  const shopifyClient = new GraphQLClient(
    `https://${platform.domain}/admin/api/graphql.json`,
    {
      headers: {
        "X-Shopify-Access-Token": platform.accessToken,
      },
    }
  );

  const webhooks = [];

  for (const event of events) {
    const shopifyTopic = mapTopic[event] || event;
    const mutation = gql`
      mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {
        webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {
          webhookSubscription {
            id
            endpoint {
              __typename
              ... on WebhookHttpEndpoint {
                callbackUrl
              }
            }
          }
          userErrors {
            field
            message
          }
        }
      }
    `;

    const result = await shopifyClient.request(mutation, {
      topic: shopifyTopic.toUpperCase(),
      webhookSubscription: {
        callbackUrl: endpoint,
        format: "JSON",
      },
    });

    webhooks.push(result.webhookSubscriptionCreate.webhookSubscription);
  }

  return { webhooks };
}

OAuth Integration

If you have a Shopify app (or app on any platform), you can implement OAuth functions to allow users to install your app directly instead of manually retrieving access tokens. This provides a smoother user experience where users can authorize your integration through the standard app installation flow.

OAuth Authorization

Purpose: Openship uses this function to generate secure OAuth authorization URLs that allow users to safely connect their channel platforms to Openship for automated purchasing

Request Body:

PropTypeDefault
platform{ domain: string; accessToken: string }-
callbackUrlstring-

Response Format:

FieldTypeDescription
authUrlstringOAuth authorization URL for user redirection
export async function oAuthFunction({
  platform,
  callbackUrl,
}: {
  platform: { domain: string; accessToken: string };
  callbackUrl: string;
}) {
  const scopes = "read_products,write_products,read_orders,write_orders,read_inventory,write_inventory";
  const shopifyAuthUrl = `https://${platform.domain}/admin/oauth/authorize?client_id=${process.env.SHOPIFY_APP_KEY}&scope=${scopes}&redirect_uri=${callbackUrl}&state=${Math.random().toString(36).substring(7)}`;
  
  return { authUrl: shopifyAuthUrl };
}

OAuth Token Exchange

Purpose: Openship uses this function to complete the OAuth flow by exchanging temporary authorization codes for permanent access tokens, establishing a secure connection to the user's channel platform

Request Body:

PropTypeDefault
platform{ domain: string; accessToken: string }-
codestring-
shopstring-
statestring-

Response Format:

FieldTypeDescription
accessTokenstringOAuth access token for API authentication
domainstringChannel domain for API requests
export async function oAuthCallbackFunction({
  platform,
  code,
  shop,
  state,
}: {
  platform: { domain: string; accessToken: string };
  code: string;
  shop: string;
  state: string;
}) {
  const tokenUrl = `https://${shop}/admin/oauth/access_token`;
  
  const response = await fetch(tokenUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      client_id: process.env.SHOPIFY_APP_KEY,
      client_secret: process.env.SHOPIFY_APP_SECRET,
      code,
    }),
  });

  if (!response.ok) {
    throw new Error("Failed to exchange OAuth code for access token");
  }

  const { access_token } = await response.json();
  
  return { 
    accessToken: access_token,
    domain: shop,
  };
}

Webhook Event Handlers

Channel integrations must implement webhook handlers to process real-time events from the platform.

Fulfillment Tracking Handler - createTrackingWebhookHandler

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

Request Body:

PropTypeDefault
platform{ domain: string; accessToken: string }-
eventany-
headersRecord<string, string>-

Response Format:

FieldTypeDescription
fulfillmentFulfillmentTracking/fulfillment details
type"fulfillment_created"Event type identifier
export async function createTrackingWebhookHandler({
  platform,
  event,
  headers,
}: {
  platform: { domain: string; accessToken: string };
  event: any;
  headers: Record<string, string>;
}) {
  // Verify webhook authenticity
  const hmac = headers["x-shopify-hmac-sha256"];
  if (!hmac) {
    throw new Error("Missing webhook HMAC");
  }

  const fulfillment = {
    id: event.id,
    orderId: event.order_id,
    status: event.status,
    trackingCompany: event.tracking_company,
    trackingNumber: event.tracking_number,
    trackingUrl: event.tracking_url,
    purchaseId: event.order_id?.toString(),
    lineItems: event.line_items.map((item) => ({
      id: item.id,
      title: item.title,
      quantity: item.quantity,
      variantId: item.variant_id,
      productId: item.product_id,
    })),
    createdAt: event.created_at,
    updatedAt: event.updated_at,
  };

  return { fulfillment, type: "fulfillment_created" };
}

Purchase Cancellation Handler - cancelPurchaseWebhookHandler

Purpose: Openship uses this webhook handler to process purchase cancellation events from channel platforms, automatically handling refunds and updating order status across connected platforms

Request Body:

PropTypeDefault
platform{ domain: string; accessToken: string }-
eventany-
headersRecord<string, string>-

Response Format:

FieldTypeDescription
orderOrderCancelled purchase object with cancellation details
type"purchase_cancelled"Event type identifier
export async function cancelPurchaseWebhookHandler({
  platform,
  event,
  headers,
}: {
  platform: { domain: string; accessToken: string };
  event: any;
  headers: Record<string, string>;
}) {
  // Verify webhook authenticity
  const hmac = headers["x-shopify-hmac-sha256"];
  if (!hmac) {
    throw new Error("Missing webhook HMAC");
  }

  const order = {
    id: event.id,
    name: event.name,
    cancelReason: event.cancel_reason,
    cancelledAt: event.cancelled_at,
    refund: event.refunds?.[0] || null,
    lineItems: event.line_items.map((item) => ({
      id: item.id,
      title: item.title,
      quantity: item.quantity,
      variantId: item.variant_id,
      productId: item.product_id,
    })),
  };

  return { order, type: "purchase_cancelled" };
}

Adding Your Channel Integration to Openship

Once you've implemented your channel integration functions, you need to create a channel platform in Openship. Here's exactly what you'll see in the Openship interface:

1. Navigate to Channel Platforms

In Openship, go to the Channels page where you'll see the platform management interface:

Platforms

2. Create Channel Platform Dialog

Click the "Create platform to get started" button to open the platform creation dialog:

Create Channel Platform

Create a platform based on an existing template

3. Platform Template Selection

When you click the dropdown, you'll see the available channel templates:

Templates
Amazon
eBay
Etsy
Demo
Facebook (soon)
Google (soon)
Walmart (soon)

Custom
Start from scratch...

4. For Built-in Integration - Select Template

If you created a built-in integration file (e.g., myplatform.ts in /features/integrations/channel/), you would:

  1. Add your template to the channelAdapters list in CreatePlatform.tsx:
const channelAdapters = {
  amazon: "amazon",
  ebay: "ebay", 
  etsy: "etsy",
  demo: "demo", // Demo integration for testing
  myplatform: "myplatform", // Add your integration here
  // ...
};
  1. Select your template from the dropdown
  2. Fill in the basic fields: Name, App Key, App Secret (if needed)

5. For Custom HTTP Integration - Select "Start from scratch..."

If you're using HTTP endpoints, select "Start from scratch..." to see all the function fields:

When you select "Start from scratch...", you'll see form fields where you can enter your custom HTTP endpoints for each function.

6. After Creating Platform

Once you create the platform, it appears in the platforms list:

Platforms

MY CHANNEL

Actions

7. Create Channels Using Your Platform

Now you can click "Create Channel" to create individual channel instances that use your platform template. Each channel will use the same integration logic but with different credentials (domain, access tokens, etc.).

The platform system ensures your integration functions are reusable across multiple channel instances while maintaining clean separation between template logic and instance-specific configurations.