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
- You register a webhook URL in your ParcelPanel account
- ParcelPanel sends HTTP POST requests to your URL when shipment events occur
- Your server processes these notifications and takes appropriate actions
Setting Up a Webhook Endpoint
To receive webhooks from ParcelPanel, you need to:
- Create an HTTP endpoint on your server that can receive POST requests
- Register this endpoint URL in your ParcelPanel dashboard
- Implement verification logic to validate incoming webhooks
- Process the webhook data according to your business needs
Webhook Notifications
HTTP Headers
Each webhook request includes the following HTTP headers:
Header | Description |
---|---|
Content-Type | Always application/json |
X-ParcelPanel-HMAC-SHA256 | Base64-encoded HMAC-SHA256 signature for request verification |
X-ParcelPanel-Topic | Type of event that triggered the webhook (e.g., shipment_status/delivered ) |
X-ParcelPanel-Triggered-At | ISO8601 timestamp when the webhook was triggered |
X-ParcelPanel-Webhook-Id | Unique identifier for the webhook request |
X-ParcelPanel-Webhook-Version | Version of the webhook (2.0) |
Event Types
ParcelPanel generates webhook events for various shipment status changes:
Event Type | Description |
---|---|
shipment_status/any_update | When any update to the shipment occurs |
shipment_status/info_received | When shipping information is received by carrier |
shipment_status/in_transit | When package is in transit |
shipment_status/out_for_delivery | When package is out for delivery |
shipment_status/ready_for_pickup | When package is ready for pickup |
shipment_status/delivered | When package is delivered |
shipment_status/failed_attempt | When delivery attempt fails |
shipment_status/exception | When 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:
- Retrieve the signature from the
X-ParcelPanel-HMAC-SHA256
header - Calculate an HMAC-SHA256 hash of the raw request body using your API key as the key
- Compare the calculated signature with the received signature
- 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 Code | Description |
---|---|
PENDING | Order created but not yet shipped |
INFO_RECEIVED | Shipping information received by carrier |
IN_TRANSIT | Package is in transit |
OUT_FOR_DELIVERY | Package out for delivery today |
READY_FOR_PICKUP | Package is ready for pickup |
DELIVERED | Package has been delivered |
EXCEPTION | Delivery exception occurred |
FAILED_ATTEMPT | Delivery attempt failed |
EXPIRED | Tracking 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:
Code | Main Status | Description |
---|---|---|
Pending_001 | PENDING | Pending |
Pending_002 | PENDING | Order processed |
InfoReceived_001 | INFO_RECEIVED | Shipping information received |
InTransit_001 | IN_TRANSIT | In transit |
InTransit_002 | IN_TRANSIT | Departed from facility |
InTransit_003 | IN_TRANSIT | Arrived at facility |
InTransit_004 | IN_TRANSIT | Customs clearance completed |
InTransit_005 | IN_TRANSIT | Customs clearance delay |
InTransit_006 | IN_TRANSIT | In transit to next facility |
InTransit_007 | IN_TRANSIT | International shipment release |
OutForDelivery_001 | OUT_FOR_DELIVERY | Out for delivery |
OutForDelivery_002 | OUT_FOR_DELIVERY | Out for delivery again |
ReadyForPickup_001 | READY_FOR_PICKUP | Ready for pickup |
Delivered_001 | DELIVERED | Delivered |
Delivered_002 | DELIVERED | Delivered to agent |
Delivered_003 | DELIVERED | Delivered to neighbor |
Delivered_004 | DELIVERED | Delivered to pickup point |
Exception_001 | EXCEPTION | Unknown exception |
Exception_002 | EXCEPTION | Delivery exception |
Exception_003 | EXCEPTION | Returned to sender |
Exception_004 | EXCEPTION | Address issue |
Exception_005 | EXCEPTION | Damaged |
Exception_006 | EXCEPTION | Lost |
Exception_007 | EXCEPTION | Held at customs |
Exception_008 | EXCEPTION | Delivery rescheduled |
FailedAttempt_001 | FAILED_ATTEMPT | Failed attempt |
FailedAttempt_002 | FAILED_ATTEMPT | Recipient not available |
FailedAttempt_003 | FAILED_ATTEMPT | Office closed |
FailedAttempt_004 | FAILED_ATTEMPT | Weather delay |
Expired_001 | EXPIRED | Tracking 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
- Respond quickly: Your endpoint should return a 2xx response as soon as possible
- Process asynchronously: Handle the webhook processing in a background job
- Handle duplicates: Implement idempotent processing to handle potential duplicate events
- Log events: Keep logs of received webhooks for debugging purposes
- Retry mechanism: Implement retry logic for any actions that might fail
Troubleshooting
Common Issues
- Not receiving webhooks
- Signature verification failures
- 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:
- Setting up your endpoint to log all incoming requests
- Configuring the webhook URL in your ParcelPanel dashboard
- Triggering test events from the ParcelPanel webhook settings page
- 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].