Skip to main content

Parcel Transportation Offers

POST /parcel/search-offers returns parcel delivery offers for a requested pickup and drop-off. The endpoint can return a single freelance rider journey, a transportation-company journey, or a stitched journey that combines first-mile, middle-mile, and last-mile segments.

Use this endpoint before creating a customer parcel delivery request. The selected offer is later persisted on the parcel so operations, billing, rider assignment, and customer tracking all know which delivery path was purchased.

Endpoint

POST /parcel/search-offers

The controller is searchParcelTransportationOffers in src/controller/parcel/index.ts. The route is registered in src/controller/parcel/router.ts.

When To Use It

Call this endpoint when a web or mobile client needs to:

  • quote parcel delivery from one address/location to another;
  • check whether lockers, agents, offices, or stations can participate in the journey;
  • find a route that can carry the parcel by size, weight, estimated value, and distance;
  • compare ECONOMY, EXPRESS, or carrier-specific price buckets;
  • receive an offer object that can be selected during parcel checkout.

Authentication

Send the same bearer token used by other Safiri Booking API endpoints.

Authorization: Bearer <JWT_TOKEN>

The endpoint also forwards this token to child parcel-trip APIs when it searches for middle-mile carrier segments.

Headers

HeaderRequiredDefaultPurpose
AuthorizationYesNoneAuthenticates the caller and is forwarded to child APIs.
Content-TypeYes for JSON bodyapplication/jsonRequired when sending a JSON body.
Content-LanguageNoenLocalizes errors and duration labels.
Content-CurrencyNoTZSCurrency used when calculating and returning rider prices. Must exist in CURRENCY.
x-company-uuidNoSafiri company UUIDCompany configuration used for parcel pricing and supported transport rules.
x-website-owner-uuidNoSafiri company UUIDFallback company UUID when x-company-uuid is absent.
x-companyNoEmpty stringCompany display name forwarded to child APIs.
x-website-ownerNoEmpty stringFallback company display name.
x-device-timezoneNoAfrica/Dar_es_SalaamFallback timezone when the origin city/country cannot be mapped to a timezone.

Company UUID precedence is:

  1. x-company-uuid
  2. x-website-owner-uuid
  3. config.SAFIRI_UUID

Company name precedence is:

  1. x-website-owner
  2. x-company
  3. empty string

Request Body

The endpoint accepts a JSON body. If the body is empty, it falls back to req.query; this supports older clients that still send query-string payloads.

Only the fields listed below are accepted. Extra fields are ignored by lodash/pick before any pricing or route logic runs.

FieldTypeRequiredDescription
originAddress or JSON stringYesPickup address/location. String values are parsed as JSON.
destinationAddress or JSON stringYesDrop-off address/location. String values are parsed as JSON.
weightnumber/stringYesParcel weight used for size category and transport capability checks.
heightnumber/stringYesParcel height used for size category and transport capability checks.
widthnumber/stringYesParcel width used for size category and transport capability checks.
lengthnumber/stringYesParcel length used for size category and transport capability checks.
estimatedValuenumber/stringNoParcel value used for insurance and threshold checks. Defaults to 0.
propertiesstring[]NoParcel property tags used by locker/agent availability checks. Defaults to [].
whatIsInTheParcelstring[]NoContent categories forwarded to child parcel-trip APIs. Defaults to [].
distancestringNoJSON-encoded Google Distance Matrix response. If absent or invalid, the backend calculates haversine distance.
insurancestringNoInsurance coverage. Defaults to BASIC.
datestringNoRequested delivery date. Defaults to today in the resolved timezone.
timestringNoRequested local time. Defaults to current time in the resolved timezone.

Address Shape

origin and destination use the parcel Address shape. The endpoint mostly needs coordinates, city, country, location type, and optional locker/office metadata.

{
"uuid": "pickup-location-uuid",
"latitude": -15.4161431,
"longitude": 28.2815028,
"name": "Arcades pickup point",
"description": "Arcades Shopping Mall, Lusaka",
"city": "Lusaka",
"country": "Zambia",
"type": "ADDRESS",
"locker": {
"slotId": "A-01"
}
}

Supported location types are:

TypeMeaning
ADDRESSA customer pickup or delivery address.
LOCKERA Safiri locker and optional slot. Requires availability checks.
AGENTA Safiri agent location. Requires availability and capacity checks.
OFFICEA company office or handoff office.
STATIONA carrier station. Stations are treated as middle-mile handoff points.

Example Request

curl -X POST "https://booking-api.safiri.app/parcel/search-offers" \
-H "Authorization: Bearer $SAFIRI_TOKEN" \
-H "Content-Type: application/json" \
-H "Content-Language: en" \
-H "Content-Currency: ZMW" \
-H "x-company-uuid: <company-uuid>" \
-H "x-device-timezone: Africa/Lusaka" \
-d '{
"origin": {
"latitude": -15.4161431,
"longitude": 28.2815028,
"description": "Arcades Shopping Mall, Lusaka",
"city": "Lusaka",
"country": "Zambia",
"type": "ADDRESS"
},
"destination": {
"latitude": -15.3308,
"longitude": 28.4526,
"description": "Kenneth Kaunda International Airport",
"city": "Lusaka",
"country": "Zambia",
"type": "OFFICE"
},
"weight": 2,
"height": 12,
"width": 18,
"length": 30,
"estimatedValue": 300,
"properties": ["FRAGILE"],
"whatIsInTheParcel": ["DOCUMENTS"],
"insurance": "BASIC",
"date": "2026-05-20",
"time": "10:30"
}'

Response

type TransportationOffersResponse = {
meta: {
count: number;
links: {
self: string;
};
createdAt: string;
expiresAt: string;
paymentTimeInSeconds: number;
};
data: IParcelOffer[];
};

paymentTimeInSeconds is currently 1800 because OFFER_EXPIRATION_TIME is 30 minutes.

Offer Shape

Each item in data is an IParcelOffer.

FieldTypeDescription
typestringAlways parcel-transportation-offer for top-level offers.
uuidstringGenerated offer UUID.
segmentsSegment[]Ordered delivery legs. One offer may contain first-mile, middle-mile, and last-mile segments.
pricePriceKeyAggregated price by service bucket.
distanceDistanceOfferAggregated distance for the offer.
durationDurationKeyAggregated duration by service bucket.
originAddressFirst segment origin.
destinationAddressLast segment destination.
estimatedDepartureTimestringEarliest segment departure time.
estimatedArrivalTimestringLatest segment arrival time.
carrierCarrierMiddle-mile carrier when present, otherwise rider/freelance carrier.
selfstringURL that recreates the search.

Segment Shape

Segments are ordered exactly as the parcel will move through the network.

FieldTypeDescription
typestringfreelance-or-rider-offer or transportation-company-offer.
uuidstringSegment UUID.
distanceDistanceOfferDistance for this leg.
durationDurationKeyDuration for this leg by service bucket.
estimatedDepartureTimestringSegment departure time.
estimatedArrivalTimestringSegment arrival time.
originAddressSegment pickup/handoff location.
destinationAddressSegment drop-off/handoff location.
pricePriceKeySegment price by service bucket.
basePricePriceKeyOptional raw/base price before final presentation.
carrierCarrierRider or transportation company operating this segment.
selfstringSegment-specific search URL.

Price Shape

PriceKey is keyed by service bucket. Freelance-only offers commonly return ECONOMY and EXPRESS. Transportation company offers may also produce composed keys such as ECONOMY-24_HOURS after aggregation.

type PriceDetail = {
currency: string;
total: string;
safiriCommission: string;
fees: Array<{
amount: string;
type: string;
}>;
grandTotal: string;
duration?: DurationOffer;
};

Common fee types include:

FeeMeaning
TRANSPORTMiddle-mile carrier transport cost.
SAFIRI_SERVICESafiri service fee or commission bucket.
FACILITATIONAgent or location facilitation fee.
LOCKERLocker usage fee.
STORAGEStorage fee when location handling requires storage.
GOVERNMENTGovernment or congestion/tax-like fee normalized into the government bucket.
INSURANCEInsurance fee.

Example Response

{
"meta": {
"count": 1,
"links": {
"self": "https://booking-api.safiri.app/parcel/search-offers?origin=%7B...%7D&destination=%7B...%7D&weight=2"
},
"createdAt": "2026-05-19T11:04:11.000+02:00",
"expiresAt": "2026-05-19T11:34:11.000+02:00",
"paymentTimeInSeconds": 1800
},
"data": [
{
"type": "parcel-transportation-offer",
"uuid": "8631df93-a295-4408-918c-c40ede5a0590",
"segments": [
{
"type": "freelance-or-rider-offer",
"uuid": "first-mile",
"distance": {
"text": "10.4 km",
"value": 10.4,
"unit": "kilometer"
},
"duration": {
"ECONOMY": {
"text": "2-4 hours",
"value": 14400,
"unit": "seconds"
},
"EXPRESS": {
"text": "2-3 hours",
"value": 10800,
"unit": "seconds"
}
},
"estimatedDepartureTime": "2026-05-20T10:30:00",
"estimatedArrivalTime": "2026-05-20T13:30:00",
"origin": {
"city": "Lusaka",
"country": "Zambia",
"type": "ADDRESS"
},
"destination": {
"city": "Lusaka",
"country": "Zambia",
"type": "OFFICE"
},
"price": {
"ECONOMY": {
"currency": "ZMW",
"total": "70",
"safiriCommission": "14",
"fees": [],
"grandTotal": "84"
}
},
"carrier": {
"type": "MOTORBIKE",
"category": "FREELANCER_OR_RIDER"
},
"self": "https://booking-api.safiri.app/parcel/search-offers?..."
}
],
"origin": {
"city": "Lusaka",
"country": "Zambia",
"type": "ADDRESS"
},
"destination": {
"city": "Lusaka",
"country": "Zambia",
"type": "OFFICE"
},
"price": {
"ECONOMY": {
"currency": "ZMW",
"total": "70",
"safiriCommission": "14",
"fees": [],
"grandTotal": "84"
}
},
"distance": {
"text": "10.4 km",
"value": 10.4,
"unit": "kilometer"
},
"duration": {
"ECONOMY": {
"text": "2-4 hours",
"value": 14400,
"unit": "seconds"
}
},
"estimatedDepartureTime": "2026-05-20T10:30:00",
"estimatedArrivalTime": "2026-05-20T13:30:00",
"carrier": {
"type": "MOTORBIKE",
"category": "FREELANCER_OR_RIDER"
},
"self": "https://booking-api.safiri.app/parcel/search-offers?..."
}
]
}

Error Responses

Errors follow the controller's standard error envelope.

{
"error": {
"name": "modal"
},
"errorCode": "BAD_REQUEST",
"errorMessage": "currencyNotSupported",
"status": 400
}

Common failures:

StatusCauseNotes
400Unsupported Content-CurrencyThe currency must exist in the CURRENCY enum.
400Company not foundApplies when a non-Safiri company UUID is provided but no active company is found.
400Parcel configuration not foundThe company must have parcel delivery configuration.
400Origin locker or agent unavailablePickup LOCKER or AGENT cannot accept the parcel.
400Destination locker or agent unavailableDestination LOCKER or AGENT cannot accept the parcel in freelance-only fallback.

Middle-mile API failures are logged and do not immediately fail the whole request. The endpoint can still return freelance-only offers when supported by configuration.

Internal Child APIs

The endpoint can call these child APIs to find middle-mile transportation-company segments:

Child APITriggering modePurpose
POST /api/parcel-trip/flightVEHICLE_TYPE.AIRSearches flight-based parcel trips.
POST /api/parcel-trip/intercityVEHICLE_TYPE.UPCOUNTRY_CARGOSearches intercity/upcountry parcel trips.
POST /api/parcel-trip/cargoVEHICLE_TYPE.CARGOSearches cargo parcel trips.

Each child request receives the normalized origin, destination, dimensions, value, insurance, parcel content, travel class, date, time, timezone, localization headers, company headers, and authorization token.