Documentation Index
Fetch the complete documentation index at: https://packageretriever.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Migrating from ShipStation
ShipStation moved API access to their $99/month Gold Plan. Package Retriever’s API is free — no subscription, no per-label markup, no surcharge for using your own carrier accounts.
This guide maps every ShipStation API concept to its Package Retriever equivalent so you can migrate your existing integration.
What you’re getting
| Feature | ShipStation | Package Retriever |
|---|
| API access | $99/month (Gold Plan) | Free |
| BYOA (own carrier accounts) | +$20/month surcharge | No surcharge |
| Rate limits | Undocumented | Published in docs |
| Sandbox environment | Locked behind Gold Plan | Free, test key prefix |
| Webhook retry behavior | Undocumented | 5 attempts, published schedule |
| Carbon emissions per label | Not available | Included on every rate |
| Multi-carrier rate shopping | Yes | Yes |
| Batch label creation | Yes (500 max) | Yes (5,000 max) |
Authentication
ShipStation uses HTTP Basic Auth with an API Key and Secret. Package Retriever uses a single Bearer token.
# ShipStation
curl -u "API_KEY:API_SECRET" https://ssapi.shipstation.com/orders
# Package Retriever
curl -H "Authorization: Bearer pr_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" \
https://api.packageretriever.com/v1/rates
One header, one key, no base64 encoding.
Endpoint mapping
| ShipStation Endpoint | Method | Package Retriever Equivalent | Status |
|---|
/shipments/createlabel | POST | POST /v1/labels | Available |
/shipments/getrates | POST | POST /v1/rates | Available |
/shipments/voidlabel | POST | DELETE /v1/labels/{id} | Available |
/shipments | GET | GET /v1/labels/{id} | Available |
/carriers | GET | GET /v1/carrier-accounts | Available |
/carriers/listservices | GET | Included in rate response | Available |
/webhooks/subscribe | POST | Dashboard settings (single URL) | Available |
/orders | GET | Not applicable (use your marketplace integration) | — |
/orders/createorder | POST | Not applicable | — |
/shipments/createshipment | POST | POST /v1/batches | Available |
/accounts/listtags | GET | Not applicable | — |
ShipStation:
const response = await fetch('https://ssapi.shipstation.com/shipments/getrates', {
method: 'POST',
headers: {
'Authorization': 'Basic ' + btoa(`${API_KEY}:${API_SECRET}`),
'Content-Type': 'application/json'
},
body: JSON.stringify({
carrierCode: 'stamps_com',
fromPostalCode: '94105',
toPostalCode: '78701',
toCountry: 'US',
weight: { value: 16, units: 'ounces' },
dimensions: { length: 9, width: 6, height: 2, units: 'inches' }
})
});
Package Retriever:
import PackageRetriever from '@packageretriever/sdk';
const pr = new PackageRetriever('pr_live_YOUR_KEY');
const rates = await pr.rates.get({
from_address: { name: 'Warehouse', street1: '417 Montgomery St', city: 'San Francisco', state: 'CA', zip: '94105', country: 'US' },
to_address: { name: 'Customer', street1: '123 Main St', city: 'Austin', state: 'TX', zip: '78701', country: 'US' },
parcel: { weight_oz: 16, length: 9, width: 6, height: 2 }
});
// rates.rates is sorted cheapest-first by default
// Every carrier returned in one response — USPS, UPS, FedEx, Sendle
console.log(rates.rates[0]);
// { carrier: 'USPS', service: 'Ground Advantage', rate_cents: 542, carbon_grams: 142, ... }
Key differences:
- All carriers returned in a single response (ShipStation requires separate calls per carrier)
- Sorted cheapest-first by default
carbon_grams included on every rate
- Full address required (not just postal codes) — enables residential surcharge detection
Label creation
ShipStation:
const label = await fetch('https://ssapi.shipstation.com/shipments/createlabel', {
method: 'POST',
headers: { 'Authorization': 'Basic ' + btoa(`${API_KEY}:${API_SECRET}`) },
body: JSON.stringify({
carrierCode: 'stamps_com',
serviceCode: 'usps_ground_advantage',
packageCode: 'package',
weight: { value: 16, units: 'ounces' },
shipFrom: { /* ... */ },
shipTo: { /* ... */ },
testLabel: false
})
});
Package Retriever:
// Step 1: Get rates (already done above)
const rates = await pr.rates.get({ /* ... */ });
// Step 2: Purchase the cheapest rate
const label = await pr.labels.create({ rate_id: rates.rates[0].id });
console.log(label.tracking_number); // 9400111899223408065744
console.log(label.label_url); // PDF download URL
console.log(label.rate_cents); // 542
Key differences:
- Two-step flow: get rates first, then purchase by
rate_id
- Payment is via prepaid wallet (not per-transaction card charge)
wallet_balance_after_cents returned on every purchase so you always know your balance
Sandbox / testing
ShipStation: Requires Gold Plan ($99/month) to access API at all. No separate test environment.
Package Retriever:
// Sandbox — use pr_test_ prefix. No billing, no real labels.
const pr = new PackageRetriever('pr_test_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4');
// Every call works identically — returns realistic responses
const rates = await pr.rates.get({ /* ... */ });
const label = await pr.labels.create({ rate_id: rates.rates[0].id });
// label.tracking_number starts with 9999 (sandbox indicator)
// label.label_url points to a sample PDF
// Wallet is infinite in sandbox mode
No separate environment to configure. Same base URL, same endpoints. Just use a test key.
BYOA (Bring Your Own Account)
ShipStation: $20/month surcharge per carrier account.
Package Retriever: Free. Connect via dashboard, use in API automatically.
// Your BYOA rates appear alongside platform rates — no extra configuration
const rates = await pr.rates.get({ /* ... */ });
// BYOA rates are labeled with account_type: 'byoa'
rates.rates.forEach(rate => {
console.log(`${rate.carrier} ${rate.service}: $${rate.rate_cents / 100} (${rate.account_type})`);
});
// USPS Ground Advantage: $5.42 (platform)
// UPS Ground: $7.18 (byoa) ← your negotiated UPS rate
// FedEx Home Delivery: $8.91 (byoa) ← your negotiated FedEx rate
Webhooks
ShipStation: Multiple webhook types, complex subscription management.
Package Retriever: One event (label.created), one URL, simple HMAC verification.
// Verify webhook signature (Node.js)
import crypto from 'crypto';
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return signature === `sha256=${expected}`;
}
// In your webhook handler:
app.post('/webhooks/pr', (req, res) => {
const isValid = verifySignature(
JSON.stringify(req.body),
req.headers['pr-signature'],
process.env.PR_WEBHOOK_SECRET
);
if (!isValid) return res.status(401).send('Invalid signature');
const { event, data } = req.body;
// event = 'label.created'
// data = full label object (tracking_number, label_url, carrier, etc.)
res.status(200).send('OK');
});
Retry schedule (published — unlike ShipStation):
- Attempt 1: Immediately
- Attempt 2: 5 minutes
- Attempt 3: 30 minutes
- Attempt 4: 2 hours
- Attempt 5: 24 hours
Batch label creation
ShipStation: Max 500 labels per batch.
Package Retriever: Max 5,000 labels per batch with parallel processing.
const batch = await pr.batches.create({
items: orders.map(order => ({
from_address: warehouse,
to_address: order.address,
parcel: order.parcel,
rate_id: order.selectedRateId,
carrier: order.carrier,
service: order.service,
rate_cents: order.rateCents
}))
});
// Full batch cost deducted from wallet upfront
console.log(batch.total_cost_cents); // 27100
console.log(batch.wallet_balance_after_cents); // 47300
// Start processing
await pr.batches.buy(batch.id);
// Poll for progress
const status = await pr.batches.get(batch.id);
console.log(status.estimated_completion_percentage); // 45
console.log(status.estimated_time_remaining_seconds); // 120
Error handling
ShipStation returns carrier-native error strings. Package Retriever normalizes every error to a consistent format:
{
"error": {
"code": "LABEL.ADDRESS.UNDELIVERABLE",
"message": "The destination address cannot receive this carrier service.",
"suggestion": "Validate the address with POST /v1/addresses/validate before creating a label.",
"docs_url": "https://docs.packageretriever.com/reference/errors#LABEL.ADDRESS.UNDELIVERABLE",
"request_id": "req_8f3kd92ms"
}
}
Every error includes:
- A dot-notation
code you can catch programmatically (error.code.startsWith('LABEL.'))
- A
suggestion telling you what to do next
- A
docs_url linking directly to the error documentation
- A
request_id for support lookups
Migration checklist
Total migration time: 2-4 hours for a typical integration.
Questions?