{"templateId":"markdown","versions":[{"version":"shipstation-v2","label":"ShipStation V2 API","link":"/apis/shipstation-v2/docs/guides/webhooks","default":true,"active":false,"folderId":"58c9a61d"},{"version":"shipengine","label":"ShipStation API (formerly ShipEngine)","link":"/apis/shipengine/docs/guides/webhooks","default":false,"active":true,"folderId":"58c9a61d"},{"version":"shipstation-v1","label":"ShipStation V1 API","link":"/apis/shipstation-v1/docs/guides/webhooks","default":false,"active":false,"folderId":"58c9a61d"}],"sharedDataIds":{"sidebar":"sidebar-apis/@shipengine/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Setting Up Webhooks","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":"setting-up-webhooks","__idx":0},"children":["Setting Up Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["ShipStation API allows you to subscribe to webhooks to receive real-time updates for long-running asynchronous operations. This allows your application to move on to other work while the operation is running rather than being blocked until it completes."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["It also allows ShipStation API to push updates to your application rather than having your application continually poll for updates. For example, you may subscribe to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["track"]}," webhook event to automatically receive an update anytime a tracking event occurs. Rather than continually sending a request to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/v1/labels/:label_id/track"]}," endpoint to see if the tracking information has been updated since the last time you checked, you can subscribe to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["track"]}," webhook event and ShipStation API will push the notification to your application via a webhook whenever the tracking details are updated."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"configuring-webhooks","__idx":1},"children":["Configuring Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Before you can begin receiving webhooks, you must configure your ShipStation API account with the HTTP endpoints you'd like for the webhooks to be sent to. You can do this through the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://dashboard.shipengine.com/"},"children":["ShipStation API Dashboard"]}," or through the API."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"requirements","__idx":2},"children":["Requirements"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You must be in the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Production"]}," environment of the ShipStation API dashboard to set up webhooks."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"configure-using-the-dashboard","__idx":3},"children":["Configure Using the Dashboard"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Log in to your ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://dashboard.shipengine.com/"},"children":["account Dashboard"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Go to ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Developer"]},", then ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Webhooks"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Click the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Add New Webhook"]}," button."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Select your ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Webhook Event"]}," and enter your ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Webhook URL"]},". You can set up multiple URLs for the same Event."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Click the green checkmark icon to save your webhook."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"img","attributes":{"src":"/assets/api-management.bbd8b4eb1f590307467648c65a31a772cda21ff00248a7423fcf79fd8d78a007.0f726a6c.png","alt":"Dashboard webhooks settings"},"children":[]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"configure-using-the-api","__idx":4},"children":["Configure Using the API"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To configure a webhook using the API, you'll need to provide a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["url"]}," and an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["event"]}," that will trigger the webhook. You'll send this data using the POST method to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/v1/environments/webhooks"]},". You may only configure one URL per event."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["NOTE:"]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"http-409-conflict","__idx":5},"children":["HTTP 409 Conflict"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you create a webhook for an event that already exists, you'll receive an HTTP 409 Conflict error when your request is sent. If this occurs, be sure to review the list of webhooks and delete the existing webhook for the event before resubmitting your request."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The payload for each type of webhook event will have a unique ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resource_type"]}," which indicates which type of event triggered the webhook."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can use the following event names and corresponding resource types in your payload when you configure a webhook through the API:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Event"},"children":["Event"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Resource Type"},"children":["Resource Type"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Batch completed"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batch"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_BATCH"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Shipment rate updated"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["rate"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_RATE"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Any tracking event"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["track"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_TRACK"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Carrier connected"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["carrier_connected"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_CARRIER_CONNECTED"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Sales Orders imported (Beta)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sales_orders_imported"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_SALES_ORDERS_IMPORTED"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Order Source refresh complete (Beta)"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["order_source_refresh_complete"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_ORDER_SOURCE_REFRESH_COMPLETE"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["A requested report is ready"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["report_complete"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["API_REPORT_COMPLETE"]}]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"example-request","__idx":6},"children":["Example Request"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This example uses a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["batch"]}," event."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["POST /v1/environment/webhooks"]}]},{"$$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://example.com/batch\",\n    \"event\": \"batch\"\n}\n","lang":"http"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"testing-webhooks","__idx":7},"children":["Testing Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can use a service like ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://webhook.site/"},"children":["Webhook.site"]}," to create temporary URLs to receive webhooks."," ","It will allow you to observe any HTTP requests the temporary URL receives. This will allow you to see the exact"," ","payload and headers sent from our system, before your application is ready to accept it."," ","Make sure to unregister the webhook after your testing is complete."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"validating-webhooks","__idx":8},"children":["Validating Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["ShipStation includes a digital signature (RSA-SHA256) in all outgoing webhooks."," ","This allows you to ensure requests received at your webhook URL were sent from"," ","our systems."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We have a ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"/apis/shipengine/docs/guides/webhooks"},"children":["full code example"]}," that demonstrates the steps."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-1-extract-the-signature-headers","__idx":9},"children":["Step 1: Extract the Signature Headers"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Extract the three signature headers from the incoming webhook request:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-shipengine-rsa-sha256-key-id"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-shipengine-rsa-sha256-signature"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-shipengine-timestamp"]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If these headers are not present, you should respond with an HTTP status ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["404"]}," ","and stop processing the request. This can help hide the existence of your"," ","webhook endpoint from anyone attempting to impersonate our service."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-2-validate-the-timestamp","__idx":10},"children":["Step 2: Validate the Timestamp"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Verify that the timestamp in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-shipengine-timestamp"]}," header is recent,"," ","in order to prevent replay attacks. Use your judgement on the age of webhooks"," ","you are willing to accept. Note that because of different server time skews, you may"," ","receive webhooks with timestamps in the future, so your code should account for that."," ","If the timestamp header is more than 5 minutes difference from the current"," ","time, you may want to respond with an HTTP status ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["400"]}," and stop processing the request."," ","If you encounter a lot of these rejections, you may want to double-check your server clocks, or"," ","increase the time range."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-3-get-the-raw-request-body","__idx":11},"children":["Step 3: Get the Raw Request Body"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Important"]},": You must use the raw, unparsed request body exactly as received."," ","Do not parse the JSON first and then re-serialize it, as this may change whitespace,"," ","property ordering, or encoding, which will cause signature verification to fail."," ","Ensure your web server framework provides access to the unparsed body."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-4-retrieve-the-public-key","__idx":12},"children":["Step 4: Retrieve the Public Key"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Fetch the JSON Web Key Set (JWKS) from our public endpoint: https://api.shipengine.com/jwks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The JWKS endpoint returns a standard ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://tools.ietf.org/html/rfc7517"},"children":["RFC 7517"]}," JSON Web Key Set containing our public keys. Find the key in the JWKS whose ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["kid"]}," (key ID) matches the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-shipengine-rsa-sha256-key-id"]}," header value."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The set of keys does not change very often, so it is generally safe to cache"," ","the JWKS response for a long period of time. If you receive a webhook request"," ","with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-shipengine-rsa-sha256-key-id"]}," value that is not in your cached copy,"," ","you should fetch the latest JWKS. The response includes an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ETag"]}," header, which"," ","you can pass in subsequent requests via the ",{"$$mdtype":"Tag","name":"a","attributes":{"href":"https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-None-Match"},"children":["If-None-Match"]}," ","header. If the contents hasn't changed, our JWKS endpoint will respond with status 304."," ","If it responds with a status 200, it means the JWKS has changed, has a new ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ETag"]},","," ","and you should update your cache."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["ShipStation may periodically rotate our signing keys. As long as you follow"," ","these guidelines related to fetching the JWKS, you should not have any service"," ","interruption. A public key  will always be present in the JWKS before"," ","we start using it for signing outgoing requests."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-5-verify-the-signature","__idx":13},"children":["Step 5: Verify the Signature"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To verify the signature, you must first construct the ",{"$$mdtype":"Tag","name":"em","attributes":{},"children":["signed payload"]},". This is"," ","the value that was hashed using our private key to produce the signature."," ","The signed payload is constructed by concatenating the value from the timestamp"," ","header, a literal period (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["."]},"), followed by the raw request body."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Example:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"2025-10-02T04:51:00Z.{\"resource_url\":\"https://api.shipengine.com/example\",\"resource_type\":\"EXAMPLE\"}\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use an RSA SHA-256 validation function on this signed payload, along with the"," ","public key from the previous step, to verify the signature."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If the signature validation fails, you should respond with an HTTP status ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["401"]},","," ","and discard the payload without any further processing."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"example","__idx":14},"children":["Example"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We've included a full working example of a NodeJS server that receives and"," ","validates webhooks, so that you can use it as a reference in your own implementation."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const crypto = require('crypto');\n\n// Cache for JWKS (in production, use a proper caching mechanism)\nlet jwksCache = null;\nlet jwksCacheETag = null;\n\nclass MissingHeadersError extends Error {}\nclass TimestampError extends Error {}\nclass SignatureError extends Error {}\n\n/**\n * Validates webhook signature\n * @throws {MissingHeadersError} When required headers are missing (should return 404)\n * @throws {TimestampError} When timestamp is out of range (should return 400)\n * @throws {SignatureError} When signature validation fails (should return 401)\n */\nasync function validateWebhookSignature(headers, rawBody) {\n  const keyId = headers['x-shipengine-rsa-sha256-key-id'];\n  const signature = headers['x-shipengine-rsa-sha256-signature'];\n  const timestamp = headers['x-shipengine-timestamp'];\n\n  if (!keyId || !signature || !timestamp) {\n    throw new MissingHeadersError('Missing required signature headers');\n  }\n\n  // Validate timestamp (5 minute window)\n  const webhookTime = new Date(timestamp);\n  const now = new Date();\n  const ageMinutes = (now - webhookTime) / 1000 / 60;\n\n  if (Math.abs(ageMinutes) > 5) {\n    throw new TimestampError(`Webhook timestamp too old or too far in future: ${ageMinutes} minutes`);\n  }\n\n  // Get public key\n  const publicKey = await getPublicKey(keyId);\n  if (!publicKey) {\n    throw new SignatureError(`Public key not found for kid: ${keyId}`);\n  }\n\n  // Construct signed payload\n  const signedPayload = `${timestamp}.${rawBody}`;\n\n  // Verify signature\n  const verify = crypto.createVerify('RSA-SHA256');\n  verify.update(signedPayload, 'utf8');\n  verify.end();\n\n  const isValid = verify.verify(\n    publicKey,\n    signature,\n    'base64'\n  );\n\n  if (!isValid) {\n    throw new SignatureError('Invalid webhook signature');\n  }\n\n  return true;\n}\n\n\n/**\n * Gets public key for a given key ID\n * Handles caching and automatic refresh if key not found\n * @returns Public key object or null if not found\n */\nasync function getPublicKey(keyId) {\n  // Try to find in cached JWKS\n  if (jwksCache) {\n    const jwk = jwksCache.keys.find(k => k.kid === keyId);\n    if (jwk) {\n      return jwkToPem(jwk);\n    }\n  }\n\n  // Key not found in cache, fetch fresh JWKS\n  jwksCache = null;\n  const jwks = await fetchJWKS();\n  const jwk = jwks.keys.find(k => k.kid === keyId);\n\n  if (!jwk) {\n    return null; // Key not found\n  }\n\n  return jwkToPem(jwk);\n}\n\n/**\n * Fetches the JWKS from ShipEngine\n */\nasync function fetchJWKS() {\n  const headers = {};\n  if (jwksCacheETag) {\n    headers['If-None-Match'] = jwksCacheETag;\n  }\n\n  const response = await fetch('https://api.shipengine.com/jwks', {\n    method: 'GET',\n    headers\n  });\n\n  if (response.status === 304 && jwksCache) {\n    // Not modified, use cache\n    return jwksCache;\n  }\n\n  if (!response.ok) {\n    throw new Error(`Failed to fetch JWKS: ${response.status}`);\n  }\n\n  jwksCache = await response.json();\n  jwksCacheETag = response.headers.get('etag');\n\n  return jwksCache;\n}\n\n/**\n * Converts JWK to PEM format public key\n */\nfunction jwkToPem(jwk) {\n  const modulus = Buffer.from(jwk.n, 'base64');\n  const exponent = Buffer.from(jwk.e, 'base64');\n\n  // Create public key from modulus and exponent\n  const key = crypto.createPublicKey({\n    key: {\n      kty: 'RSA',\n      n: jwk.n,\n      e: jwk.e\n    },\n    format: 'jwk'\n  });\n\n  return key;\n}\n\n// Express.js middleware example\nfunction webhookValidationMiddleware(req, res, next) {\n  // Capture raw body\n  let rawBody = '';\n\n  req.on('data', (chunk) => {\n    rawBody += chunk.toString('utf8');\n  });\n\n  req.on('end', async () => {\n    try {\n      await validateWebhookSignature(req.headers, rawBody);\n      req.body = JSON.parse(rawBody); // Now safe to parse\n      next();\n    } catch (error) {\n      console.error('Webhook validation failed:', error.message);\n\n      if (error instanceof MissingHeadersError) {\n        res.status(404).send();\n      } else if (error instanceof TimestampError) {\n        res.status(400).json({ error: error.message });\n      } else if (error instanceof SignatureError) {\n        res.status(401).json({ error: 'Invalid webhook signature' });\n      } else {\n        res.status(500).json({ error: 'Internal server error' });\n      }\n    }\n  });\n}\n\nconst express = require('express');\nconst app = express();\napp.post('/webhook', webhookValidationMiddleware, (req, res) => {\n  // Process validated webhook\n  console.log('Validated webhook:', req.body);\n  res.status(200).send('OK');\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n  console.log(`Webhook server listening on port ${PORT}`);\n});\n\n\n// How to run:\n// 1. Save this entire code block into a file named server.js\n// 2. npm install --save express\n// 3. npm start\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"webhook-payloads","__idx":15},"children":["Webhook Payloads"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Example payloads for each type of webhook are provided below. You can expect to receive a message with the same structure as these examples whenever you subscribe to the corresponding event. You'll notice that each payload includes a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resource_type"]}," and a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["resource_url"]},". Some payloads will contain additional information as well."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When ShipStation API dispatches a webhook, we allow 10 seconds for you to acknowledge you have successfully received the payload (your listener should return a 2xx response to us). If we don't receive an acknowledgement within 10 seconds, the system will put the payload back into the queue and make a maximum of ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["two additional attempts"]}," to dispatch the given payload. These attempts are typically separated by 30 minutes. However, this can swap to other timing intervals under certain conditions. If all three attempts receive no response, the event will be removed from the dispatch queue."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"example-batch-event-payload","__idx":16},"children":["Example Batch Event 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":"Heading","attributes":{"level":3,"id":"example-track-event-payload","__idx":17},"children":["Example Track Event Payload"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"resource_url\": \"https://api.shipengine.com/v1/tracking?carrier_code=usps&tracking_number=9400111298370264401222\",\n  \"resource_type\": \"API_TRACK\",\n  \"data\": {\n    \"label_url\": null,\n    \"tracking_number\": \"9400111298370264401222\",\n    \"status_code\": \"IT\",\n    \"carrier_detail_code\": null,\n    \"status_description\": \"In Transit\",\n    \"carrier_status_code\": \"NT\",\n    \"carrier_status_description\": \"Your package is moving within the USPS network and is on track to be delivered the expected delivery date. It is currently in transit to the next facility.\",\n    \"ship_date\": \"2020-06-30T16:09:00\",\n    \"estimated_delivery_date\": \"2020-07-06T00:00:00\",\n    \"actual_delivery_date\": null,\n    \"exception_description\": null,\n    \"events\": [\n      {\n        \"occurred_at\": \"2020-07-02T00:00:00Z\",\n        \"carrier_occurred_at\": \"2020-07-02T00:00:00\",\n        \"description\": \"In Transit, Arriving On Time\",\n        \"city_locality\": \"\",\n        \"state_province\": \"\",\n        \"postal_code\": \"\",\n        \"country_code\": \"\",\n        \"company_name\": \"\",\n        \"signer\": \"\",\n        \"event_code\": \"NT\",\n        \"event_description\": \"In Transit, Arriving on Time\",\n        \"carrier_detail_code\": null,\n        \"status_code\": null,\n        \"latitude\": null,\n        \"longitude\": null\n      },\n      {\n        \"occurred_at\": \"2020-06-30T20:09:00Z\",\n        \"carrier_occurred_at\": \"2020-06-30T16:09:00\",\n        \"description\": \"Shipment Received, Package Acceptance Pending\",\n        \"city_locality\": \"VERSAILLES\",\n        \"state_province\": \"KY\",\n        \"postal_code\": \"40383\",\n        \"country_code\": \"\",\n        \"company_name\": \"\",\n        \"signer\": \"\",\n        \"event_code\": \"TM\",\n        \"event_description\": \"Shipment Received, Package Acceptance Pending\",\n        \"carrier_detail_code\": null,\n        \"status_code\": null,\n        \"latitude\": 37.8614,\n        \"longitude\": -84.6646\n      }\n    ]\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["INFO:"]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"event-timestamps","__idx":18},"children":["Event Timestamps"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["carrier_occurred_at"]}," is the timestamp of the event received from the carrier. It is assumed to be the local time of where the event occurred. Please note that this event property is not yet fully supported across all carriers."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["occurred_at"]}," is the UTC based time of the event's occurrence."]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"example-rate-event-payload","__idx":19},"children":["Example Rate Event Payload"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"resource_url\": \"https://api.shipengine.com/v1/shipments/se-2120221/rates\",\n  \"resource_type\": \"API_RATE\"\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"example-carrier-connected-event-payload","__idx":20},"children":["Example Carrier Connected Event Payload"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"resource_url\": \"https://api.shipengine.com/v1/carriers/se-1234\",\n  \"resource_type\": \"API_CARRIER_CONNECTED\"\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"example-report-complete-event-payload","__idx":21},"children":["Example Report Complete Event Payload"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"resource_url\": \"https://api.shipengine.com/adjustments/se-0bdf1f26-5708-4e0b-a548-fd2a5720779f\",\n  \"resource_type\": \"API_REPORT_COMPLETE\"\n}\n\n","lang":"json"},"children":[]}]},"headings":[{"value":"Setting Up Webhooks","id":"setting-up-webhooks","depth":1},{"value":"Configuring Webhooks","id":"configuring-webhooks","depth":2},{"value":"Requirements","id":"requirements","depth":3},{"value":"Configure Using the Dashboard","id":"configure-using-the-dashboard","depth":3},{"value":"Configure Using the API","id":"configure-using-the-api","depth":3},{"value":"HTTP 409 Conflict","id":"http-409-conflict","depth":3},{"value":"Example Request","id":"example-request","depth":4},{"value":"Testing Webhooks","id":"testing-webhooks","depth":2},{"value":"Validating Webhooks","id":"validating-webhooks","depth":2},{"value":"Step 1: Extract the Signature Headers","id":"step-1-extract-the-signature-headers","depth":3},{"value":"Step 2: Validate the Timestamp","id":"step-2-validate-the-timestamp","depth":3},{"value":"Step 3: Get the Raw Request Body","id":"step-3-get-the-raw-request-body","depth":3},{"value":"Step 4: Retrieve the Public Key","id":"step-4-retrieve-the-public-key","depth":3},{"value":"Step 5: Verify the Signature","id":"step-5-verify-the-signature","depth":3},{"value":"Example","id":"example","depth":3},{"value":"Webhook Payloads","id":"webhook-payloads","depth":2},{"value":"Example Batch Event Payload","id":"example-batch-event-payload","depth":3},{"value":"Example Track Event Payload","id":"example-track-event-payload","depth":3},{"value":"Event Timestamps","id":"event-timestamps","depth":3},{"value":"Example Rate Event Payload","id":"example-rate-event-payload","depth":3},{"value":"Example Carrier Connected Event Payload","id":"example-carrier-connected-event-payload","depth":3},{"value":"Example Report Complete Event Payload","id":"example-report-complete-event-payload","depth":3}],"frontmatter":{"seo":{"title":"Setting Up Webhooks"}},"lastModified":"2026-04-08T10:47:45.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/apis/shipengine/docs/guides/webhooks","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}