Parcel Tracking Security: Protecting Shipment Data and API Keys
Shipping data is more sensitive than most developers realize. A tracking number can reveal purchase history, home addresses, delivery patterns, and business relationships. As you build tracking integrations, security should be a first-class concern — not an afterthought. This guide covers the essential security practices for any application that handles parcel tracking data.
Securing Your API Keys
Never Expose Keys in Client-Side Code
The most common mistake: embedding API keys in frontend JavaScript.
// WRONG — API key visible to anyone who opens browser DevTools
const response = await fetch('https://api.whereparcel.com/v2/track', {
headers: { 'Authorization': 'Bearer wp_live_abc123...:sk_live_xyz789...' },
});
Instead, proxy requests through your backend:
// Frontend — calls your own API
const response = await fetch('/api/tracking', {
method: 'POST',
body: JSON.stringify({ trackingNumber: '123456789012' }),
});
// Backend — adds API key server-side
app.post('/api/tracking', async (req, res) => {
const result = await fetch('https://api.whereparcel.com/v2/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WHEREPARCEL_API_KEY}:${process.env.WHEREPARCEL_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
trackingItems: [{ carrier: req.body.carrier, trackingNumber: req.body.trackingNumber }],
}),
});
res.json(await result.json());
});
Use Environment Variables
Store API keys in environment variables, never in code:
# .env (never commit this file)
WHEREPARCEL_API_KEY=wp_live_abc123...
# .gitignore
.env
.env.local
.env.production
Rotate Keys Regularly
Set a schedule for key rotation and use the WhereParcel dashboard to manage multiple active keys:
- Generate a new API key
- Deploy the new key to your application
- Verify the new key works in production
- Revoke the old key
Preventing Tracking Number Enumeration
Tracking numbers are often sequential or follow predictable patterns. Without proper protection, attackers can enumerate tracking numbers to access other customers’ shipment data.
Rate Limit Your Tracking Endpoint
import rateLimit from 'express-rate-limit';
const trackingLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 30, // 30 requests per window per IP
message: { error: 'Too many tracking requests, please try again later' },
standardHeaders: true,
});
app.use('/api/tracking', trackingLimiter);
Require Authentication
Don’t allow anonymous tracking lookups if you can avoid it:
app.post('/api/tracking', authenticate, async (req, res) => {
const { trackingNumber } = req.body;
// Verify this tracking number belongs to the authenticated user
const shipment = await db.shipments.findOne({
trackingNumber,
userId: req.user.id,
});
if (!shipment) {
return res.status(404).json({ error: 'Shipment not found' });
}
// Only then fetch tracking data
const tracking = await getTracking(trackingNumber, shipment.carrier);
res.json(tracking);
});
Add Request Fingerprinting
Detect and block enumeration attempts:
async function detectEnumeration(req) {
const key = `tracking:${req.ip}`;
const recentNumbers = await redis.lrange(key, 0, -1);
// Track which numbers this IP has queried
await redis.lpush(key, req.body.trackingNumber);
await redis.ltrim(key, 0, 99); // Keep last 100
await redis.expire(key, 3600); // 1 hour window
// Flag if querying many different tracking numbers
const uniqueNumbers = new Set(recentNumbers).size;
if (uniqueNumbers > 20) {
await flagSuspiciousActivity(req.ip, 'tracking_enumeration');
return true;
}
return false;
}
Protecting Sensitive Data
Minimize Data Storage
Only store what you need:
// Store minimal tracking data
const trackingRecord = {
trackingNumber: shipment.trackingNumber,
carrier: shipment.carrier,
currentStatus: tracking.status,
lastUpdated: tracking.lastEvent.timestamp,
// DON'T store: full address, recipient name, package contents
};
Mask Tracking Numbers in Logs
Never log full tracking numbers in application logs:
function maskTrackingNumber(number) {
if (number.length <= 6) return '***';
return number.slice(0, 3) + '*'.repeat(number.length - 6) + number.slice(-3);
}
// Log: "Tracking lookup for 123******890"
logger.info(`Tracking lookup for ${maskTrackingNumber(trackingNumber)}`);
Encrypt Data at Rest
If you cache tracking data, encrypt sensitive fields:
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
function encryptField(text, key) {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
Webhook Security
When receiving tracking updates via webhooks, verify the source:
Validate Webhook Signatures
import crypto from 'crypto';
app.post('/webhooks/tracking', (req, res) => {
const signature = req.headers['x-whereparcel-signature'];
const payload = JSON.stringify(req.body);
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
res.status(200).json({ received: true });
});
Use HTTPS Only
Always use HTTPS for webhook endpoints. Reject HTTP webhook registrations.
Verify Webhook Source IPs
Optionally restrict webhook processing to WhereParcel’s IP ranges:
const ALLOWED_IPS = [
// WhereParcel webhook IPs — check docs for current list
'203.0.113.0/24',
];
app.post('/webhooks/tracking', verifySourceIP(ALLOWED_IPS), processWebhook);
Data Privacy Compliance
GDPR Considerations
If you serve European customers:
- Data minimization — Only collect tracking data you need
- Right to erasure — Be able to delete a customer’s tracking history
- Purpose limitation — Don’t use tracking data for purposes beyond delivery tracking
- Data retention — Set automatic deletion policies for old tracking data
// Automatic data cleanup
async function cleanupOldTrackingData() {
const retentionPeriod = 90; // days
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - retentionPeriod);
await db.trackingEvents.deleteMany({
createdAt: { $lt: cutoff },
});
}
Cross-Border Data Considerations
Tracking data crosses borders by nature. Be aware of:
- Korea’s PIPA — Personal Information Protection Act applies to Korean shipment data
- Japan’s APPI — Act on Protection of Personal Information
- China’s PIPL — Personal Information Protection Law
WhereParcel processes data in compliance with these regulations, but your application must also handle the data responsibly.
Security Checklist
Use this checklist for your tracking integration:
- API keys stored in environment variables, not code
- API calls proxied through backend, not made from client
- Tracking endpoint rate-limited
- Authentication required for tracking lookups
- Tracking numbers not logged in full
- Webhook signatures validated
- HTTPS enforced for all endpoints
- Data retention policy implemented
- Old tracking data automatically purged
- API key rotation schedule defined
- Enumeration detection in place
Summary
Security in shipping integrations comes down to a few core principles: protect your API keys, control access to tracking data, verify webhook sources, and respect data privacy regulations. These practices protect both your business and your customers’ sensitive shipment information.
For API authentication details, see our Getting Started guide. For webhook security, see Webhook Best Practices.