Official TypeScript SDK for Speedy shipping API - Bulgaria's leading courier service.
✅ 100% Test Coverage - All 69 integration tests passing
✅ Full TypeScript Support - Complete type definitions
✅ Promise-based API - Modern async/await syntax
✅ Comprehensive Error Handling - Detailed error types
✅ Production Ready - Fully validated with real 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: "[email protected]",
},
recipient: {
phone1: { number: "0899445566" },
clientName: "Vasil Georgiev",
email: "[email protected]",
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,
// Optional: Enable caching for nomenclature data
cache: {
enabled: true,
directory: ".cache/speedy",
ttl: 3600, // Time to live in seconds
},
});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: "[email protected]",
},
recipient: {
phone1: { number: "0899445566" },
clientName: "Vasil Georgiev",
email: "[email protected]",
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: "[email protected]",
},
recipient: {
phone1: { number: "0899445566" },
clientName: "Vasil Georgiev",
email: "[email protected]",
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: "[email protected]",
},
recipient: {
phone1: { number: "0899445566" },
clientName: "Vasil Georgiev",
email: "[email protected]",
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: "[email protected]",
},
recipient: {
phone1: { number: "0899445566" },
clientName: "Vasil Georgiev",
email: "[email protected]",
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: "[email protected]",
},
recipient: {
phone1: { number: "0899445566" },
clientName: "Vasil Georgiev",
email: "[email protected]",
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: "[email protected]",
},
recipient: {
phone1: { number: "0899445566" },
clientName: "Vasil Georgiev",
email: "[email protected]",
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: "[email protected]",
},
recipient: {
phone1: { number: "0899445566" },
clientName: "Vasil Georgiev",
email: "[email protected]",
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: "[email protected]",
},
recipient: {
phone1: { number: "0899445566" },
clientName: "Hans Mueller",
email: "[email protected]",
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: "[email protected]",
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("Details:", error.details);
} 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_password
SPEEDY_BASE_URL=https://api.speedy.bg/v1import { 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
}
}For frequently accessed nomenclature data (offices, cities, etc.), enable caching:
const speedy = new SpeedyClient({
username: "...",
password: "...",
cache: {
enabled: true,
directory: ".cache/speedy",
ttl: 86400, // Cache for 24 hours
},
});
// Export all nomenclature data to cache
await speedy.exportAllData();
// Check cache status
const status = speedy.getCacheStatus();
console.log(status);
// Clear cache when needed
speedy.clearCache();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 >= 16
- 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-org/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.
- 🎉 Initial release
- ✅ Complete Speedy API v1 support
- ✅ Full TypeScript support
- ✅ 100% test coverage (69/69 tests passing)
- ✅ 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.