How-to guides
/
Create a custom channel

Create a custom channel

Openship supports some fulfillment channels out of the box like Shopify, but also works with custom channels.

Custom channels, like all channels, consist of 4 endpoints:

Search products endpoint

1. Let's start with a function that returns an array of products. You can fetch these products from a CMS, Google Sheet, or any platform. Each product in the array will have 5 values:

  • image:
    string
    product image
  • title:
    string
    product title
  • productId:
    string
    product identifier
  • variantId:
    string
    variant product identifier, if none send 0
  • price:
    string
    product price
  • availableForSale:
    boolean
    true if item is available
api/search-products.js

export default async (req, res) => {
const allProducts = [
{
image: "https://example.com/book.jpeg",
title: "Pocket Book",
productId: "887262",
variantId: "0",
price: "9.99",
availableForSale: true,
},
];
return res
.status(200)
.json({ products: allProducts });
}


2. Openship will send some attributes to the endpoint:

  • accessToken: access token to verify the request
  • searchEntry: search products based on this value
  • productId: product identifier
  • variantId: product variant identifier

Let's put these values to use.

api/search-products.js

export default async (req, res) => {
const {
accessToken,
searchEntry,
productId,
variantId,
} = req.query;
const allProducts = [
{
image: "https://example.com/book.jpeg",
title: "Pocket Book",
productId: "887262",
variantId: "0",
price: "9.99",
availableForSale: true,
},
];
return res
.status(200)
.json({ products: allProducts });
}


3. First, we check this access token against our .env file to make the user has been granted access.

api/search-products.js

export default async (req, res) => {
const {
accessToken,
searchEntry,
productId,
variantId,
} = req.query;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
const allProducts = [
{
image: "https://example.com/book.jpeg",
title: "Pocket Book",
productId: "887262",
variantId: "0",
price: "9.99",
availableForSale: true,
},
];
return res
.status(200)
.json({ products: allProducts });
};


4. Next, let's check if the search entry parameter exists and if so, filter our products based on that value.

api/search-products.js

export default async (req, res) => {
const {
accessToken,
searchEntry,
productId,
variantId,
} = req.query;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
const allProducts = [
{
image: "https://example.com/book.jpeg",
title: "Pocket Book",
productId: "887262",
variantId: "0",
price: "9.99",
availableForSale: true,
},
];
if (searchEntry) {
const products = allProducts.filter((product) =>
product.title.includes(searchEntry)
);
return res.status(200).json({ products });
}
return res
.status(200)
.json({ products: allProducts });
};


5. Next, let's check if the productId and variantId exist. If so, filter allProducts based on these values. If no products are found after filtering, return an error.

api/search-products.js

export default async function handler(req, res) {
const {
accessToken,
searchEntry,
productId,
variantId,
} = req.query;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
const allProducts = [
{
image: "https://example.com/book.jpeg",
title: "Pocket Book",
productId: "887262",
variantId: "0",
price: "9.99",
availableForSale: true,
},
];
if (searchEntry) {
const products = allProducts.filter((product) =>
product.title.includes(searchEntry)
);
return res.status(200).json({ products });
}
if (productId && variantId) {
const products = allProducts.filter(
(product) =>
product.productId === productId &&
product.variantId === variantId
);
if (products.length > 0) {
return res.status(200).json({ products });
}
return res
.status(400)
.json({ error: "Not found" });
}
return res
.status(200)
.json({ products: allProducts });
}

Search products endpoint

1. Let's start with a function that returns an array of products. You can fetch these products from a CMS, Google Sheet, or any platform. Each product in the array will have 5 values:

  • image:
    string
    product image
  • title:
    string
    product title
  • productId:
    string
    product identifier
  • variantId:
    string
    variant product identifier, if none send 0
  • price:
    string
    product price
  • availableForSale:
    boolean
    true if item is available

2. Openship will send some attributes to the endpoint:

  • accessToken: access token to verify the request
  • searchEntry: search products based on this value
  • productId: product identifier
  • variantId: product variant identifier

Let's put these values to use.


3. First, we check this access token against our .env file to make the user has been granted access.


4. Next, let's check if the search entry parameter exists and if so, filter our products based on that value.


5. Next, let's check if the productId and variantId exist. If so, filter allProducts based on these values. If no products are found after filtering, return an error.

api/search-products.js
ExpandClose

export default async (req, res) => {
const allProducts = [
{
image: "https://example.com/book.jpeg",
title: "Pocket Book",
productId: "887262",
variantId: "0",
price: "9.99",
availableForSale: true,
},
];
return res
.status(200)
.json({ products: allProducts });
}

Create purchase endpoint

1. Openship will use this create purchase function to create purchases on the channel. Openship sends the following attributes to the endpoint:

  • accessToken: access token to verify the request
  • email: email connected to the Openship account
  • metafields: channel-specific fields
  • cartItems: products that need to be fulfilled
    • productId: product identifier
    • variantId: product variant identifier
    • name: product title
    • quantity: quantity of product to ship
    • price: product cost
  • address: shipping address
    • first_name
    • last_name
    • streetAddress1
    • streetAddress2
    • city
    • state
    • zip
    • state
    • country

Let's put these values to use.

api/create-purchase.js

export default async (req, res) => {
const {
accessToken,
email,
metafields: {
[key]: value
},
cartItems: [
{
productId,
variantId,
name,
quantity,
price,
},
],
address: {
first_name,
last_name,
streetAddress1,
streetAddress2,
city,
state,
zip,
country,
},
} = req.body;
};


2. First, we check this access token against our .env file to make the user has been granted access.

api/create-purchase.js

export default async (req, res) => {
const {
accessToken,
email,
metafields: {
[key]: value
},
cartItems: [
{
productId,
variantId,
name,
quantity,
price,
},
],
address: {
first_name,
last_name,
streetAddress1,
streetAddress2,
city,
state,
zip,
country,
},
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
};


3. Using this information, we can create a purchase on Shopify, Google Sheets, CMS, or any existing platform. Let's keep it simple and send an email with the purchase information to our supplier:

  • First, let's create the email content using the address and products from our request body
api/create-purchase.js

export default async (req, res) => {
const {
accessToken,
email,
metafields: {
[key]: value
},
cartItems: [
{
productId,
variantId,
name,
quantity,
price,
},
],
address: {
first_name,
last_name,
streetAddress1,
streetAddress2,
city,
state,
zip,
country,
},
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const newPurchaseId = Date.now().toString();
let cartItemsHtml = "";
cartItems.forEach((c) => {
cartItemsHtml += `
<p>Title: ${c.name}</p>
<p>Product Id: ${c.productId}</p>
<p>Variant Id: ${c.variantId}</p>
<p>Quantity: ${c.quantity}</p>
<p>Price: ${c.price}</p>
<p>______________________________</p>
`;
});
const html = `
<div>
<h2>purchase ${newPurchaseId}</h2>
<p>Shipping Address</p>
<p>${first_name}${" "}${last_name}</p>
<p>${streetAddress1}</p>
<p>${streetAddress2}</p>
<p>${city}</p>
<p>${state}</p>
<p>${zip}</p>
<p>${country}</p>
<p>Products to ship</p>
<p>______________________________</p>
${cartItemsHtml}
</div>
`;
} catch {
}
};

  • Next, we'll use the nodemailer package and the ethereal.email service to send a test email. If the purchase is created successfully, we return 2 values:
    • purchaseId:
      string
      purchase ID of the newly created purchase (in this case, we just get the current time in milliseconds)
    • url:
      string
      link to an purchase confirmation page
api/create-purchase.js

import { createTransport, getTestMessageUrl } from "nodemailer";
export default async (req, res) => {
const {
accessToken,
email,
metafields: {
[key]: value
},
cartItems: [
{
productId,
variantId,
name,
quantity,
price,
},
],
address: {
first_name,
last_name,
streetAddress1,
streetAddress2,
city,
state,
zip,
country,
},
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const newPurchaseId = Date.now().toString();
let cartItemsHtml = "";
cartItems.forEach((c) => {
cartItemsHtml += `
<p>Title: ${c.name}</p>
<p>Product Id: ${c.productId}</p>
<p>Variant Id: ${c.variantId}</p>
<p>Quantity: ${c.quantity}</p>
<p>Price: ${c.price}</p>
<p>______________________________</p>
`;
});
const html = `
<div>
<h2>purchase ${newPurchaseId}</h2>
<p>Shipping Address</p>
<p>${first_name}${" "}${last_name}</p>
<p>${streetAddress1}</p>
<p>${streetAddress2}</p>
<p>${city}</p>
<p>${state}</p>
<p>${zip}</p>
<p>${country}</p>
<p>Products to ship</p>
<p>______________________________</p>
${cartItemsHtml}
</div>
`;
const transport = createTransport({
host: "smtp.ethereal.email",
port: 587,
auth: {
user: "marcellus.gutmann@ethereal.email",
pass: "KXWMwWjS3nXBd7xYyM",
},
});
const purchaseEmail = await transport.sendMail({
to: "yoursupplier@awesome.com",
from: email,
subject: `purchase ${newPurchaseId}`,
html,
});
return res.status(200).json({
purchaseId: newPurchaseId,
url: getTestMessageUrl(purchaseEmail),
});
} catch {
}
};


4. Lastly, if purchase creation fails, we return an error.

api/create-purchase.js

import { createTransport, getTestMessageUrl } from "nodemailer";
export default async (req, res) => {
const {
accessToken,
email,
metafields: {
[key]: value
},
cartItems: [
{
productId,
variantId,
name,
quantity,
price,
},
],
address: {
first_name,
last_name,
streetAddress1,
streetAddress2,
city,
state,
zip,
country,
},
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const newPurchaseId = Date.now().toString();
let cartItemsHtml = "";
cartItems.forEach((c) => {
cartItemsHtml += `
<p>Title: ${c.name}</p>
<p>Product Id: ${c.productId}</p>
<p>Variant Id: ${c.variantId}</p>
<p>Quantity: ${c.quantity}</p>
<p>Price: ${c.price}</p>
<p>______________________________</p>
`;
});
const html = `
<div>
<h2>purchase ${newPurchaseId}</h2>
<p>Shipping Address</p>
<p>${first_name}${" "}${last_name}</p>
<p>${streetAddress1}</p>
<p>${streetAddress2}</p>
<p>${city}</p>
<p>${state}</p>
<p>${zip}</p>
<p>${country}</p>
<p>Products to ship</p>
<p>______________________________</p>
${cartItemsHtml}
</div>
`;
const transport = createTransport({
host: "smtp.ethereal.email",
port: 587,
auth: {
user: "marcellus.gutmann@ethereal.email",
pass: "KXWMwWjS3nXBd7xYyM",
},
});
const purchaseEmail = await transport.sendMail({
to: "yoursupplier@awesome.com",
from: email,
subject: `purchase ${newPurchaseId}`,
html,
});
return res.status(200).json({
purchaseId: newPurchaseId,
url: getTestMessageUrl(purchaseEmail),
});
} catch {
return res.status(400).json({
error: "purchase creation failed.",
});
}
};

Create purchase endpoint

1. Openship will use this create purchase function to create purchases on the channel. Openship sends the following attributes to the endpoint:

  • accessToken: access token to verify the request
  • email: email connected to the Openship account
  • metafields: channel-specific fields
  • cartItems: products that need to be fulfilled
    • productId: product identifier
    • variantId: product variant identifier
    • name: product title
    • quantity: quantity of product to ship
    • price: product cost
  • address: shipping address
    • first_name
    • last_name
    • streetAddress1
    • streetAddress2
    • city
    • state
    • zip
    • state
    • country

Let's put these values to use.


2. First, we check this access token against our .env file to make the user has been granted access.


3. Using this information, we can create a purchase on Shopify, Google Sheets, CMS, or any existing platform. Let's keep it simple and send an email with the purchase information to our supplier:

  • First, let's create the email content using the address and products from our request body
  • Next, we'll use the nodemailer package and the ethereal.email service to send a test email. If the purchase is created successfully, we return 2 values:
    • purchaseId:
      string
      purchase ID of the newly created purchase (in this case, we just get the current time in milliseconds)
    • url:
      string
      link to an purchase confirmation page

4. Lastly, if purchase creation fails, we return an error.

api/create-purchase.js
ExpandClose

export default async (req, res) => {
const {
accessToken,
email,
metafields: {
[key]: value
},
cartItems: [
{
productId,
variantId,
name,
quantity,
price,
},
],
address: {
first_name,
last_name,
streetAddress1,
streetAddress2,
city,
state,
zip,
country,
},
} = req.body;
};

Create tracking endpoint

Once an purchase is created using the function above, the channel has to create tracking details on Openship. To do this, we must first get the API key from Openship. On the left sidebar, you'll see the key icon.

After generating a key, add this to your .env file as OPENSHIP_KEY. Since we'll be using Openship's GraphQL API, we'll also add OPENSHIP_DOMAIN to the .env file.

This is how the .env file looks so far:

  • ACCESS_TOKEN: access token to verify the request
  • OPENSHIP_DOMAIN: domain where your Openship API can be accessed (normally ends in /api/graphql)
  • OPENSHIP_KEY: API key created on your Openship instance
.env

ACCESS_TOKEN=supersecretpassword
OPENSHIP_DOMAIN=https://myshop.myopenship.com/api/graphql
OPENSHIP_KEY=bc5394008c83802e



1. Let's start creating our create-tracking function. First, we'll get these 4 values from the request body:

  • accessToken:
    string
    access token to verify the request
  • purchaseId:
    string
    purchase ID of the newly created purchase
  • trackingNumber:
    string
    shipping tracking number
  • trackingCompany:
    string
    shipping tracking company

We'll check the accessToken against ACCESS_TOKEN variable that's in our .env file.

api/create-tracking.js

export default async (req, res) => {
const {
accessToken,
purchaseId,
trackingNumber,
trackingCompany,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
};


2. To make requests to Openship's GraphQL API, we'll use the gql and request imports from the graphql-request package.

We'll use OPENSHIP_DOMAIN as the url and pass OPENSHIP_KEY a header named x-api-key.

api/create-tracking.js

import { request, gql } from "graphql-request";
export default async (req, res) => {
const {
accessToken,
purchaseId,
trackingNumber,
trackingCompany,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const trackingDetails = await request({
url: process.env.OPENSHIP_DOMAIN,
requestHeaders: {
"x-api-key": process.env.OPENSHIP_KEY,
},
});
} catch {
}
};


3. Let's create the mutation which will add the tracking details on Openship. We'll pass the mutation under document and pass the values from the request body as variables.

api/create-tracking.js

import { request, gql } from "graphql-request";
export default async (req, res) => {
const {
accessToken,
purchaseId,
trackingNumber,
trackingCompany,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const trackingDetails = await request({
url: process.env.OPENSHIP_DOMAIN,
requestHeaders: {
"x-api-key": process.env.OPENSHIP_KEY,
},
document: gql`
mutation (
$data: TrackingDetailCreateInput!
) {
createTrackingDetail(data: $data) {
id
}
}
`,
variables: {
data: {
trackingCompany,
trackingNumber,
purchaseId,
},
},
});
} catch {
}
};


4. Lastly, if tracking creation is successful, we'll return the tracking information.

api/create-tracking.js

import { request, gql } from "graphql-request";
export default async (req, res) => {
const {
accessToken,
purchaseId,
trackingNumber,
trackingCompany,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const trackingDetails = await request({
url: process.env.OPENSHIP_DOMAIN,
requestHeaders: {
"x-api-key": process.env.OPENSHIP_KEY,
},
document: gql`
mutation (
$data: TrackingDetailCreateInput!
) {
createTrackingDetail(data: $data) {
id
}
}
`,
variables: {
data: {
trackingCompany,
trackingNumber,
purchaseId,
},
},
});
return res
.status(200)
.json({ trackingDetails });
} catch {
}
};


5. We'll also catch any errors if tracking creation fails.

api/create-tracking.js

import { request, gql } from "graphql-request";
export default async (req, res) => {
const {
accessToken,
purchaseId,
trackingNumber,
trackingCompany,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const trackingDetails = await request({
url: process.env.OPENSHIP_DOMAIN,
requestHeaders: {
"x-api-key": process.env.OPENSHIP_KEY,
},
document: gql`
mutation (
$data: TrackingDetailCreateInput!
) {
createTrackingDetail(data: $data) {
id
}
}
`,
variables: {
data: {
trackingCompany,
trackingNumber,
purchaseId,
},
},
});
return res
.status(200)
.json({ trackingDetails });
} catch {
return res.status(400).json({
error: "Tracking creation failed.",
});
}
};


1. Let's start creating our create-tracking function. First, we'll get these 4 values from the request body:

  • accessToken:
    string
    access token to verify the request
  • purchaseId:
    string
    purchase ID of the newly created purchase
  • trackingNumber:
    string
    shipping tracking number
  • trackingCompany:
    string
    shipping tracking company

We'll check the accessToken against ACCESS_TOKEN variable that's in our .env file.


2. To make requests to Openship's GraphQL API, we'll use the gql and request imports from the graphql-request package.

We'll use OPENSHIP_DOMAIN as the url and pass OPENSHIP_KEY a header named x-api-key.


3. Let's create the mutation which will add the tracking details on Openship. We'll pass the mutation under document and pass the values from the request body as variables.


4. Lastly, if tracking creation is successful, we'll return the tracking information.


5. We'll also catch any errors if tracking creation fails.

api/create-tracking.js
ExpandClose

export default async (req, res) => {
const {
accessToken,
purchaseId,
trackingNumber,
trackingCompany,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
};

Cancel purchase endpoint

Purchases can also be cancelled. This is useful if an item is out of stock, damaged, lost, or for any reason that would prevent a purchase order from being fulfilled. When a purchase is cancelled, the connected order will be marked as PENDING so that it can be fulfilled again.


1. Let's start creating our cancel-purchase function. First, we'll get these 2 values from the request body:

  • accessToken:
    string
    access token to verify the request
  • purchaseId:
    string
    purchase ID that needs to be cancelled

We'll check the accessToken against ACCESS_TOKEN variable that's in our .env file.

api/cancel-purchase.js

export default async (req, res) => {
const {
accessToken,
purchaseId,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
};


2. We'll use the same function as before to access Openship's GraphQL API.

api/cancel-purchase.js

import { request, gql } from "graphql-request";
export default async (req, res) => {
const {
accessToken,
purchaseId,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const cancelledPurchase = await request({
url: process.env.OPENSHIP_DOMAIN,
requestHeaders: {
"x-api-key": process.env.OPENSHIP_KEY,
},
});
} catch {
}
};


3. This time, we'll call the cancelPurchase mutation and pass the purchaseId as the variable.

api/cancel-purchase.js

import { request, gql } from "graphql-request";
export default async (req, res) => {
const {
accessToken,
purchaseId,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const cancelledPurchase = await request({
url: process.env.OPENSHIP_DOMAIN,
requestHeaders: {
"x-api-key": process.env.OPENSHIP_KEY,
},
document: gql`
mutation ($purchaseId: String!) {
cancelPurchase(purchaseId: $purchaseId) {
id
}
}
`,
variables: { purchaseId },
});
} catch {
}
};


4. Lastly, if the purchase cancellation is successful, we'll return the response.

api/cancel-purchase.js

import { request, gql } from "graphql-request";
export default async (req, res) => {
const {
accessToken,
purchaseId,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const cancelledPurchase = await request({
url: process.env.OPENSHIP_DOMAIN,
requestHeaders: {
"x-api-key": process.env.OPENSHIP_KEY,
},
document: gql`
mutation ($purchaseId: String!) {
cancelPurchase(purchaseId: $purchaseId) {
id
}
}
`,
variables: { purchaseId },
});
return res
.status(200)
.json({ cancelledPurchase });
} catch {
}
};


5. We'll also catch any errors if purchase cancellation fails.

api/cancel-purchase.js

import { request, gql } from "graphql-request";
export default async (req, res) => {
const {
accessToken,
purchaseId,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
try {
const cancelledPurchase = await request({
url: process.env.OPENSHIP_DOMAIN,
requestHeaders: {
"x-api-key": process.env.OPENSHIP_KEY,
},
document: gql`
mutation ($purchaseId: String!) {
cancelPurchase(purchaseId: $purchaseId) {
id
}
}
`,
variables: { purchaseId },
});
return res
.status(200)
.json({ cancelledPurchase });
} catch {
return res.status(400).json({
error: "Purchase cancellation failed.",
});
}
};


1. Let's start creating our cancel-purchase function. First, we'll get these 2 values from the request body:

  • accessToken:
    string
    access token to verify the request
  • purchaseId:
    string
    purchase ID that needs to be cancelled

We'll check the accessToken against ACCESS_TOKEN variable that's in our .env file.


2. We'll use the same function as before to access Openship's GraphQL API.


3. This time, we'll call the cancelPurchase mutation and pass the purchaseId as the variable.


4. Lastly, if the purchase cancellation is successful, we'll return the response.


5. We'll also catch any errors if purchase cancellation fails.

api/cancel-purchase.js
ExpandClose

export default async (req, res) => {
const {
accessToken,
purchaseId,
} = req.body;
if (process.env.ACCESS_TOKEN !== accessToken) {
return res
.status(403)
.json({ error: "Denied" });
}
};

Deploying the channel

Now that we have our functions built, we have to deploy them. We'll keep it simple and add these functions to a Next.js application as API Routes. This is a good starting place when building your own channel. Check out the CodeSandbox below to customize it and make it your own.

When you're finished customizing, you can deploy the application to Vercel, Netlify, or any platform that supports node.js.

We have already deployed the demo channel we just made. To test it, add the channel and choose DEMO under the channel type.

Create Channel

Deploy this channel yourself on Vercel: