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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
searchEntry | string | - |
after? | string | - |
Response Format:
| Field | Type | Description |
|---|---|---|
products | Product[] | 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
productId | string | - |
variantId? | string | - |
Response Format:
| Field | Type | Description |
|---|---|---|
product | Product | Single 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
searchEntry | string | - |
after? | string | - |
Response Format:
| Field | Type | Description |
|---|---|---|
orders | Order[] | 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
productId | string | - |
variantId | string | - |
inventory? | number | - |
price? | string | - |
Response Format:
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the update operation succeeded |
results | object[] | 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
endpoint | string | - |
events | string[] | - |
Response Format:
| Field | Type | Description |
|---|---|---|
webhooks | Webhook[] | Array of created webhook objects |
webhookId | string | ID 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
callbackUrl | string | - |
Response Format:
| Field | Type | Description |
|---|---|---|
authUrl | string | OAuth 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
code | string | - |
shop | string | - |
state | string | - |
Response Format:
| Field | Type | Description |
|---|---|---|
accessToken | string | OAuth access token for API authentication |
domain | string | Shop 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
event | any | - |
headers | Record<string, string> | - |
Response Format:
| Field | Type | Description |
|---|---|---|
order | Order | Processed 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
event | any | - |
headers | Record<string, string> | - |
Response Format:
| Field | Type | Description |
|---|---|---|
order | Order | Cancelled 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
event | any | - |
headers | Record<string, string> | - |
Response Format:
| Field | Type | Description |
|---|---|---|
fulfillment | Fulfillment | Tracking/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:
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:
- 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
// ...
};- Select your template from the dropdown
- 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
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.