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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
searchEntry | string | - |
after? | string | - |
Response Format:
| Field | Type | Description |
|---|---|---|
products | Product[] | 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
productId | string | - |
variantId? | string | - |
Response Format:
| Field | Type | Description |
|---|---|---|
product | Product | Single 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
cartItems | CartItem[] | - |
shipping? | ShippingAddress | - |
notes? | string | - |
Response Format:
| Field | Type | Description |
|---|---|---|
purchaseId | string | Unique identifier for the purchase |
orderNumber | string | Human-readable order number |
totalPrice | string | Total cost of the purchase |
invoiceUrl | string | URL to the purchase invoice |
lineItems | LineItem[] | Items included in the purchase |
status | string | Current 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
endpoint | string | - |
events | string[] | - |
Response Format:
| Field | Type | Description |
|---|---|---|
webhooks | Webhook[] | 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:
| 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 channel 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 | Channel 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:
| 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 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:
| Prop | Type | Default |
|---|---|---|
platform | { domain: string; accessToken: string } | - |
event | any | - |
headers | Record<string, string> | - |
Response Format:
| Field | Type | Description |
|---|---|---|
order | Order | Cancelled 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:
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:
- 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
// ...
};- 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 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.