Webhooks v2.0

Introduction

ParcelPanel Webhooks allow your application to receive real-time notifications about shipment events. Instead of continuously polling our system for updates, webhooks push information to your server as events occur, allowing you to build integrations that react immediately to changes in shipment status.

This document describes how to integrate with ParcelPanel's Webhook system to receive shipment tracking updates.

How Webhooks Work

  1. You register a webhook URL in your ParcelPanel account
  2. ParcelPanel sends HTTP POST requests to your URL when shipment events occur
  3. Your server processes these notifications and takes appropriate actions

Setting Up a Webhook Endpoint

To receive webhooks from ParcelPanel, you need to:

  1. Create an HTTP endpoint on your server that can receive POST requests
  2. Register this endpoint URL in your ParcelPanel dashboard
  3. Implement verification logic to validate incoming webhooks
  4. Process the webhook data according to your business needs

Webhook Notifications

HTTP Headers

Each webhook request includes the following HTTP headers:

HeaderDescription
Content-TypeAlways application/json
X-ParcelPanel-HMAC-SHA256Base64-encoded HMAC-SHA256 signature for request verification
X-ParcelPanel-TopicType of event that triggered the webhook (e.g., shipment_status/delivered)
X-ParcelPanel-Triggered-AtISO8601 timestamp when the webhook was triggered
X-ParcelPanel-Webhook-IdUnique identifier for the webhook request
X-ParcelPanel-Webhook-VersionVersion of the webhook (2.0)

Event Types

ParcelPanel generates webhook events for various shipment status changes:

Event TypeDescription
shipment_status/any_updateWhen any update to the shipment occurs
shipment_status/info_receivedWhen shipping information is received by carrier
shipment_status/in_transitWhen package is in transit
shipment_status/out_for_deliveryWhen package is out for delivery
shipment_status/ready_for_pickupWhen package is ready for pickup
shipment_status/deliveredWhen package is delivered
shipment_status/failed_attemptWhen delivery attempt fails
shipment_status/exceptionWhen a delivery exception occurs

Request Body Structure

Webhook notifications contain detailed information about the shipment event in JSON format:

{
  "order_id": 6140516335690,
  "order_number": "#1030",
  "order_tags": ["ParcelPanel", "Webhook2.0"],
  "store": {
    "name": "Example Store",
    "url": "https://example.myshopify.com"
  },
  "customer": {
    "name": "Aaliyah Bins",
    "email": "[email protected]",
    "phone": "12345678901"
  },
  "shipping_address": {
    "name": "Aaliyah Bins",
    "phone": "12345678901",
    "country": "United States",
    "country_code": "US",
    "province": "California",
    "province_code": "CA",
    "city": "Mountain View",
    "zip": "94043",
    "address1": "1600 Amphitheatre Parkway",
    "address2": null,
    "company": "Googleplex"
  },
  "tracking_link": "https://example.myshopify.com/apps/parcelpanel?nums=YT2436021211003147",
  "status": "DELIVERED",
  "status_label": "Delivered",
  "substatus": "Delivered_001",
  "substatus_label": "Delivered",
  "tracking_number": "YT2436021211003147",
  "carrier": {
    "name": "YunExpress",
    "code": "yunexpress",
    "contact": "400-8575-500",
    "logo_url": "https://cdn.parcelpanel.com/assets/common/images/express/yunexpress.png",
    "url": "http://www.yuntrack.com/track/detail?id=YT2436021211003147"
  },
  "transit_time": 11,
  "residence_time": 1,
  "estimated_delivery_date": {
    "source": "CUSTOM",
    "display_text": "Jan 12, 2025 - Jan 16, 2025"
  },
  "order_date": "2024-12-23T01:02:40+00:00",
  "fulfillment_date": "2024-12-24T01:25:40+00:00",
  "pickup_date": "2025-01-03T01:28:40",
  "delivery_date": "2025-01-13T14:36:00",
  "last_mile_tracking_supported": true,
  "last_mile": {
    "carrier_name": "USPS",
    "carrier_code": "usps",
    "tracking_number": "9261290358097849005373",
    "carrier_contact": "1 800-275-8777",
    "carrier_logo_url": "https://cdn.parcelpanel.com/assets/common/images/express/usps.png",
    "carrier_url": "https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=9261290358097849005373"
  },
  "products": [
    {
      "id": 6941337714762,
      "title": "Amazing Aluminum Bag Collection10",
      "variant_id": 39969756151882,
      "variant_title": "xs / red / water",
      "quantity": 3,
      "image_url": "https://cdn.shopify.com/s/files/1/0563/3435/2458/products/AAUvwnj0ICORVuxs41ODOvnhvedArLiSV20df7r8XBjEUQ_s900-c-k-c0x00ffffff-no-rj_147ad8db-b4f4-40a5-b751-c0a8428b8ae7-951612.jpg?v=1678447639",
      "url": "https://example.myshopify.com/apps/parcelpanel/bland?q=eyJ0IjoxNzQ1NTY1MzAzLCJwcm9kdWN0X2lkIjo2OTQxMzM3NzE0NzYyLCJoYW5kbGUiOiJhbWF6aW5nLWFsdW1pbnVtLWJhZy1jb2xsZWN0aW9uMTAiLCJmcm9tIjoicGFyY2VscGFuZWwtYWRtaW4ifQ"
    }
  ],
  "checkpoints": [
    {
      "detail": "Delivered, In/At Mailbox, FAIRBANKS, AK 99701",
      "status": "DELIVERED",
      "status_label": "Delivered",
      "substatus": "Delivered_001",
      "substatus_label": "Delivered",
      "checkpoint_time": "2025-01-13T14:36:00"
    },
    {
      "detail": "Out for Delivery, FAIRBANKS, AK 99701",
      "status": "OUT_FOR_DELIVERY",
      "status_label": "Out for delivery",
      "substatus": "OutForDelivery_001",
      "substatus_label": "Out for delivery",
      "checkpoint_time": "2025-01-13T06:45:00"
    }
  ]
}

Webhook Verification

To ensure webhook requests are coming from ParcelPanel, you should verify the signature included in each request:

  1. Retrieve the signature from the X-ParcelPanel-HMAC-SHA256 header
  2. Calculate an HMAC-SHA256 hash of the raw request body using your API key as the key
  3. Compare the calculated signature with the received signature
  4. If they match, process the webhook; otherwise, reject it

Example in Node.js

const crypto = require('crypto');

function verifyWebhookSignature(requestBody, signature, apiKey) {
  const calculatedSignature = crypto
    .createHmac('sha256', apiKey)
    .update(requestBody)
    .digest('base64');
  
  try {
    return crypto.timingSafeEqual(
      Buffer.from(calculatedSignature),
      Buffer.from(signature)
    );
  } catch (e) {
    return false;
  }
}

Example in Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class WebhookSignatureVerifier {
    
    public static boolean verifySignature(String requestBody, String signature, String apiKey) {
        try {
            Mac hmacSHA256 = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(apiKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            hmacSHA256.init(secretKey);
            
            byte[] calculatedSignatureBytes = hmacSHA256.doFinal(requestBody.getBytes(StandardCharsets.UTF_8));
            String calculatedSignature = Base64.getEncoder().encodeToString(calculatedSignatureBytes);
            
            return signature.equals(calculatedSignature);
        } catch (Exception e) {
            return false;
        }
    }
}

Example in Go

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"crypto/subtle"
)

func VerifyWebhookSignature(requestBody []byte, signature string, apiKey string) bool {
	mac := hmac.New(sha256.New, []byte(apiKey))
	mac.Write(requestBody)
	
	calculatedSignature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
	
	return subtle.ConstantTimeCompare([]byte(calculatedSignature), []byte(signature)) == 1
}

Example in PHP

function verifyWebhookSignature($requestBody, $signature, $apiKey) {
    $calculatedSignature = base64_encode(hash_hmac('sha256', $requestBody, $apiKey, true));
    
    return hash_equals($calculatedSignature, $signature);
}

Example in C#

using System;
using System.Security.Cryptography;
using System.Text;

public class WebhookSignatureVerifier
{
    public static bool VerifySignature(string requestBody, string signature, string apiKey)
    {
        try
        {
            using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiKey)))
            {
                byte[] hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(requestBody));
                string calculatedSignature = Convert.ToBase64String(hash);
                
                // Use time-constant string comparison to prevent timing attacks
                return string.Equals(calculatedSignature, signature, StringComparison.Ordinal);
            }
        }
        catch
        {
            return false;
        }
    }
}

Example in Ruby

require 'openssl'
require 'base64'

def verify_webhook_signature(request_body, signature, api_key)
  calculated_signature = Base64.strict_encode64(
    OpenSSL::HMAC.digest('sha256', api_key, request_body)
  )
  
  # Use secure comparison method to prevent timing attacks
  ActiveSupport::SecurityUtils.secure_compare(calculated_signature, signature)
rescue
  false
end

Shipment Status Codes

ParcelPanel uses the following status codes to indicate the current state of a shipment:

Main Status Codes

Status CodeDescription
PENDINGOrder created but not yet shipped
INFO_RECEIVEDShipping information received by carrier
IN_TRANSITPackage is in transit
OUT_FOR_DELIVERYPackage out for delivery today
READY_FOR_PICKUPPackage is ready for pickup
DELIVEREDPackage has been delivered
EXCEPTIONDelivery exception occurred
FAILED_ATTEMPTDelivery attempt failed
EXPIREDTracking information is no longer available

Substatus Codes

In addition to the main status codes, ParcelPanel provides more granular tracking information through substatus codes. These codes offer more detailed information about the shipment's current state:

CodeMain StatusDescription
Pending_001PENDINGPending
Pending_002PENDINGOrder processed
InfoReceived_001INFO_RECEIVEDShipping information received
InTransit_001IN_TRANSITIn transit
InTransit_002IN_TRANSITDeparted from facility
InTransit_003IN_TRANSITArrived at facility
InTransit_004IN_TRANSITCustoms clearance completed
InTransit_005IN_TRANSITCustoms clearance delay
InTransit_006IN_TRANSITIn transit to next facility
InTransit_007IN_TRANSITInternational shipment release
OutForDelivery_001OUT_FOR_DELIVERYOut for delivery
OutForDelivery_002OUT_FOR_DELIVERYOut for delivery again
ReadyForPickup_001READY_FOR_PICKUPReady for pickup
Delivered_001DELIVEREDDelivered
Delivered_002DELIVEREDDelivered to agent
Delivered_003DELIVEREDDelivered to neighbor
Delivered_004DELIVEREDDelivered to pickup point
Exception_001EXCEPTIONUnknown exception
Exception_002EXCEPTIONDelivery exception
Exception_003EXCEPTIONReturned to sender
Exception_004EXCEPTIONAddress issue
Exception_005EXCEPTIONDamaged
Exception_006EXCEPTIONLost
Exception_007EXCEPTIONHeld at customs
Exception_008EXCEPTIONDelivery rescheduled
FailedAttempt_001FAILED_ATTEMPTFailed attempt
FailedAttempt_002FAILED_ATTEMPTRecipient not available
FailedAttempt_003FAILED_ATTEMPTOffice closed
FailedAttempt_004FAILED_ATTEMPTWeather delay
Expired_001EXPIREDTracking expired

For full reference documentation on all status codes, please refer to the ParcelPanel API v2.

These status codes are used consistently across both the API and Webhook systems.

Best Practices

  1. Respond quickly: Your endpoint should return a 2xx response as soon as possible
  2. Process asynchronously: Handle the webhook processing in a background job
  3. Handle duplicates: Implement idempotent processing to handle potential duplicate events
  4. Log events: Keep logs of received webhooks for debugging purposes
  5. Retry mechanism: Implement retry logic for any actions that might fail

Troubleshooting

Common Issues

  1. Not receiving webhooks
  2. Signature verification failures
  3. Webhook processing errors

Webhook Retry Policy

If ParcelPanel cannot deliver a webhook (receives a non-2xx response code), it will retry with an exponential backoff algorithm for a total of 5 attempts:

  • First retry: 10 seconds after failure
  • Second retry: 30 seconds after failure
  • Third retry: 60 seconds after failure
  • Fourth retry: 120 seconds (2 minutes) after failure
  • Fifth retry: 300 seconds (5 minutes) after failure

After all 5 retry attempts are exhausted, the webhook will be dropped and will not be sent again.

Testing Your Webhook

You can test your webhook integration by:

  1. Setting up your endpoint to log all incoming requests
  2. Configuring the webhook URL in your ParcelPanel dashboard
  3. Triggering test events from the ParcelPanel webhook settings page
  4. Verifying your endpoint successfully processes the test webhooks

Support

If you're experiencing issues with webhooks or have questions about integration, please contact our support team at [email protected].