In this Quick Start, we're going to build a simple Output Adapter that receives shipment data and outputs a text file with the mode of transportation. This is an oversimplified use case, and your production adapter will probably do a lot more work, however the purpose of this adapter is to show you how to interact with Chain.io and register your adapter.
Our adapter has the following requirements
This sample code represents a plain javascript implementation of our webhook and callback. This could be built into an ExpressJS server or be implemented on a hosted platform like AWS Lambda.
const CryptoJS = require('crypto-js')
// Do NOT commit the shared secret into your code repository
// Use whatever secrets manager you feel is appropriate to your
// development process to inject it into your code base.
const MY_CHAINIO_SECRET = process.env.CHAINIO_SHARED_SECRET
function makeSignature(body) {
const hash = CryptoJS.HmacSHA256(body, MY_CHAINIO_SECRET)
return CryptoJS.enc.Base64.stringify(hash)
}
function verifySignature(body, sig) {
const expected = makeSignature(body)
if (expected !== sig) {
throw new Error('bad signature')
}
}
async function processPayload (body, headers) {
verifySignature(body, headers['x-chainio-signature']
const userLogs = []
let status = 'success'
const { configuration, payload, callback } = JSON.parse(body)
// Extract configuration values from request that the user
// has configured in the Chain.io portal setup for the flow
const { modeTable, errorOnMissing } = configuration
// Extract the transport_mode property from the actual data payload
// Each payload will be unique to the type of integration, this
// particular example is using a visibility register shipment
// payload (vis.register_shipment)
const { transport_mode } = payload
let returnPayload = transport_mode
const conversion = modeTable.find(mt => mt.src === transport_mode)
if (conversion) {
returnPayload = conversion.dest
userLogs.push({
level: 'info',
message: `Converted ${transport_mode} to ${conversion.dest}`,
timestamp: Date.now().toISOString()
})
} else if (errorOnMissing) {
userLogs.push({
level: 'error',
message: `Source mode ${transport_mode} was not found, processing stopped`,
timestamp: Date.now().toISOString()
})
status = 'error'
} else {
userLogs.push({
level: 'info',
message: `Source mode ${transport_mode} was not found in conversion table, leaving it alone`,
timestamp: Date.now().toISOString()
})
}
const callbackVal = {
status,
userLogs,
callback // always return the callback property exactly as it was sent to you
// This sample does not include the optional dataTags & files objects
// you can learn more about them elsewhere in the documentation
}
if (status === 'success') {
callbackVal.payload = [
{
file_name: `mode-${Date.now()}.txt`,
mime_type: 'text/plain',
body: Buffer.from(returnPayload).toString('base64'),
content_transfer_encoding: 'base64'
}
]
}
const body = JSON.stringify(callbackVal)
return fetch('https://webhooks.chain.io/callback', {
method: 'post',
body,
headers: {
'Content-Type': 'application/json',
'x-chainio-signature': makeSignature(body)
}
})
}
In order for users to access your adapter, you'll need to register it using a developer key and our registration API.
// sample registration body
const options = {
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/json',
'x-api-key': process.env.CHAINIO_API_KEY //secret API key - DO NOT STORE IN YOUR SOURCE CODE
},
body: JSON.stringify({
integration_identifiers: ['vis.receive_shipment_update'],
display_name: 'Mode Text File Generator',
display_description: 'Make text file with your mode of transportation',
support_url: 'https://mymodetranslator.com/help',
adapter_type: 'output',
webhook_url: 'https://fake.fake/my_url',
shared_secret: 'abc1239193118$$@!#1',
adapter_icon_url: 'https://cdn-icons-png.flaticon.com/512/9908/9908191.png',
publication_type: 'public',
configuration_options: [
{
name: 'customerNumber',
label: 'Customer Number',
support_url: 'https://docs.co2-co.sample/help#customerNumber',
description: 'Your CO2-Co customer number',
config_type: 'regex',
required: false,
regex_config: {
regex: '^[0-9]{5}#x27;,
regex_error: 'Your customer number must be exactly 5 digits.'
}
},
{
name: 'errorOnMissing',
label: 'Error On Missing Mode',
description: "Should this job fail if the mode isn't in the table or just pass along the value provided.",
config_type: 'boolean'
}
]
})
};
fetch('https://webhooks.chain.io/adapters', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));