{"templateId":"markdown","versions":[{"version":"shipstation-v2","label":"ShipStation V2 API","link":"/apis/shipstation-v2/docs/labels/webhooks","default":true,"active":false,"folderId":"58c9a61d"},{"version":"shipengine","label":"ShipStation API (formerly ShipEngine)","link":"/apis/shipengine/docs/labels/webhooks","default":false,"active":true,"folderId":"58c9a61d"},{"version":"shipstation-v1","label":"ShipStation V1 API","link":"/apis/shipstation-v1/docs/labels/webhooks","default":false,"active":false,"folderId":"58c9a61d"}],"sharedDataIds":{"sidebar":"sidebar-apis/@shipengine/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Creating a Webhook Listener","keywords":"shipping, labels, shipstation, documentation, api","siteUrl":"https://docs.shipstation.com","lang":"en-US","llmstxt":{"hide":false,"title":"ShipStation API LLM Docs","description":"Find links and references to all markdown documentation for use with LLMs","excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"creating-a-webhook-listener","__idx":0},"children":["Creating a Webhook Listener"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["As described in our ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"/apis/shipengine/docs/guides/webhooks"},"children":["Setting Up Webhooks"]},", testing webhooks using ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://requestbin.com/"},"children":["RequestBin.com"]}," is a great way to get started with using and understanding webhooks."," ","It provides temporary URLs that you can register through the ShipStation API dashboard or by using the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1/environment/webhooks"]}," endpoint."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When using ShipStation API in a production environment, you will need to provide a more robust web service to host your own custom endpoints. This guide walks you through creating a simple web application that exposes an endpoint you can use to subscribe to ShipStation API ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batch"]}," webhooks."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We will be developing a web application that listens for webhook requests, often called a webhook listener. Our application will run a web server and listen for HTTP POST requests on a specified endpoint. This example is written in ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://developer.mozilla.org/en-US/docs/Web/JavaScript"},"children":["JavaScript"]},"/",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://nodejs.org/"},"children":["NodeJS"]}," and uses the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://expressjs.com/"},"children":["express"]}," web framework. Once we receive a webhook indicating that a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batch"]}," has completed processing, we will make a series of calls the to ShipStation API to download the labels. For this we will be using an HTTP client called ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://github.com/axios/axios"},"children":["axios"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"requirements","__idx":1},"children":["Requirements"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To follow along with the steps in this guide, you will need to do the following:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Install ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://nodejs.org"},"children":["NodeJS"]}," per the site's instructions. Likewise, you can use other package managers, such as ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://brew.sh/"},"children":["Homebrew"]}," on macOS or Linux, and ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://chocolatey.org/"},"children":["Chocolatey"]}," for Windows."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Verify that ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://www.npmjs.com/"},"children":["npm"]}," was installed successfully along with NodeJS by running the following command from a terminal: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["npm -v"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Clone the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://github.com/ShipEngine/code-samples"},"children":["code-samples repo"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Open an editor where you can write and run code. We will be using ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://code.visualstudio.com/"},"children":["Visual Studio Code"]}," in this guide, but you can use any editor you are comfortable with."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["In VS Code, select ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["File"]}," in the menu bar at the top and then select ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Open Folder"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Navigate to the directory where you cloned the repo and select ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["code-samples/node-webhook-listener-batch"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Click the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Select"]}," button."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"install-dependencies","__idx":2},"children":["Install Dependencies"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We will be using several external NPM packages in our web application, such as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["axios"]},". These are known as dependencies. We could install each package individually by running ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["npm install <package name>"]},". However, it is customary to provide a ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://github.com/ShipEngine/code-samples/blob/218fef76a2ac4bad4f29322a1a6ee64b6a93552b/node-webhook-listener-batch/package.json#L1"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["package.json"]}]}," file in the root of your project that lists all the project's dependencies as well as other identifying information about the application. This facilitates installation since all dependencies for all files in the project are listed in a central location and can all be installed at once with a single command."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Open a terminal in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["node-webhook-listener-batch"]}," directory and run the following command: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["npm install"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This command installs the dependencies we listed in the",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["package.json"]}," file and creates a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["package.json.lock"]}," file to nail down the specific versions installed since the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["package.json"]}," file allows us to specify minimum versions."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["TIP:"]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"open-a-terminal","__idx":3},"children":["Open a Terminal"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can access a terminal directly from Visual Studio Code. Click ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Terminal"]}," on the menu bar and select ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["New"]}," and a terminal will open up at the bottom of your screen."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"import-dependencies","__idx":4},"children":["Import Dependencies"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We've now installed all the dependencies required for this application, and we are ready to take a look at the code, which resides in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["index.js"]}," file."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We start by importing the tools and frameworks mentioned above, as well as a few others, at the top of our file. The code below includes all the packages needed by this application. We will be using ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]},", which is a web framework that provides the web server we are using. We will configure ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," to use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["body-parser"]}," package so that we can access the data in the requests sent to the endpoint. We will use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["axios"]}," to make HTTP requests to the ShipStation API, and we will use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fs"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["path"]}," to write the downloaded label to the file system."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You may have noticed that the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["package.json"]}," did not list ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fs"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["path"]},". That's because these are core NodeJS modules that do not need to be installed separately. We do, however, still need to import them before we can use them in our application."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const express = require('express');\nconst axios= require('axios');\nconst bodyParser = require('body-parser');\nconst fs= require('fs')\nconst path = require('path')\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"create-the-web-application","__idx":5},"children":["Create the Web Application"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["At this point, we have specified and installed dependencies, and we are ready to create our web application and define endpoints. When you configure webhooks in ShipStation API, you provide a URL to which ShipStation API will send an HTTP POST request with a JSON payload whenever a particular event occurs. We recommend that you create an individual endpoint for each type of webhook you wish to subscribe to and limit traffic on those endpoints to ShipStation API webhook traffic."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The code below creates an instance of an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," web server and assigns it to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["app"]}," variable. We then configure our ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," application to use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["body-parser"]}," package. Since we called the variable ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["bodyParser"]}," in the import statement above, that's how we reference it in the code below. The last line of code starts the server, listening on port ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["3000"]}," for incoming requests. This line is customarily the last one in the script. We will be filling in our endpoint implementation in the space between."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const app = express();\napp.use(bodyParser.urlencoded({ extended: false }));\napp.use(bodyParser.json());\n\n...\n\nlet server = app.listen(3000, function() {\n  console.log('Listening on port %d', server.address().port);\n});\n\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"create-the-batch-endpoint","__idx":6},"children":["Create the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/batch"]}," Endpoint"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We now have a very simple web application called ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["app,"]}," but we have not defined any endpoints for our application."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["In this example, we are going to create a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/batch"]}," endpoint to use for batch event webhooks."," ","One of the benefits of developing and hosting your own web application is that you can programmatically trigger other events to occur once you receive the webhook, which we'll demonstrate in our example endpoints."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Let's start by taking a look at the payload we expect to receive on this endpoint."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"sample-batch-webhook-payload","__idx":7},"children":["Sample Batch Webhook Payload"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"resource_url\": \"https://api.shipengine.com/v1/batches/se-1013119\",\n  \"resource_type\": \"API_BATCH\"\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can see from the example above that the batch webhook payload includes a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resource_url"]},". We can use this URL to get more information about the batch and ultimately download the labels."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The code below is an example implementation of an endpoint that listens for HTTP POST requests. We do this by making a call to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["app.post"]}," method of our ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," server instance. The first parameter we pass to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["app.post"]}," method is the path we want to use for the endpoint, in this case ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/batch"]},". The second parameter is a callback function."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you need more context on asynchronous programming and callback functions, check out ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://javascript.info/callbacks"},"children":["this reference"]},". The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["app.post"]}," method is asynchronous, meaning that the program does not wait for this call to return before moving to the next line of code in the file. Instead, we pass a callback function as the second parameter to tell the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["app.post"]}," method what to do when it completes. In this case, we define the callback function directly in the call to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["app.post"]},". This is known as an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["anonymous function"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Notice that the anonymous callback function has the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["async"]}," keyword in front of the parameters."," ","We declare this function as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["async"]}," because we will be using the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["async/await"]}," syntax in our ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["axios"]}," calls, and this syntax can only be used in asynchronous functions. If you need more context around using ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["async/await"]},", this is a ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await"},"children":["great reference"]},". Once the endpoint receives a request, it invokes the callback function."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When we receive a request on this endpoint, we first extract the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resource_url"]}," from the request body sent in the webhook request. We have access to this data on the request object because we configured the application to use the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://www.npmjs.com/package/body-parser"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["body-parser"]}]}," package."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Finally, we send a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]}," HTTP status code to terminate the call and return program control to the main application. Whenever ShipStation API sends a tracking event webhook to your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/batch"]}," endpoint, this code will be called."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"app.post('/batch', async (req, res) => {\n\n  const resourceUrl = req.body.resource_url;\n  res.sendStatus(200);\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We then use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["axios"]},", a Promise-based HTTP client, to send an HTTP GET request to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resource_url"]}," we extracted. We use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["await"]}," keyword to indicate that the application should wait until the HTTP request is complete before continuing with the rest of the program."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["At this point, we make a call to the ShipStation API, so we need to provide an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["api-key"]}," header in the HTTP request to identify ourselves as having access to this batch. The code below references a variable called ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_KEY"]},", which is declared at the top of the file. You will need to set this variable to a valid API key for your account before running this application. Refer to our ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"/apis/shipengine/docs/guides/auth"},"children":["Security & Authentication guide"]}," if you need a more information."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We performed a GET request, by setting ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["method: 'get'"]}," in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["axios"]}," configuration. We also told the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["axios"]}," client what sort of data we expect to receive from this request by setting the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["responseType"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["application/json"]},". Finally, we included the URL we wish to access, which we stored in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resourceUrl"]}," variable, and add our API key as an HTTP header. The response from this"," ","HTTP request is stored in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resourceUrlResponse"]}," variable, which we will use further along in the application."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const resourceUrlResponse = await axios({\n    method: 'get',\n    url: resourceUrl,\n    responseType: 'application/json',\n    headers: {\n      'api-key': API_KEY\n    }\n});\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Once we perform a GET request on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resource_url"]},", we get a response similar to the following."," ","You can see it includes a list of URLs from which we can download our completed labels."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"sample-resource-url-response","__idx":8},"children":["Sample Resource URL Response"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"{\n  label_layout: '4x6',\n  label_format: 'pdf',\n  batch_id: 'se-1032673',\n  external_batch_id: null,\n  batch_notes: 'Warehouse 7 Batch',\n  created_at: '2020-06-25T21:27:51.667Z',\n  processed_at: '2020-06-25T21:28:03.32Z',\n  errors: 0,\n  warnings: 0,\n  completed: 1,\n  forms: 0,\n  count: 1,\n  batch_shipments_url:  {\n    href:\n      'https://api.shipengine.com/v1/shipments?batch_id=se-1032673'\n    },\n    batch_labels_url:  {\n      href: 'https://api.shipengine.com/v1/labels?batch_id=se-1032673'\n    },\n    batch_errors_url:  {\n      href: 'https://api.shipengine.com/v1/batches/se-1032673/errors'\n    },\n   label_download:  {\n     pdf: 'https://api.shipengine.com/v1/downloads/10/HDFhhN38nU-rS-3lrxxawQ/label-1032673.pdf',\n     zpl: 'https://api.shipengine.com/v1/downloads/10/HDFhhN38nU-rS-3lrxxawQ/label-1032673.zpl',\n     href: 'https://api.shipengine.com/v1/downloads/10/HDFhhN38nU-rS-3lrxxawQ/label-1032673.pdf' },\n   form_download:  {\n     href:  'https://api.shipengine.com/v1/downloads/10/R9ci610cDU62uEJF-IxOhg/form-1032673.pdf'\n   },\n   status: 'completed'\n }\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We next extract the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["label_download.pdf"]}," URL from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["labelUrlResponse"]}," variable, which contains the HTTP response for the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resourceUrl"]},", and use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["axios"]}," HTTP client to perform a GET on this URL. However, this time we set the client's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["responseType"]}," property to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["stream"]}," rather than ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["application/json"]}," since we will be receiving binary data that we want to save to the file system. Again, we include the URL we wish to access and add our API key as an HTTP header. We also set the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batchId"]}," variable to the batch ID we extract from the response."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const labelUrl = resourceUrlResponse.data.label_download.pdf;\nconst batchId = resourceUrlResponse.data.batch_id;\n\nconst labelUrlResponse = await axios({\n  method: 'get',\n  url: labelUrl,\n  responseType: 'stream',\n  headers: {\n    'api-key': API_KEY\n  }\n});\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Finally, we write the contents of the label file that were streamed above to a file on the file system once the request completes. We use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batchId"]}," variable, which contains the batch ID we extracted from the response payload above, to create a unique file name and to identify the batch that these labels came from."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This first few lines creates the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["labels"]}," directory. We enclose this in a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["try/catch"]}," block in case the folder already exists."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Line 10 specifies the path to the file we wish to create to hold the downloaded label, and line 11 creates a writeable stream that can write the contents to the file just created on the file system. The last two lines write the data on the stream to the file on the file system in incremental chunks. Using a stream is more efficient on RAM usage when downloading a large batch of labels since the data is read into memory and written to the file system in chunks rather than reading the entire file into memory before writing it to the file system."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const dir = 'labels';\n\ntry {\n   if (!fs.existsSync(path.resolve(__dirname, dir,))) {\n     fs.mkdirSync(dir)\n   }\n} catch (err) {\n  console.error('Error creating directory: ', err)\n}\n\nconst filePath = path.resolve(__dirname, 'labels', `${batchId}.pdf`);\nconst writer = fs.createWriteStream(filePath, { flag:'wx'});\n\nlabelUrlResponse.data.on('data', function(chunk) {\n  writer.write(chunk);\n});\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"putting-it-all-together","__idx":9},"children":["Putting It All Together"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We have now performed the following:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Cloned the repo."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Installed the packages using ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["npm install"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Created import statements for the packages we want to use in our application."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Created an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["express"]}," web application that listens for requests on port ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["3000"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Created an endpoint to use for batch processing events."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The complete script for the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/batch"]}," endpoint is included below.  If you cloned our ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://github.com/ShipEngine/code-samples"},"children":["code-samples repository"]},", you will find this example application in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["node-webhook-listener-batch"]}," directory."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const express = require('express');\nconst axios= require('axios');\nconst bodyParser = require('body-parser');\nconst fs = require('fs')\nconst path = require('path')\n\nconst app = express();\napp.use(bodyParser.urlencoded({ extended: false }));\napp.use(bodyParser.json());\n\nconst API_KEY = YOUR_API_KEY;\n\napp.post('/batch', async (req, res) => {\n\n  const resourceUrl = req.body.resource_url;\n  res.sendStatus(200);\n\n  const resourceUrlResponse = await axios({\n    method: 'get',\n    url: resourceUrl,\n    responseType: 'application/json',\n    headers: {\n      'api-key': API_KEY\n    }\n  });\n\n  const labelUrl = resourceUrlResponse.data.label_download.pdf;\n  const batchId = resourceUrlResponse.data.batch_id;\n\n  const labelUrlResponse = await axios({\n     method: 'get',\n     url: labelUrl,\n     responseType: 'stream',\n     headers: {\n       'api-key': API_KEY\n     }\n  });\n\n  const dir = 'labels';\n\n  try {\n    if (!fs.existsSync(path.resolve(__dirname, dir,))) {\n      fs.mkdirSync(dir)\n    }\n  } catch (err) {\n    console.error('Error creating directory: ', err)\n  }\n\n  const filePath = path.resolve(__dirname, 'labels', `${batchId}.pdf`);\n  const writer = fs.createWriteStream(filePath, { flag:'wx'});\n\n  labelUrlResponse.data.on('data', function(chunk) {\n    writer.write(chunk);\n  });\n\n});\n\nlet server = app.listen(3000, function() {\n  console.log('Listening on port %d', server.address().port);\n});\n\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"running-the-webhook-listener","__idx":10},"children":["Running the Webhook Listener"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We have now written a web application that exposes a single endpoint. Let's start by running it locally to test it out. If you are developing in Visual Studio Code, follow these steps to run your application:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Make sure ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["index.js"]}," is open in Visual Studio Code"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Click ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Run"]}," in the top menu."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Select ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Start Debugging"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Selected ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Node.js"]}," in the environment drop-down list that is displayed."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You should see a debug window at the bottom of the screen. Your IDE should look similar to this."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"img","attributes":{"src":"/assets/vsc-batch-debug.809346f357e65d1e0378f942ff8395e47fc68aa9f547d82c4176852aaf38b04b.6eb7f06a.png","alt":""},"children":[]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"testing-the-webhook-listener","__idx":11},"children":["Testing the Webhook Listener"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your application is now running! But let's test it out before we try to use it to receive webhook requests. Follow these steps to test that your application is working."]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Download and install ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://www.postman.com/"},"children":["Postman"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Open Postman"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Click the orange ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["New"]}," button"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Select ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Request"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Give your request a name and click ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Save"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Change the method type from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," in the drop-down"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Enter ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["http://localhost:3000/batch"]}," for the URL"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Select the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Body"]}," tab"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Change the type from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["none"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["raw"]}," in the drop-down box"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Change the type from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Text"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["JSON"]}," in the drop-down box"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Copy the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"#sample-batch-webhook-payload"},"children":["sample"]}," payload above and paste it into the request body"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Paste it into the body area"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Click the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Send"]}," button. You should get a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]}," HTTP status code and see ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["OK"]}," in the body. Your screen will look similar to this."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"img","attributes":{"src":"/assets/postman.f09a5a62d8c44868a5712418585221115cbdfdf0c7336de47b9af0f0b8323b81.6eb7f06a.png","alt":"Postman"},"children":[]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"validation","__idx":12},"children":["Validation"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We got a successful response from Postman, but if you look at your VS Code debug console, you will see that an unhandled exception was thrown. Your VS Code console should look similar to the following:"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"img","attributes":{"src":"/assets/vsc-400-debug.3ef474e1d5a391598d7dfc5aaf7d87e9ac198995ceb2653eb544a34bbd990875.6eb7f06a.png","alt":"VSCode"},"children":[]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["What's the problem? We sent back a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200"]}," code, and then we hit errors further along in the code. We didn't add any logic in our application to check for and handle errors and expose those errors to the caller. In our case, the sample request payload includes a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resource_url"]}," that belongs to another account, so we hit an authentication error when we tried to access that URL using our API key. This shouldn't happen with the live payloads this endpoint will receive from ShipStation API, but it does highlight the need to think through possible errors and exception handling before using your application in a production environment."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Before using the application in production, you will need check that the message is what you expect and only attempt to access those properties if you receive the correct message. Furthermore, you can check that requests received on your endpoint are coming from ShipStation API by inspecting the headers from within your endpoint. All requests coming from ShipStation API will have the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["user-agent"]}," header set to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ShipEngine/v1"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"testing-with-real-webhooks","__idx":13},"children":["Testing with Real Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We validated that our web application is working properly by sending a request to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["localhost"]}," via Postman. In order to test the application with actual webhook requests from ShipStation API, we will need to make it publicly accessible. There are several ways to do this for production, but for test purposes we will use ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://ngrok.com/"},"children":["ngrok"]}," to expose our web application so that ShipStation API can access it to send webhooks."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Follow these steps to test your application with ShipStation API:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Download and install ngrok."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Open a terminal and change to the directory where ngrok is installed."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Run the following command: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ngrok http 3000"]},". This tells ngrok that you want to expose port 3000 on your localhost. ngrok will response with something similar to this:"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"img","attributes":{"src":"/assets/ngrok.04581d86ce224beb77def421ed7b51f9ebdb20a4be654370c3a4a846f3caa8ba.6eb7f06a.png","alt":"ngrok"},"children":[]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The command we ran above allows ngrok to forward HTTP traffic to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["localhost"]}," on port ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["3000"]},". It generated a random, unique URL we can use to configure webhooks in ShipStation API. In the example above, this URL is ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["http://f2ea123672fc.ngrok.io"]},". You'll get a new URL each time you start ngrok, so this won't be a permanent configuration, but it will allow us to test that the web application is receiving and processing ShipStation API webhooks as expected."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Copy the URL that was generated when you ran the ngrok command and follow our ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"/apis/shipengine/docs/guides/webhooks"},"children":["webhooks guide"]}," to configure ShipStation API to use this URL. You will need to add the endpoint to the URL when you add it to ShipStation API. For example, given the URL generated above, we would use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["http://f2ea123672fc.ngrok.io/batch"]}," for the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batch"]}," events, or \"Batch Completed\" if you are configuring from the dashboard."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"triggering-the-webhook","__idx":14},"children":["Triggering the Webhook"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Now that we have configured ShipStation API to send webhook requests whenever a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batch"]}," event completes, let's perform the steps necessary to trigger a real webhook request."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Follow our ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"/apis/shipengine/docs/reference/process-batch"},"children":["Process a Batch guide"]}," to complete the steps necessary to process a batch and cause a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batch"]}," webhook request to be sent. Alternately, you can use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["trigger-webhooks.js"]}," script in our ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://github.com/ShipEngine/code-samples"},"children":["code samples repo"]}," to trigger the webhook. You can follow the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://github.com/ShipEngine/code-samples/tree/master/node-webhook-listener"},"children":["readme"]}," for guidance."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"authentication","__idx":15},"children":["Authentication"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You may also wish to add security to your webhooks by using ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://en.wikipedia.org/wiki/Basic_access_authentication"},"children":["Basic Authentication"]},". This would require you to supply the username and password directly in the URL as specified in the example below. It would prevent any traffic from reaching your endpoint that did not include the valid username and password in the URL."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"http","header":{"controls":{"copy":{}}},"source":"POST /v1/environment/webhooks HTTP/1.1\nHost: api.shipengine.com\nAPI-Key: __YOUR_API_KEY_HERE__\nContent-Type: application/json\n\n{\n  \"url\": \"https://username:password@example.com\",\n  \"event\": \"batch\"\n}\n","lang":"http"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"getting-to-production","__idx":16},"children":["Getting to Production"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["At this point, we have created a simple web application that listens for webhook requests on the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/batch"]}," endpoint. We tested it locally by sending a sample webhook payload using Postman. We then tested it with live webhook traffic by using ngrok to publicly expose our web application so that ShipStation API could reach it. While this is a great approach to learning how to build a webhook"," ","listener application, you will need a more permanent hosting solution before integrating your application into your production environment. There are additional steps you will need to take before you can configure ShipStation API to use the endpoints exposed by this application. Namely, you will need to host your application and make it publicly accessible so that ShipStation API can reach it."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you have an existing web application that integrates with ShipStation API, then you should be familiar with the steps required to host your webhook listener application. If not, you'll probably need to start by registering a domain name for your application. You will then need to select a cloud provider, such as ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://aws.amazon.com/websites/"},"children":["AWS"]},", ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"http://microsoft.azure.com"},"children":["Azure"]},", or ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://cloud.google.com/solutions/web-hosting?utm_campaign=na-US-all-en-dr-bkws-all-all-trial-b-dr-1008076&utm_term=KW_%2Bgoogle+%2Bweb+%2Bhosting-ST_%2BGoogle+%2Bweb+%2Bhosting&gclid=EAIaIQobChMIxqzT8pSn6gIVSr7ACh0Tug8fEAAYASAAEgKfj_D_BwE&utm_content=text-ad-none-any-DEV_c-CRE_113112112207-ADGP_Hybrid+%7C+AW+SEM+%7C+BKWS+%7C+US+%7C+en+%7C+Multi+%7E+Google+Web+Hosting-KWID_43700009942847439-kwd-26940697878&utm_source=google&utm_medium=cpc"},"children":["GCP"]}," to host your application and provide DNS services for your application."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You might also use a web hosting service that handles domain registration, hosting, security, and"," ","other facets of web hosting for you."]}]},"headings":[{"value":"Creating a Webhook Listener","id":"creating-a-webhook-listener","depth":1},{"value":"Requirements","id":"requirements","depth":2},{"value":"Install Dependencies","id":"install-dependencies","depth":2},{"value":"Open a Terminal","id":"open-a-terminal","depth":3},{"value":"Import Dependencies","id":"import-dependencies","depth":2},{"value":"Create the Web Application","id":"create-the-web-application","depth":2},{"value":"Create the /batch Endpoint","id":"create-the-batch-endpoint","depth":3},{"value":"Sample Batch Webhook Payload","id":"sample-batch-webhook-payload","depth":3},{"value":"Sample Resource URL Response","id":"sample-resource-url-response","depth":3},{"value":"Putting It All Together","id":"putting-it-all-together","depth":2},{"value":"Running the Webhook Listener","id":"running-the-webhook-listener","depth":2},{"value":"Testing the Webhook Listener","id":"testing-the-webhook-listener","depth":2},{"value":"Validation","id":"validation","depth":2},{"value":"Testing with Real Webhooks","id":"testing-with-real-webhooks","depth":2},{"value":"Triggering the Webhook","id":"triggering-the-webhook","depth":2},{"value":"Authentication","id":"authentication","depth":2},{"value":"Getting to Production","id":"getting-to-production","depth":2}],"frontmatter":{"seo":{"title":"Creating a Webhook Listener"}},"lastModified":"2026-04-08T10:47:45.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/apis/shipengine/docs/labels/webhooks","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}