Create Shop Integration

Learn how to create custom shop integrations

Overview

Shops are the places where your customers are already placing orders like marketplaces or e-commerce platforms. Openship has a built-in integration for Shopify so new integrations can be built by following the same pattern.

Shopify Shop Integration Reference

The Shopify shop integration (/features/integrations/shop/shopify.ts) demonstrates all required functions for a complete shop integration. This guide will explain how each function works and how to implement it for your own shop 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 allow users to search and browse products from connected shop platforms when creating product matches or exploring available inventory

Request Body:

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

Response Format:

FieldTypeDescription
productsProduct[]Array of 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,
  });

  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 when users select specific products for matching, viewing current inventory levels, or syncing product data between platforms

Request Body:

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

Response Format:

FieldTypeDescription
productProductSingle product object with full 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");
  }

  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}/admin/products/${productVariant.product.id.split("/").pop()}/variants/${productVariant.id.split("/").pop()}`,
  };

  return { product };
}

Search Orders - searchOrdersFunction

Purpose: Openship uses this function to retrieve orders from connected shop platforms, allowing the system to automatically process incoming orders and trigger corresponding purchases on matched channel platforms

Request Body:

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

Response Format:

FieldTypeDescription
ordersOrder[]Array of order objects with customer and line item details
pageInfo{ hasNextPage: boolean; endCursor: string }Pagination information
export async function searchOrdersFunction({
  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 SearchOrders($query: String, $after: String) {
      orders(first: 15, query: $query, after: $after) {
        edges {
          node {
            id
            name
            email
            createdAt
            updatedAt
            displayFulfillmentStatus
            displayFinancialStatus
            totalPriceSet {
              presentmentMoney {
                amount
                currencyCode
              }
            }
            shippingAddress {
              firstName
              lastName
              address1
              address2
              city
              province
              zip
              country
            }
            lineItems(first: 10) {
              edges {
                node {
                  id
                  title
                  quantity
                  image { originalSrc }
                  variant {
                    id
                    title
                    price
                    product {
                      id
                      title
                      handle
                    }
                  }
                }
              }
            }
          }
          cursor
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  `;

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

  const formattedOrders = orders.edges.map(({ node, cursor }) => ({
    orderId: node.id.split("/").pop(),
    orderName: node.name,
    link: `https://${platform.domain}/admin/orders/${node.id.split("/").pop()}`,
    date: new Date(node.createdAt).toLocaleDateString(),
    firstName: node.shippingAddress?.firstName || "",
    lastName: node.shippingAddress?.lastName || "",
    streetAddress1: node.shippingAddress?.address1 || "",
    streetAddress2: node.shippingAddress?.address2 || "",
    city: node.shippingAddress?.city || "",
    state: node.shippingAddress?.province || "",
    zip: node.shippingAddress?.zip || "",
    country: node.shippingAddress?.country || "",
    email: node.email || "",
    fulfillmentStatus: node.displayFulfillmentStatus,
    financialStatus: node.displayFinancialStatus,
    totalPrice: node.totalPriceSet.presentmentMoney.amount,
    currency: node.totalPriceSet.presentmentMoney.currencyCode,
    lineItems: node.lineItems.edges.map(({ node: lineItem }) => ({
      lineItemId: lineItem.id.split("/").pop(),
      name: lineItem.title,
      quantity: lineItem.quantity,
      image: lineItem.image?.originalSrc || "",
      price: lineItem.variant?.price || "0",
      variantId: lineItem.variant?.id.split("/").pop(),
      productId: lineItem.variant?.product.id.split("/").pop(),
    })),
    cursor,
  }));

  return { orders: formattedOrders, pageInfo: orders.pageInfo };
}

Update Product - updateProductFunction

Purpose: Openship uses this function to automatically sync inventory levels and pricing between matched products across different platforms, ensuring accurate stock counts and competitive pricing

Request Body:

PropTypeDefault
platform{ domain: string; accessToken: string }-
productIdstring-
variantIdstring-
inventory?number-
price?string-

Response Format:

FieldTypeDescription
successbooleanWhether the update operation succeeded
resultsobject[]Array of mutation results from the platform
export async function updateProductFunction({
  platform,
  productId,
  variantId,
  inventory,
  price,
}: {
  platform: { domain: string; accessToken: string }; 
  productId: string;
  variantId: string;
  inventory?: number;
  price?: string;
}) {
  const shopifyClient = new GraphQLClient(
    `https://${platform.domain}/admin/api/graphql.json`,
    {
      headers: {
        "X-Shopify-Access-Token": platform.accessToken,
      },
    }
  );

  const mutations = [];

  // Update price if provided
  if (price !== undefined) {
    const updatePriceMutation = gql`
      mutation UpdateProductVariantPrice($input: ProductVariantInput!) {
        productVariantUpdate(input: $input) {
          productVariant {
            id
            price
          }
          userErrors {
            field
            message
          }
        }
      }
    `;

    mutations.push(
      shopifyClient.request(updatePriceMutation, {
        input: {
          id: `gid://shopify/ProductVariant/${variantId}`,
          price: price,
        },
      })
    );
  }

  // Update inventory if provided
  if (inventory !== undefined) {
    // Get inventory item ID and location
    const getVariantQuery = gql`
      query GetVariantWithInventory($id: ID!) {
        productVariant(id: $id) {
          inventoryQuantity
          inventoryItem { id }
        }
      }
    `;

    const variantData = await shopifyClient.request(getVariantQuery, {
      id: `gid://shopify/ProductVariant/${variantId}`,
    });

    if (!variantData.productVariant?.inventoryItem?.id) {
      throw new Error("Unable to find inventory item for variant");
    }

    // Get first location
    const getLocationsQuery = gql`
      query GetLocations {
        locations(first: 1) {
          edges {
            node {
              id
              name
            }
          }
        }
      }
    `;

    const locationsData = await shopifyClient.request(getLocationsQuery);
    const location = locationsData.locations.edges[0]?.node;

    if (!location) {
      throw new Error("No locations found for shop");
    }

    // Update inventory
    const updateInventoryMutation = gql`
      mutation InventoryAdjustQuantities($input: InventoryAdjustQuantitiesInput!) {
        inventoryAdjustQuantities(input: $input) {
          inventoryAdjustmentGroup { id }
          userErrors {
            field
            message
          }
        }
      }
    `;

    mutations.push(
      shopifyClient.request(updateInventoryMutation, {
        input: {
          reason: "correction",
          name: "available", 
          changes: [{
            inventoryItemId: variantData.productVariant.inventoryItem.id,
            locationId: location.id,
            delta: inventory
          }]
        }
      })
    );
  }

  const results = await Promise.all(mutations);
  return { success: true, results };
}

Create Webhook

Purpose: Openship uses this function to set up real-time webhooks that automatically notify the system when orders are created, cancelled, or fulfilled on your shop platform, enabling instant order processing and inventory updates

Request Body:

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

Response Format:

FieldTypeDescription
webhooksWebhook[]Array of created webhook objects
webhookIdstringID of the first created webhook
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,
      webhookSubscription: {
        callbackUrl: endpoint,
        format: "JSON",
      },
    });

    if (result.webhookSubscriptionCreate.userErrors.length > 0) {
      throw new Error(`Error creating webhook: ${result.webhookSubscriptionCreate.userErrors[0].message}`);
    }

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

  const webhookId = webhooks[0]?.id?.split("/").pop();
  return { webhooks, webhookId };
}

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 shop platforms to Openship without sharing sensitive credentials

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 shop platform

Request Body:

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

Response Format:

FieldTypeDescription
accessTokenstringOAuth access token for API authentication
domainstringShop 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

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

Order Creation Handler - createOrderWebhookHandler

Purpose: Openship uses this webhook handler to instantly receive and process new orders from your shop platform, automatically triggering the workflow to create corresponding purchases on matched channel platforms

Request Body:

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

Response Format:

FieldTypeDescription
orderOrderProcessed order object
type"order_created"Event type identifier
export async function createOrderWebhookHandler({
  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,
    email: event.email,
    financialStatus: event.financial_status,
    fulfillmentStatus: event.fulfillment_status,
    totalPrice: event.total_price,
    currency: event.currency,
    lineItems: event.line_items.map((item) => ({
      id: item.id,
      title: item.title,
      quantity: item.quantity,
      price: item.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?.address1 || "",
      address2: event.shipping_address?.address2 || "",
      city: event.shipping_address?.city || "",
      province: event.shipping_address?.province || "",
      country: event.shipping_address?.country || "",
      zip: event.shipping_address?.zip || "",
    },
    createdAt: event.created_at,
    updatedAt: event.updated_at,
  };

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

Order Cancellation Handler - cancelOrderWebhookHandler

Purpose: Openship uses this webhook handler to process order cancellation events from your shop platform, automatically handling refunds and updating inventory across connected platforms

Request Body:

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

Response Format:

FieldTypeDescription
orderOrderCancelled order object with cancellation details
type"order_cancelled"Event type identifier
export async function cancelOrderWebhookHandler({
  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: "order_cancelled" };
}

Tracking/Fulfillment Handler - addTrackingFunction

Purpose: Openship uses this function to add tracking information to fulfilled orders, automatically updating customers with shipping details and tracking numbers from your connected platforms

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 addTrackingFunction({
  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,
    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" };
}

Adding Your Shop Integration to Openship

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

1. Navigate to Shop Platforms

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

Platforms

2. Create Shop Platform Dialog

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

Create Shop Platform

Create a platform based on an existing template

3. Platform Template Selection

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

Templates
Shopify
Bigcommerce
Woocommerce
Demo
Medusa (soon)
Magento (soon)
Stripe (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/shop/), you would:

  1. Add your template to the shopAdapters list in CreatePlatform.tsx:
const shopAdapters = {
  shopify: "shopify",
  bigcommerce: "bigcommerce", 
  woocommerce: "woocommerce",
  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 PLATFORM

Actions

7. Create Shops Using Your Platform

Now you can click "Create Shop" to create individual shop instances that use your platform template. Each shop 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 shop instances while maintaining clean separation between template logic and instance-specific configurations.