Unofficial TypeScript SDK for Speedy shipping API - Bulgaria's leading courier service.
Independent community implementation. Not officially affiliated with or endorsed by Speedy AD.
- Core API Coverage - Shipments, tracking, printing, address lookup, offices, and rate calculation
- 100% Type Safe - Full TypeScript support with complete type definitions
- Dual Format - Ships both CommonJS and ES Modules
- Promise-based API - Modern async/await syntax
- Custom Error Types - Specific error classes for each failure mode
- Required Filters - Listing endpoints guarded against huge response dumps
- Well Tested - Comprehensive integration tests against the live Speedy API
npm install @alphabite/speedy-sdkyarn add @alphabite/speedy-sdkpnpm add @alphabite/speedy-sdkimport { SpeedyClient } from '@alphabite/speedy-sdk';
// Initialize the client
const speedy = new SpeedyClient({
username: 'your_username',
password: 'your_password',
environment: 'production',
});
// Calculate shipping cost
const calculation = await speedy.calculation.calculate({
recipient: {
privatePerson: true,
addressLocation: { siteId: 68134 }, // Sofia
},
service: {
serviceIds: [505],
},
content: {
parcelsCount: 1,
totalWeight: 0.6,
},
payment: {
courierServicePayer: 'SENDER',
},
});
console.log(`Price: ${calculation.calculations[0].price.amount} BGN`);
// Create a shipment
const shipment = await speedy.shipments.create({
service: {
serviceId: 505,
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: 0.6,
contents: 'Mobile Phone',
package: 'BOX',
},
payment: {
courierServicePayer: 'SENDER',
},
sender: {
phone1: { number: '0888112233' },
contactName: 'Ivan Petrov',
email: 'ivan@example.com',
},
recipient: {
phone1: { number: '0899445566' },
clientName: 'Vasil Georgiev',
email: 'vasil@example.com',
privatePerson: true,
pickupOfficeId: 14,
},
ref1: 'ORDER-12345',
});
console.log(`Shipment ID: ${shipment.id}`);const speedy = new SpeedyClient({
username: 'your_username',
password: 'your_password',
environment: 'production', // 'production' or 'sandbox'
});const speedy = new SpeedyClient({
username: 'your_username',
password: 'your_password',
environment: 'production',
// Optional: Request timeout in milliseconds (default: 30000)
timeout: 30000,
// Optional: Maximum retry attempts for failed requests (default: 3)
maxRetries: 3,
});To prevent timeout errors and huge response payloads, certain listing methods require at least one filter:
// ✅ Correct
const result = await speedy.address.findSites({ countryId: 100 });
const result = await speedy.address.findSites({
countryId: 100,
name: 'SOFIA',
});
const result = await speedy.address.findSites({
countryId: 100,
postCode: '1000',
});
// ❌ Wrong - will throw
const result = await speedy.address.findSites({}); // Error!// ✅ Correct
const result = await speedy.offices.find({ countryId: 100 });
const result = await speedy.offices.find({ siteId: 68134 });
const result = await speedy.offices.find({ name: 'SOFIA' });
// ❌ Wrong - will throw
const result = await speedy.offices.find({}); // Error!The SDK provides access to the following services:
speedy.address- Address and location servicesspeedy.offices- Office lookupspeedy.calculation- Shipping cost calculationsspeedy.shipments- Shipment creation and managementspeedy.tracking- Shipment trackingspeedy.print- Label printing
// Find cities by name
const result = await speedy.address.findSites({
countryId: 100, // Bulgaria
name: 'SOFIA',
});
console.log(result.sites); // or result.cities (backwards compatible)
// Find by postcode
const result = await speedy.address.findSites({
countryId: 100,
postCode: '1000',
});// Find all offices in Bulgaria
const result = await speedy.offices.find({
countryId: 100,
});
// Find offices in a specific city
const result = await speedy.offices.find({
siteId: 68134, // Sofia
});
// Find office by partial name
const result = await speedy.offices.find({
name: 'SOFIA',
});const result = await speedy.address.findStreets(
68134, // Sofia siteId
'VASIL LEVSKI', // Street name (partial match)
);
console.log(result.streets);const result = await speedy.address.findComplexes(
68134, // Sofia siteId
'KRASNA', // Complex name (partial match)
);
console.log(result.complexes); // or result.quarters (backwards compatible)const calculation = await speedy.calculation.calculate({
recipient: {
privatePerson: true,
addressLocation: {
siteId: 68134,
},
},
service: {
serviceIds: [505, 421], // Can request multiple services
},
content: {
parcelsCount: 1,
totalWeight: 0.6,
},
payment: {
courierServicePayer: 'SENDER', // 'SENDER' | 'RECIPIENT' | 'THIRD_PARTY'
},
});
// Access all calculated prices
calculation.calculations.forEach((calc) => {
console.log(`Service ${calc.serviceId}:`);
console.log(` Price: ${calc.price.amount} ${calc.price.currency}`);
console.log(` Total: ${calc.price.total}`);
console.log(` Delivery: ${calc.deliveryDeadline}`);
});const calculation = await speedy.calculation.calculate({
recipient: {
privatePerson: true,
pickupOfficeId: 77,
},
service: {
serviceIds: [505],
},
content: {
parcelsCount: 1,
totalWeight: 0.8,
},
payment: {
courierServicePayer: 'RECIPIENT',
},
});const calculation = await speedy.calculation.calculate({
recipient: {
privatePerson: true,
addressLocation: { siteId: 68134 },
},
service: {
serviceIds: [306], // International service
},
content: {
parcelsCount: 2,
totalWeight: 15.5,
parcels: [
{
weight: 8,
size: { width: 30, height: 20, depth: 10 },
},
{
weight: 7.5,
size: { width: 25, height: 15, depth: 10 },
},
],
},
payment: {
courierServicePayer: 'SENDER',
},
});const shipment = await speedy.shipments.create({
service: {
serviceId: 505,
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: 0.6,
contents: 'Mobile Phone',
package: 'BOX',
},
payment: {
courierServicePayer: 'RECIPIENT',
},
sender: {
phone1: { number: '0888112233' },
contactName: 'Ivan Petrov',
email: 'ivan@example.com',
},
recipient: {
phone1: { number: '0899445566' },
clientName: 'Vasil Georgiev',
email: 'vasil@example.com',
privatePerson: true,
address: {
countryId: 100,
siteId: 68134,
streetId: 3109,
streetNo: '1A',
complexId: 29,
blockNo: '301',
entranceNo: '2',
floorNo: '3',
apartmentNo: '4',
},
},
ref1: 'ORDER-12345',
});const shipment = await speedy.shipments.create({
service: {
serviceId: 505,
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: 0.6,
contents: 'Documents',
package: 'ENVELOPE',
},
payment: {
courierServicePayer: 'SENDER',
},
sender: {
phone1: { number: '0888112233' },
contactName: 'Ivan Petrov',
email: 'ivan@example.com',
},
recipient: {
phone1: { number: '0899445566' },
clientName: 'Vasil Georgiev',
email: 'vasil@example.com',
privatePerson: true,
address: {
countryId: 100,
siteType: 'gr.',
siteName: 'SOFIA',
streetType: 'ul.',
streetName: 'VASIL LEVSKI',
streetNo: '10',
},
},
ref1: 'ORDER-67890',
});const shipment = await speedy.shipments.create({
service: {
serviceId: 505,
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: 0.5,
contents: 'Accessories',
package: 'BOX',
},
payment: {
courierServicePayer: 'RECIPIENT',
},
sender: {
phone1: { number: '0888112233' },
contactName: 'Ivan Petrov',
email: 'ivan@example.com',
},
recipient: {
phone1: { number: '0899445566' },
clientName: 'Vasil Georgiev',
email: 'vasil@example.com',
privatePerson: true,
pickupOfficeId: 14, // Recipient picks up from office
},
ref1: 'ORDER-11111',
});const shipment = await speedy.shipments.create({
service: {
serviceId: 505,
additionalServices: {
cod: {
amount: 100.0,
processingType: 'CASH', // or "BANK_TRANSFER"
},
},
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: 1.0,
contents: 'Electronics',
package: 'BOX',
},
payment: {
courierServicePayer: 'RECIPIENT',
},
sender: {
phone1: { number: '0888112233' },
contactName: 'Ivan Petrov',
email: 'ivan@example.com',
},
recipient: {
phone1: { number: '0899445566' },
clientName: 'Vasil Georgiev',
email: 'vasil@example.com',
privatePerson: true,
pickupOfficeId: 14,
},
ref1: 'COD-ORDER-123',
});const shipment = await speedy.shipments.create({
service: {
serviceId: 505,
additionalServices: {
declaredValue: {
amount: 500.0,
fragile: true,
ignoreIfNotApplicable: true,
},
},
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: 0.8,
contents: 'Valuable Items',
package: 'BOX',
},
payment: {
courierServicePayer: 'SENDER',
declaredValuePayer: 'SENDER',
},
sender: {
phone1: { number: '0888112233' },
contactName: 'Ivan Petrov',
email: 'ivan@example.com',
},
recipient: {
phone1: { number: '0899445566' },
clientName: 'Vasil Georgiev',
email: 'vasil@example.com',
privatePerson: true,
address: {
countryId: 100,
siteId: 68134,
streetId: 3109,
streetNo: '1A',
},
},
ref1: 'VALUABLE-ORDER-456',
});const shipment = await speedy.shipments.create({
service: {
serviceId: 505,
additionalServices: {
obpd: {
option: 'OPEN', // 'OPEN' | 'TEST' | 'RETURN'
returnShipmentServiceId: 505,
returnShipmentPayer: 'SENDER',
},
},
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: 0.7,
contents: 'Clothing',
package: 'BOX',
},
payment: {
courierServicePayer: 'RECIPIENT',
},
sender: {
phone1: { number: '0888112233' },
contactName: 'Ivan Petrov',
email: 'ivan@example.com',
},
recipient: {
phone1: { number: '0899445566' },
clientName: 'Vasil Georgiev',
email: 'vasil@example.com',
privatePerson: true,
pickupOfficeId: 14,
},
ref1: 'OBPD-ORDER-789',
});// Shipment to Greece
const shipment = await speedy.shipments.create({
service: {
serviceId: 202, // International service for Greece
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: 1.0,
contents: 'Books',
package: 'BOX',
},
payment: {
courierServicePayer: 'SENDER',
},
sender: {
phone1: { number: '0888112233' },
contactName: 'Ivan Petrov',
email: 'ivan@example.com',
},
recipient: {
phone1: { number: '0899445566' },
clientName: 'Vasil Georgiev',
email: 'vasil@example.com',
privatePerson: true,
address: {
countryId: 300, // Greece
siteName: 'THESSALONIKI',
postCode: '54629',
addressLine1: '28 Monastiriou str',
addressLine2: 'Additional info',
},
},
ref1: 'INT-ORDER-999',
});// For some international services, parcel dimensions are required
const shipment = await speedy.shipments.create({
service: {
serviceId: 306, // Service to Germany/France
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: 8.0,
contents: 'Electronics',
package: 'BOX',
parcels: [
{
seqNo: 1,
weight: 8,
size: {
width: 30,
depth: 20,
height: 35,
},
},
],
},
payment: {
courierServicePayer: 'SENDER',
},
sender: {
phone1: { number: '0888112233' },
contactName: 'Ivan Petrov',
email: 'ivan@example.com',
},
recipient: {
phone1: { number: '0899445566' },
clientName: 'Hans Mueller',
email: 'hans@example.com',
privatePerson: true,
address: {
countryId: 276, // Germany
siteName: 'MUNICH',
postCode: '80001',
addressLine1: 'Hauptstrasse 123',
addressLine2: 'Apartment 4B',
},
},
ref1: 'DE-ORDER-777',
});const tracking = await speedy.tracking.track([{ id: '299999990' }]);
if (tracking.parcels) {
tracking.parcels.forEach((parcel) => {
console.log(`Parcel ${parcel.parcelId}:`);
parcel.operations?.forEach((op) => {
console.log(` ${op.date}: ${op.operationDescription}`);
});
});
}const tracking = await speedy.tracking.track([
{ id: '299999990' },
{ id: '299999991' },
{ id: '299999992' },
]);const tracking = await speedy.tracking.track([{ ref: 'ORDER-12345' }]);const bulkData = await speedy.tracking.getBulkTrackingFiles(0);
// Returns tracking data files for bulk processingconst result = await speedy.print.print({
parcels: [{ id: '299999990' }],
paperSize: 'A6',
format: 'pdf',
});
if (result.data) {
// result.data is base64 encoded PDF
const pdfBuffer = Buffer.from(result.data, 'base64');
// Save or display the PDF
}const result = await speedy.print.print({
parcels: [{ id: '299999990' }, { id: '299999991' }, { id: '299999992' }],
paperSize: 'A6',
format: 'pdf',
});// A4 format
const a4Label = await speedy.print.print({
parcels: [{ id: '299999990' }],
paperSize: 'A4',
format: 'pdf',
});
// ZPL format (for thermal printers)
const zplLabel = await speedy.print.print({
parcels: [{ id: '299999990' }],
paperSize: 'A6',
format: 'zpl',
});
// A4 with 4 A6 labels
const multiLabel = await speedy.print.print({
parcels: [
{ id: '299999990' },
{ id: '299999991' },
{ id: '299999992' },
{ id: '299999993' },
],
paperSize: 'A4_4xA6',
format: 'pdf',
});const result = await speedy.print.printExtended({
parcels: [{ id: '299999990' }],
paperSize: 'A6',
format: 'pdf',
});const info = await speedy.print.getLabelInfo([{ id: '299999990' }]);const info = await speedy.shipments.getInfo(['shipment-id-1', 'shipment-id-2']);
console.log(info.shipments);try {
const result = await speedy.shipments.cancel(
'shipment-id',
'Customer requested cancellation',
);
console.log('Shipment cancelled successfully');
} catch (error) {
console.error('Failed to cancel:', error);
}import { SpeedyClient } from '@alphabite/speedy-sdk';
async function processOrder(orderData: any) {
const speedy = new SpeedyClient({
username: process.env.SPEEDY_USERNAME!,
password: process.env.SPEEDY_PASSWORD!,
environment: 'production',
});
try {
// Step 1: Find recipient's city
const cities = await speedy.address.findSites({
countryId: 100,
name: orderData.city,
});
const city = cities.sites[0];
console.log(`Found city: ${city.name} (ID: ${city.id})`);
// Step 2: Find nearest office
const offices = await speedy.offices.find({
siteId: city.id,
});
const nearestOffice = offices.offices[0];
console.log(`Nearest office: ${nearestOffice.name}`);
// Step 3: Calculate shipping cost
const calculation = await speedy.calculation.calculate({
recipient: {
privatePerson: true,
pickupOfficeId: nearestOffice.id,
},
service: {
serviceIds: [505],
},
content: {
parcelsCount: 1,
totalWeight: orderData.weight,
},
payment: {
courierServicePayer: 'SENDER',
},
});
const price = calculation.calculations[0].price.amount;
console.log(`Shipping cost: ${price} BGN`);
// Step 4: Create shipment
const shipment = await speedy.shipments.create({
service: {
serviceId: 505,
additionalServices: {
cod: {
amount: orderData.totalAmount,
processingType: 'CASH',
},
},
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: orderData.weight,
contents: orderData.contents,
package: 'BOX',
},
payment: {
courierServicePayer: 'SENDER',
},
sender: {
phone1: { number: process.env.SENDER_PHONE! },
contactName: process.env.SENDER_NAME!,
email: process.env.SENDER_EMAIL!,
},
recipient: {
phone1: { number: orderData.phone },
clientName: orderData.customerName,
email: orderData.email,
privatePerson: true,
pickupOfficeId: nearestOffice.id,
},
ref1: `ORDER-${orderData.orderId}`,
});
console.log(`✅ Shipment created: ${shipment.id}`);
// Step 5: Print label
if (shipment.parcels && shipment.parcels.length > 0) {
const label = await speedy.print.print({
parcels: [{ id: shipment.parcels[0].id }],
paperSize: 'A6',
format: 'pdf',
});
if (label.data) {
// Save label to file or send to printer
const fs = require('fs');
const pdfBuffer = Buffer.from(label.data, 'base64');
fs.writeFileSync(`label-${shipment.id}.pdf`, pdfBuffer);
console.log(`✅ Label saved: label-${shipment.id}.pdf`);
}
}
// Step 6: Track shipment
const tracking = await speedy.tracking.track([
{ id: shipment.parcels![0].id },
]);
console.log('✅ Tracking info:', tracking.parcels);
return {
shipmentId: shipment.id,
parcelId: shipment.parcels![0].id,
cost: price,
};
} catch (error) {
console.error('❌ Error processing order:', error);
throw error;
}
}
// Usage
processOrder({
orderId: '12345',
city: 'SOFIA',
customerName: 'Vasil Georgiev',
phone: '0899445566',
email: 'vasil@example.com',
weight: 0.8,
contents: 'Mobile Phone',
totalAmount: 299.99,
});The Speedy API supports multiple ways to specify addresses:
address: {
countryId: 100,
siteId: 68134,
streetId: 3109,
streetNo: "1A",
complexId: 29,
blockNo: "301",
entranceNo: "2",
floorNo: "3",
apartmentNo: "4",
}address: {
countryId: 100,
siteType: "gr.",
siteName: "SOFIA",
streetType: "ul.",
streetName: "VASIL LEVSKI",
streetNo: "10",
}address: {
countryId: 100,
siteName: "SOFIA",
postCode: "1000",
streetType: "ul.",
streetName: "VASIL LEVSKI",
streetNo: "10",
}address: {
countryId: 100,
siteId: 68134,
addressNote: "ul. VASIL LEVSKI, No.10, bl.301, ent.2, fl.3, ap.4",
}Note: Using IDs is most reliable. Using addressNote may cause delays or processing issues.
The SDK provides detailed error types for different scenarios:
import {
SpeedyAPIError,
SpeedyAuthenticationError,
SpeedyNetworkError,
SpeedyRateLimitError
} from "@alphabite/speedy-sdk";
try {
const shipment = await speedy.shipments.create({...});
} catch (error) {
if (error instanceof SpeedyAuthenticationError) {
console.error("Authentication failed - check credentials");
} else if (error instanceof SpeedyRateLimitError) {
console.error("Rate limit exceeded - retry after:", error.retryAfter);
} else if (error instanceof SpeedyNetworkError) {
console.error("Network error:", error.message);
} else if (error instanceof SpeedyAPIError) {
console.error("API error:", error.message);
console.error("Status:", error.statusCode);
console.error("Response:", error.response);
console.error("Request ID:", error.requestId);
} else {
console.error("Unknown error:", error);
}
}Common Speedy service IDs:
- 505 - Standard domestic service (Bulgaria)
- 421 - Express domestic service
- 202 - Standard service to Greece
- 306 - Standard service to Germany/France
- 202 - Service to other Balkan countries
Note: Use the calculation API to discover available services for specific routes.
// When customer completes checkout
async function createShippingLabel(order) {
const speedy = new SpeedyClient({
username: process.env.SPEEDY_USERNAME!,
password: process.env.SPEEDY_PASSWORD!,
environment: 'production',
});
// Create shipment with COD
const shipment = await speedy.shipments.create({
service: {
serviceId: 505,
additionalServices: {
cod: {
amount: order.total,
processingType: 'CASH',
},
},
autoAdjustPickupDate: true,
},
content: {
parcelsCount: 1,
totalWeight: order.weight,
contents: order.description,
package: 'BOX',
},
payment: {
courierServicePayer: 'RECIPIENT',
},
sender: {
phone1: { number: process.env.SHOP_PHONE! },
contactName: process.env.SHOP_CONTACT!,
email: process.env.SHOP_EMAIL!,
},
recipient: {
phone1: { number: order.customerPhone },
clientName: order.customerName,
email: order.customerEmail,
privatePerson: true,
address: {
countryId: 100,
siteId: order.cityId,
streetName: order.street,
streetNo: order.streetNo,
addressNote: order.additionalInfo,
},
},
ref1: `ORDER-${order.id}`,
});
// Print label
const label = await speedy.print.print({
parcels: [{ id: shipment.parcels![0].id }],
paperSize: 'A6',
format: 'pdf',
});
return {
shipmentId: shipment.id,
parcelId: shipment.parcels![0].id,
labelPdf: label.data,
};
}async function findNearestOffice(cityName: string) {
const speedy = new SpeedyClient({...});
// Find the city
const cities = await speedy.address.findSites({
countryId: 100,
name: cityName,
});
if (cities.sites.length === 0) {
throw new Error(`City not found: ${cityName}`);
}
const city = cities.sites[0];
// Find offices in the city
const result = await speedy.offices.find({
siteId: city.id,
});
return result.offices.map(office => ({
id: office.id,
name: office.name,
address: office.address,
workingHours: office.workingTimeSchedule,
phones: office.phones,
}));
}
// Usage
const offices = await findNearestOffice("SOFIA");
console.log(`Found ${offices.length} offices`);async function getShipmentStatus(parcelIds: string[]) {
const speedy = new SpeedyClient({...});
const tracking = await speedy.tracking.track(
parcelIds.map(id => ({ id }))
);
return tracking.parcels?.map(parcel => ({
parcelId: parcel.parcelId,
currentStatus: parcel.operations?.[0]?.operationDescription,
lastUpdate: parcel.operations?.[0]?.date,
history: parcel.operations,
}));
}
// Usage
const statuses = await getShipmentStatus([
"299999990",
"299999991",
]);
statuses?.forEach(status => {
console.log(`${status.parcelId}: ${status.currentStatus}`);
});For production applications, store credentials in environment variables:
# .env file
SPEEDY_USERNAME=your_username
SPEEDY_PASSWORD=your_passwordimport { SpeedyClient } from '@alphabite/speedy-sdk';
import dotenv from 'dotenv';
dotenv.config();
const speedy = new SpeedyClient({
username: process.env.SPEEDY_USERNAME!,
password: process.env.SPEEDY_PASSWORD!,
environment: 'production',
});The SDK is written in TypeScript and provides full type definitions:
import {
SpeedyClient,
CreateShipmentRequest,
CreateShipmentResponse,
CalculationRequest,
CalculationResponse,
TrackShipmentResponse,
PrintRequest,
PrintResponse,
// ... and many more
} from "@alphabite/speedy-sdk";
// All API calls are fully typed
const shipment: CreateShipmentResponse = await speedy.shipments.create({
// TypeScript will auto-complete and validate all fields
...
});The Speedy API has rate limits. The SDK automatically handles rate limit errors and provides retry information.
try {
await speedy.shipments.create({...});
} catch (error) {
if (error instanceof SpeedyRateLimitError) {
console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
// Wait and retry
}
}The SDK doesn't include built-in caching to keep it lightweight and flexible. Here are recommended caching patterns you can implement for frequently accessed nomenclature data (offices, cities, etc.):
import Redis from 'ioredis';
const redis = new Redis();
const speedy = new SpeedyClient({
username: process.env.SPEEDY_USERNAME!,
password: process.env.SPEEDY_PASSWORD!,
});
async function getCitiesWithCache(countryId: number) {
const key = `speedy:cities:${countryId}`;
// Check cache
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
// Fetch from API
const cities = await speedy.address.findSites({ countryId });
// Store in cache (24 hour TTL)
await redis.set(key, JSON.stringify(cities), 'EX', 86400);
return cities;
}import * as fs from 'fs';
import * as path from 'path';
const CACHE_DIR = './cache';
const TTL = 24 * 60 * 60 * 1000; // 24 hours
async function getCitiesWithFileCache(countryId: number) {
const file = path.join(CACHE_DIR, `cities-${countryId}.json`);
// Check if cache exists and is fresh
if (fs.existsSync(file)) {
const stats = fs.statSync(file);
const age = Date.now() - stats.mtime.getTime();
if (age < TTL) {
return JSON.parse(fs.readFileSync(file, 'utf-8'));
}
}
// Fetch from API
const cities = await speedy.address.findSites({ countryId });
// Save to cache
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
}
fs.writeFileSync(file, JSON.stringify(cities, null, 2));
return cities;
}const cache = new Map<string, { data: any; timestamp: number }>();
const TTL = 24 * 60 * 60 * 1000;
async function getCitiesWithMemoryCache(countryId: number) {
const key = `cities:${countryId}`;
const cached = cache.get(key);
// Check if cache is fresh
if (cached && Date.now() - cached.timestamp < TTL) {
return cached.data;
}
// Fetch from API
const cities = await speedy.address.findSites({ countryId });
// Store in cache
cache.set(key, { data: cities, timestamp: Date.now() });
return cities;
}// Using your ORM (Prisma, TypeORM, etc.)
async function getCitiesWithDbCache(countryId: number) {
// Check database
let cacheEntry = await db.cache.findUnique({
where: { key: `cities:${countryId}` },
});
if (cacheEntry && Date.now() - cacheEntry.timestamp < TTL) {
return JSON.parse(cacheEntry.data);
}
// Fetch from API
const cities = await speedy.address.findSites({ countryId });
// Store in database
await db.cache.upsert({
where: { key: `cities:${countryId}` },
create: {
key: `cities:${countryId}`,
data: JSON.stringify(cities),
timestamp: Date.now(),
},
update: {
data: JSON.stringify(cities),
timestamp: Date.now(),
},
});
return cities;
}For large bulk operations, you may need to increase the timeout:
const speedy = new SpeedyClient({
username: '...',
password: '...',
timeout: 60000, // 60 seconds for bulk operations
});The SDK includes comprehensive integration tests:
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run only integration tests
npm run test:integration
# Run tests with UI
npm run test:ui- Node.js >= 14
- TypeScript >= 5.0 (for TypeScript projects)
For complete API documentation, visit:
- 100 - Bulgaria (България)
- 276 - Germany (Германия)
- 300 - Greece (Гърция)
- 250 - France (Франция)
- 380 - Italy (Италия)
- 724 - Spain (Испания)
- 642 - Romania (Румъния)
- 826 - United Kingdom (Великобритания)
The following package types are supported:
BOX- Standard boxENVELOPE- EnvelopePALLET- PalletPARCEL- Generic parcel
For issues, questions, or contributions:
- GitHub Issues: https://github.com/alphabite-dev/speedy-sdk/issues
- Speedy Support: https://www.speedy.bg/bg/kontakti
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
🆕 New Features
- Added
servicesresource:speedy.services.getServices()to fetch available services for a profile/client. - Added types for
/servicesendpoint (GetServicesRequest,GetServicesResponse).
- Removed built-in caching. The
cacheconfig option,exportAllData(),getCacheStatus(), andclearCache()have been removed. See Caching Strategies for recommended user-implemented patterns. - Required filters on listing endpoints.
offices.find()now requires at least one ofcountryId,cityId,siteId, orname.address.findSites()now requires at least one ofcountryId,name, orpostCode. Calls without filters previously returned massive payloads.
🆕 Other Changes
- Split into a monorepo:
@alphabite/speedy-sdk(the SDK) and@alphabite/speedy-types(standalone types). - Switched build from
tsctotsup; now publishes both CommonJS (.js) and ES Modules (.mjs) with type definitions.
- 🎉 Initial release
- ✅ Complete Speedy API v1 support
- ✅ Full TypeScript support
- ✅ Comprehensive integration tests
- ✅ Support for all shipment types (domestic, international)
- ✅ Complete address lookup services
- ✅ Calculation services
- ✅ Tracking services
- ✅ Print services (PDF, ZPL)
- ✅ Comprehensive error handling
Developed by Alphabite
Not officially affiliated with Speedy AD. This is an independent SDK implementation.
This SDK is not officially endorsed by Speedy AD. It is an independent implementation of the Speedy API. Please ensure you have proper authorization and credentials from Speedy before using this SDK in production.