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 Openship channels, are split into 2 endpoints, searching for products and placing orders.

💡

Creating custom channels requires a little programming knowledge. Need us to make you a custom channel? Get in touch.

Creating the search products endpoint

  1. Let's start with a barebones function that returns an empty products array
// searchProducts function
export default async function handler(req, res) {
const allProducts = []
res.status(200).json({ products: allProducts })
}
  1. Now, we can fill in this products array with products we want to return to Openship. You can fetch these products from a CMS, Google Sheet, or any platform. To keep it simple for this guide, we will return a JSON with the products.
// searchProducts function
export default async function handler(req, res) {
const allProducts = [
{
// image of the product
image:
'https://user-images.githubusercontent.com/41929050/61567048-13938600-aa33-11e9-9cfd-712191013192.jpeg',
// product title
title: 'The Quantified Cactus: An Easy Plant Soil Moisture Sensor',
// product identifier
productId: '887262',
// variant product identifier, if none send 0
variantId: '0',
// product price
price: '12.89',
// true if item is available
availableForSale: true,
},
{
image:
'https://user-images.githubusercontent.com/41929050/61567049-13938600-aa33-11e9-9c69-a4184bf8e524.jpeg',
title: 'A beautiful switch-on book light',
productId: '773642',
variantId: '0',
price: '4.26',
availableForSale: true,
},
]
res.status(200).json({ products: allProducts })
}
💡

Please note that each variant needs to be a different product in the array. For example, if you have a red and black color variant, each will need to be a separate product with different variantIds.

  1. Openship will send some attributes to the endpoint:
const {
// we need to check this accessToken against our
// ENV file to make user has been granted access
accessToken,
// we need to filter our products based on this searchEntry
searchEntry,
// in some cases Openship will send a productId and variantId
// the function needs to find that product and return it
productId,
variantId,
} = req.query

Let's account for these attributes:

// searchProducts function
export default async function handler(req, res) {
const { accessToken, searchEntry, productId, variantId } = req.query
// Check if accessToken from Openship matches ENV
// if not, return "Access Denied"
if (!process.env.ACCESS_TOKEN || process.env.ACCESS_TOKEN === accessToken) {
const allProducts = [
{
image:
'https://user-images.githubusercontent.com/41929050/61567048-13938600-aa33-11e9-9cfd-712191013192.jpeg',
title: 'The Quantified Cactus: An Easy Plant Soil Moisture Sensor',
productId: '887262',
variantId: '0',
price: '12.89',
availableForSale: true,
},
{
image:
'https://user-images.githubusercontent.com/41929050/61567049-13938600-aa33-11e9-9c69-a4184bf8e524.jpeg',
title: 'A beautiful switch-on book light',
productId: '773642',
variantId: '0',
price: '4.26',
availableForSale: true,
},
]
// if productId and variantId exists, filter allProducts based on these values
// if no products are found after filter, return error
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: 'Product not found' })
}
// if searchEntry exists, filter allProducts based on that value
if (searchEntry) {
const products = allProducts.filter((product) =>
product.title.includes(searchEntry)
)
return res.status(200).json({ products })
}
return res.status(200).json({ products: allProducts })
}
return res.status(400).json({ error: 'Access denied' })
}

🎉 Hooray, the search products endpoint is done!

Creating the place order endpoint

  1. The placeOrder function will recieve the following attributes in the request body
// placeOrder function
const {
// we need to check this accessToken against our
// ENV file to make user has been granted access
accessToken,
// email connected to the Openship account
email,
// shipping address
address: {
first_name,
last_name,
streetAddress1,
streetAddress2,
city,
state,
zip,
},
// array of cartItems
cartItems: [
{
// productId and variantId same as the ones sent in searchProducts endpoint
productId,
variantId,
// product title
name,
quantity,
price,
},
],
} = req.body
  1. Using this information, we can create an order on Shopify, Google Sheets, CMS, or any existing platform. Let's keep it simple and send an email with the order information to our supplier. We're going to use the nodemailer package to do so:
// placeOrder function
import { createTransport, getTestMessageUrl } from 'nodemailer'
export default async function handler(req, res) {
const {
accessToken,
email,
address: {
first_name,
last_name,
streetAddress1,
streetAddress2,
city,
state,
zip,
},
cartItems,
} = req.body
if (!process.env.ACCESS_TOKEN || process.env.ACCESS_TOKEN === accessToken) {
try {
// we are using ethereal.email which will allow us to send test emails
// use the user and pass below at https://ethereal.email to see the created email
const transport = createTransport({
host: 'smtp.ethereal.email',
port: 587,
auth: {
user: 'lexus31@ethereal.email',
pass: '2S2R8P29bTFQr2K2kY',
},
})
// we can use the current time as an order ID
const newOrderId = 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 className="email" style="
border: 1px solid black;
padding: 20px;
font-family: sans-serif;
line-height: 2;
font-size: 20px;
">
<h2>Order ${Date.now()}</h2>
<p>Address</p>
<p>${first_name}${' '}${last_name}</p>
<p>${streetAddress1}</p>
<p>${streetAddress2}</p>
<p>${city}</p>
<p>${state}</p>
<p>${zip}</p>
<p>Line Items</p>
<p>_________________________________</p>
${cartItemsHtml}
</div>
`
const orderEmail = await transport.sendMail({
to: 'yoursupplier@awesome.com',
from: email,
subject: `Order ${newOrderId}`,
html,
})
// if order creation is successful, we return
// url: String (link to an order confirmation page)
// purchaseId: String (Order ID of the newly created order)
return res.status(200).json({
url: getTestMessageUrl(orderEmail),
purchaseId: newOrderId,
})
} catch {
// if order creation fails, we need to return an error
return res.status(400).json({ error: 'Something went wrong.' })
}
}
res.status(400).json({ error: 'Access denied' })
}

🎉 Hooray, the place order endpoint is done!

Adding the channel on Openship

Now that the 2 functions are done, we need to deploy them. For this we will be using a Next.js application and adding these functions as API routes.

Here's the Github repo where we have done this: https://github.com/junaid33/os-channel. You can also deploy your own channel and adapt it to your needs.

The channel we built is deployed at https://os-channel.vercel.app.

To test it on your Openship account, go to https://app.openship.org/channels and choose custom as the channel type. Choose any name and access token. The channel is not locked down so any access token should be fine.

For search endpoint, enter https://os-channel.vercel.app/api/searchProducts

For placement endpoint, enter https://os-channel.vercel.app/api/placeOrder

Adding tracking to the created order

When the channel ships the order, it will need to send the tracking information to Openship so it can be uploaded to Shopify. This is done by hitting this endpoint:

https://www.app.openship.org/api/webhooks/orderShip

with this body:

{
// this is the order ID created from placeOrder function
"order_id": "30656294165",
"tracking_numbers": "1Z586Y510334272054",
"tracking_company": "UPS"
}

🎉 You are now ready to use your channel!