Train Booking
The Train Booking API lets you discover train stations, search available SGR train trips, retrieve cabin, fare, and seat information, hold selected seats, and create SGR train reservations.
Overview
The Train Booking API currently provides:
- SGR station discovery for origin and destination selection
- Train search between two stations on a travel date
- Trip details including route, departure and arrival times, price, stop times, railway cars, and operator details
- Cabin, passenger-type pricing, seat map, and booking requirement details for a selected train
- Seat hold and release for seat-selection flows
- Reservation creation for one-way and return SGR journeys, with a 15-minute Safiri payment window
Authentication
Train search and train details are protected Safiri Booking API endpoints and require JWT authentication. Obtain a JWT token by logging in through the /api/logon endpoint.
For detailed information on authenticating and obtaining a token, see the Authentication guide.
The train station list is served by the train GTFS API and can be used to populate station pickers before calling the protected booking API.
Headers
Required for train search, train details, seat hold/release, and reservation:
Authorization: Bearer <JWT_TOKEN>- Your JWT authentication token
Optional:
Content-Language: <language_code>- Language for responses (default:en)Content-Currency: <currency_code>- Preferred display currency where supported. Train responses return the baseprice.currencyand formatted values inprice.otherCurrencies.
Wallet Charges for Third-Party Agents
Train booking integrations are intended for third-party agents using a pre-funded Safiri wallet. The API returns Safiri's payable amount separately from whatever retail amount you decide to charge your own customer.
Safiri deducts the returned train payable amount from your wallet:
| Component | Where to find it | Description |
|---|---|---|
| Transporter fee | price.fees[] where type is TRANSPORTER; also price.total | The TRC train fare payable to the transporter. |
| Safiri fee | price.fees[] where type is SAFIRI_COMMISSION | Safiri's fee for the booking. |
| Wallet debit | price.grandTotal | The amount Safiri charges to your wallet for that train ticket. This is the transporter fee plus Safiri-controlled fees returned in price.fees. |
You can charge your customer any additional agency, service, or handling fee on top of price.grandTotal. That extra customer-facing markup is managed by your own system; do not send it in the train booking request, and Safiri does not deduct it from your wallet.
For example, if the API returns price.total of 22500, SAFIRI_COMMISSION of 2000, and price.grandTotal of 24500, Safiri will deduct 24500 from your wallet. If you charge your customer 26000, the extra 1500 is your own markup outside the Safiri wallet debit.
Endpoints
Get Train Stations
Retrieve SGR stations and their Safiri stop IDs. Use the returned stop_id values as the from and to query parameters when searching for trains.
Endpoint: GET https://train-gtfs-api.safiri.app/api/stops?mini=true
Example Request:
GET https://train-gtfs-api.safiri.app/api/stops?mini=true
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
mini | boolean | No | When true, returns the compact station payload used by search and autocomplete flows. |
Response Fields
| Field | Type | Description |
|---|---|---|
stop_id | string | Safiri stop ID. Use this exact value in train search, for example DARESALAAM:SGR:0. |
stop_lat | number | Station latitude. |
stop_lon | number | Station longitude. |
stop_name | string | Human-readable station name. |
stop_desc | string | Station location or address description. |
stop_timezone | string | IANA timezone for the station. |
city_geohash | string | Geohash used for station clustering and spatial search. |
popularity | number | Ranking value used by search and selection UIs. |
partners | array | Provider metadata. For SGR train search, use stops with a TRC partner whose service is SGR. |
Example response
[
{
"stop_id": "DARESALAAM:SGR:0",
"stop_lat": -6.82488714,
"stop_lon": 39.2830351,
"stop_name": "Dar es Salaam (John Magufuli SGR Station)",
"stop_desc": "Kisutu, Ilala, Dar es Salaam, Tanzania",
"stop_timezone": "Africa/Dar_es_Salaam",
"city_geohash": "kygcjv",
"popularity": 30,
"partners": [
{
"provider": "TRC",
"service": "SGR",
"id": 1,
"name": "Dar Es Salaam"
}
]
},
{
"stop_id": "DODOMA:SGR:0",
"stop_lat": -6.21138079,
"stop_lon": 35.73413998,
"stop_name": "Dodoma (Samia SGR Station)",
"stop_desc": "Dodoma, Tanzania",
"stop_timezone": "Africa/Dar_es_Salaam",
"city_geohash": "kycktj",
"popularity": 30,
"partners": [
{
"provider": "TRC",
"service": "SGR",
"id": 12,
"name": "Dodoma"
}
]
}
]
Search for Trains
Search for available SGR train trips between two train stations on a travel date.
Endpoint: GET /api/trip/train/search
Example Request:
GET /api/trip/train/search?from=DARESALAAM:SGR:0&to=MOROGORO:SGR:0&date=2026-06-10
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Origin station stop_id from the train stops endpoint. Example: DARESALAAM:SGR:0. |
to | string | Yes | Destination station stop_id from the train stops endpoint. Example: MOROGORO:SGR:0. |
date | string | Yes | Travel date in YYYY-MM-DD format. |
Example response
{
"meta": {
"count": 6,
"provider": "TRC_SGR",
"links": {
"self": "https://booking-api.safiri.app/api/trip/train/search?from=DARESALAAM%3ASGR%3A0&to=MOROGORO%3ASGR%3A0&date=2026-06-10"
},
"createdAt": "2026-06-10T01:29:53.925Z"
},
"data": [
{
"provider": "TRC_SGR",
"trip": {
"id": 98,
"name": "DAR - DOM \\ EXPRESS",
"number": "11902",
"trainSetId": 20,
"timeTableId": 299,
"timeTableTrainSetId": 8537,
"gtfsTripId": "SASGR002:O",
"headsign": "Dodoma via Pugu, Soga, Ruvu, Ngerengere, Morogoro, Mkata, Kilosa, Kidete, Gulwe, Igandu",
"wheelchairAccessible": 1,
"bikesAllowed": null
},
"route": {
"id": 68,
"gtfsRouteId": "SASGR002:SER002",
"name": "DAR - DOM"
},
"departure": {
"date": "2026-06-09",
"time": "06:00:00",
"dateTime": "2026-06-09T06:00:00+03:00",
"stop": {
"stop_id": "DARESALAAM:SGR:0",
"stop_lat": -6.82488714,
"stop_lon": 39.2830351,
"stop_name": "Dar es Salaam (John Magufuli SGR Station)",
"stop_desc": "Kisutu, Ilala, Dar es Salaam, Tanzania",
"stop_timezone": "Africa/Dar_es_Salaam",
"city_geohash": "kygcjv",
"popularity": 30,
"partners": [
{
"provider": "TRC",
"service": "SGR",
"id": 1,
"name": "Dar Es Salaam"
}
]
}
},
"arrival": {
"date": "2026-06-09",
"time": "07:40:00",
"dateTime": "2026-06-09T07:40:00+03:00",
"stop": {
"stop_id": "MOROGORO:SGR:0",
"stop_lat": -6.75460532,
"stop_lon": 37.66439974,
"stop_name": "Morogoro (Jakaya Kikwete SGR Station)",
"stop_desc": "Morogoro, Tanzania",
"stop_timezone": "Africa/Dar_es_Salaam",
"city_geohash": "kyfc94",
"popularity": 30,
"partners": [
{
"provider": "TRC",
"service": "SGR",
"id": 6,
"name": "Morogoro"
}
]
}
},
"price": {
"pricingContext": "ONLINE",
"currency": "TZS",
"total": "22500",
"bookingFeeTotal": "2000",
"grandTotal": "24500",
"fees": [
{
"type": "TRANSPORTER",
"amount": "22500",
"recipient": "TRC"
},
{
"type": "FACILITATION",
"amount": "0",
"recipient": "SAFIRI_AGENT"
},
{
"type": "SAFIRI_COMMISSION",
"amount": "2000",
"recipient": "SAFIRI"
}
],
"otherCurrencies": {
"TZS": {
"amount": 22500,
"formatted": "TZS 22,500.00"
},
"USD": {
"amount": 8.57,
"formatted": "USD 8.57"
},
"KES": {
"amount": 1107.74,
"formatted": "KES 1,107.74"
},
"UGX": {
"amount": 32274.83,
"formatted": "UGX 32,274.83"
},
"ZMW": {
"amount": 150.59,
"formatted": "ZMW 150.59"
}
}
},
"stopTimes": [
{
"stopId": "DARESALAAM:SGR:0",
"stopSequence": 0,
"arrivalTime": "05:55:00",
"departureTime": "06:00:00",
"stopName": "Dar es Salaam (John Magufuli SGR Station)",
"stopDescription": "Kisutu, Ilala, Dar es Salaam, Tanzania",
"latitude": -6.82488714,
"longitude": 39.2830351
},
{
"stopId": "MOROGORO:SGR:0",
"stopSequence": 5,
"arrivalTime": "08:35:33",
"departureTime": "08:40:33",
"stopName": "Morogoro (Jakaya Kikwete SGR Station)",
"stopDescription": "Morogoro, Tanzania",
"latitude": -6.75460532,
"longitude": 37.66439974
}
],
"cars": [
{
"id": 417,
"carId": 19,
"orderNo": 1,
"typeId": 151,
"typeName": "Royal Class",
"capacity": 44,
"standardSeatCapacity": 44,
"disabledSeatCapacity": 0,
"emptySeats": 0,
"emptyStandardSeats": 0,
"emptyDisabledSeats": 0,
"hasSeat": true,
"bookedSeats": []
},
{
"id": 420,
"carId": 15,
"orderNo": 4,
"typeId": 12,
"typeName": "Economy",
"capacity": 76,
"standardSeatCapacity": 74,
"disabledSeatCapacity": 2,
"emptySeats": 0,
"emptyStandardSeats": 0,
"emptyDisabledSeats": 0,
"hasSeat": true,
"bookedSeats": []
}
],
"transitAgency": {
"name": "Tanzania Railways Corporation",
"tradingName": "Tanzania Railways Corporation",
"tinNumber": "000000001",
"uuid": "204ea6a5-e374-4a22-a8b0-7a1d406c0514",
"phoneNumber": "+255800110042",
"email": "info@trc.co.tz",
"website": "https://sgrticket.trc.co.tz",
"type": "TRAIN",
"logos": []
}
}
]
}
Use one item from data as the selected train option in your application UI. Pass that full selected item to Get Train Details to retrieve cabins, passenger-type prices, seat maps, and booking requirements.
Response Structure
| Field | Type | Description |
|---|---|---|
meta | object | Search metadata including result count, provider, self link, and response timestamp. |
data | array | List of train options matching the requested origin, destination, and date. |
Meta Object
| Field | Type | Description |
|---|---|---|
count | number | Number of train options returned. |
provider | string | Train provider key. Current value is TRC_SGR. |
links.self | string | Fully qualified URL for the current search request. |
createdAt | string | ISO timestamp for when the search response was generated. |
Train Result Object
Each item in data represents one train option.
| Field | Type | Description |
|---|---|---|
provider | string | Provider key for the train option. Current value is TRC_SGR. |
trip | object | Train identity, timetable IDs, GTFS trip ID, headsign, and accessibility flags. |
route | object | Route identity and GTFS route metadata. |
departure | object | Departure date, time, ISO date-time, and origin stop. |
arrival | object | Arrival date, time, ISO date-time, and destination stop. |
price | object | TRC fare, Safiri fee breakdown, collected/debited total, base currency, and converted otherCurrencies. |
stopTimes | array or null | Ordered route stops with arrival and departure times. May be null if GTFS stop-time enrichment is unavailable. |
cars | array | Railway car classes, capacities, seat availability, and booked seat metadata. |
carTypes | array | Deduplicated cabin/class list derived from cars. Useful for cabin filters. |
transitAgency | object | Operator information for Tanzania Railways Corporation. |
Trip Object
| Field | Type | Description |
|---|---|---|
id | number | TRC trip identifier. |
name | string | Train trip name. |
number | string | Train number. |
trainSetId | number | TRC train set identifier. |
timeTableId | number | TRC timetable identifier. |
timeTableTrainSetId | number | TRC timetable train set identifier. |
gtfsTripId | string or null | Matching GTFS trip ID when available. |
headsign | string or null | Route headsign showing the destination and major intermediate stops. |
wheelchairAccessible | number or null | GTFS wheelchair accessibility flag when available. |
bikesAllowed | number or null | GTFS bike allowance flag when available. |
Price Object
| Field | Type | Description |
|---|---|---|
pricingContext | string | Pricing mode used for display and booking: ONLINE, SAFIRI_AGENT, or THIRD_PARTY_AGENT. |
currency | string | Base fare currency. Current SGR fares are returned in TZS. |
total | string | Dynamic TRC fare amount. This is the transporter ticket cost and is included in the wallet debit. |
bookingFeeTotal | string | Safiri-controlled fees above the TRC fare. For third-party agents, this is Safiri's booking fee unless another fee component is explicitly returned. |
grandTotal | string | Amount Safiri debits from the third-party agent wallet for this ticket. It does not include any additional markup you charge your customer outside Safiri. |
fees | array | Wallet-debit component breakdown. Includes TRANSPORTER, FACILITATION, and SAFIRI_COMMISSION; zero-value components are included as "0". |
otherCurrencies | object | Converted TRC fare amounts keyed by currency code. Each currency contains an amount and a human-readable formatted value. |
Price Fee Object
| Field | Type | Description |
|---|---|---|
type | string | Fee component type: TRANSPORTER, FACILITATION, or SAFIRI_COMMISSION. |
amount | string | Component amount in the price currency. The returned fee amounts sum to grandTotal. |
recipient | string | Component recipient, for example TRC, SAFIRI_AGENT, or SAFIRI. |
Currency Amount Object
| Field | Type | Description |
|---|---|---|
amount | number | Numeric amount in the target currency. |
formatted | string | Display-ready amount with currency code and separators. |
Railway Car Object
| Field | Type | Description |
|---|---|---|
id | number | Timetable railway car identifier. |
carId | number | Railway car type identifier. |
orderNo | number | Car order in the train. |
typeId | number | TRC class/type identifier. |
typeName | string | Passenger class name, for example Royal Class, Business, or Economy. |
capacity | number | Total car capacity. |
standardSeatCapacity | number | Standard seat capacity. |
disabledSeatCapacity | number | Accessible seat capacity. |
emptySeats | number | Currently available seats for the car. |
emptyStandardSeats | number | Currently available standard seats. |
emptyDisabledSeats | number | Currently available accessible seats. |
hasSeat | boolean | Whether the car has reserved seats. |
bookedSeats | array | Seat booking metadata when returned by the provider. |
Get Train Details
Retrieve detailed cabin, passenger-type fare, seat map, and booking requirement information for one selected train option.
Endpoint: POST /api/trip/train/details
Example Request:
POST /api/trip/train/details
Request Body
Pass the full train option selected from the Search for Trains response.
{
"trip": {
"provider": "TRC_SGR",
"trip": {
"id": 98,
"name": "DAR - DOM \\ EXPRESS",
"number": "11902",
"trainSetId": 20,
"timeTableId": 299,
"timeTableTrainSetId": 8537
},
"route": {
"id": 68,
"gtfsRouteId": "SASGR002:SER002",
"name": "DAR - DOM"
},
"departure": { "...": "full departure object from search" },
"arrival": { "...": "full arrival object from search" },
"price": { "...": "full price object from search" },
"stopTimes": [],
"cars": [],
"transitAgency": { "...": "full transit agency object from search" }
}
}
Response Structure
| Field | Type | Description |
|---|---|---|
meta | object | Details metadata including provider, self link, and response timestamp. |
data | object | The selected train option enriched with train-set, cabin, seat, fare, and booking requirement details. |
Details Data Object
The details response returns the same selected trip fields from search, plus train and bookingRequirements.
| Field | Type | Description |
|---|---|---|
provider | string | Provider key for the train option. Current value is TRC_SGR. |
trip | object | Train identity, timetable IDs, GTFS trip ID, headsign, and accessibility flags. |
route | object | Route identity and GTFS route metadata. |
departure | object | Departure date, time, ISO date-time, and origin stop. |
arrival | object | Arrival date, time, ISO date-time, and destination stop. |
price | object | Base amount, base currency, and converted otherCurrencies. |
stopTimes | array or null | Ordered route stops with arrival and departure times. |
train | object | Detailed train-set information and cabin/car layout. |
bookingRequirements | object | Passenger details and option lists required for seat reservation. |
transitAgency | object | Operator information for Tanzania Railways Corporation. |
Train Object
| Field | Type | Description |
|---|---|---|
id | number | Timetable train set identifier. |
typeId | number | Train type identifier. |
type | string | Train type name, for example High Speed Train. |
name | string | Train set name. |
number | number | Train set number. |
carTypes | array | Deduplicated cabin/class list derived from cars. |
cars | array | Cabin/car details, including prices and seats. |
Train Car Object
| Field | Type | Description |
|---|---|---|
id | number | Timetable car identifier. |
name | string | Car name from the provider. |
orderNo | number | Car order in the train. |
carId | number | Railway car identifier. |
carTypeId | number | Railway car class/type identifier. |
carType | string | Passenger class name, for example Royal Class, Business, or Economy. |
capacity | number | Total car capacity. |
standardCapacity | number | Standard seat capacity. |
disabledCapacity | number | Accessible seat capacity. |
emptySeats | number | Currently available seats in this car. |
rowCount | number | Number of seat-layout rows. |
columnCount | number | Number of seat-layout columns. |
hasSeat | boolean | Whether the car has selectable seats. |
isActive | boolean | Whether the car is active. |
isDisabled | boolean | Whether the car is disabled/unavailable. |
prices | array | Passenger-type prices for this car. |
seats | array | Seat map for this car. |
Car Price Object
| Field | Type | Description |
|---|---|---|
ticketTypeId | number | Passenger ticket type identifier. |
ticketTypeCode | number | Passenger ticket type code. |
ticketTypeName | string | Passenger type, for example Adult (13 and Above) or Child (4 - 12 Years). |
pricingContext | string | Pricing mode used for this fare: ONLINE, SAFIRI_AGENT, or THIRD_PARTY_AGENT. |
total | string | Dynamic TRC fare amount for this passenger type and car. This is the transporter ticket cost and is included in the wallet debit. |
bookingFeeTotal | string | Safiri-controlled fees above the TRC fare. For third-party agents, this is Safiri's booking fee unless another fee component is explicitly returned. |
grandTotal | string | Amount Safiri debits from the third-party agent wallet for this ticket. It does not include any additional markup you charge your customer outside Safiri. |
fees | array | Wallet-debit component breakdown with TRANSPORTER, FACILITATION, and SAFIRI_COMMISSION. |
formattedPrice | string | Display-ready fare amount. |
currency | string | Fare currency. |
otherCurrencies | object | Converted fare amounts keyed by currency code. |
luggageTypeId | number | Selected/default luggage type identifier. |
luggageTypeName | string | Selected/default luggage type name. |
luggagePrice | number | Luggage price amount. |
Seat Object
| Field | Type | Description |
|---|---|---|
id | string | Seat identifier. |
label | string | Seat label shown to passengers. Empty for non-seat layout cells. |
positionY | number | Zero-based display row. |
positionX | number | Zero-based display column within the row. |
key | string | Stable frontend key for the layout cell. |
type | string | Layout cell type, for example SEAT, DISABLED_SEAT, AISLE, WC, LUGGAGE, or BED. |
class | string | Cabin/class for the car, for example ECONOMY, BUSINESS, or ROYAL_CLASS. |
orientation | string | Seat orientation for layout rendering: UP, DOWN, LEFT, or RIGHT. |
available | boolean | Whether the seat can be selected. |
Booking Requirements Object
| Field | Type | Description |
|---|---|---|
travelerRequirements | object | Required traveler fields for seat reservation. |
nationalityOptions | array | Valid passenger nationality/residency options. |
luggageOptions | array | Valid luggage options. |
ticketTypeOptions | array | Valid passenger ticket type options. |
Example response
{
"meta": {
"provider": "TRC_SGR",
"links": {
"self": "https://booking-api.safiri.app/api/trip/train/details"
},
"createdAt": "2026-06-10T01:30:01.015Z"
},
"data": {
"provider": "TRC_SGR",
"trip": {
"id": 98,
"name": "DAR - DOM \\ EXPRESS",
"number": "11902",
"trainSetId": 20,
"timeTableId": 299,
"timeTableTrainSetId": 8537,
"gtfsTripId": "SASGR002:O",
"headsign": "Dodoma via Pugu, Soga, Ruvu, Ngerengere, Morogoro, Mkata, Kilosa, Kidete, Gulwe, Igandu",
"wheelchairAccessible": 1,
"bikesAllowed": null
},
"route": {
"id": 68,
"gtfsRouteId": "SASGR002:SER002",
"name": "DAR - DOM"
},
"departure": {
"date": "2026-06-09",
"time": "06:00:00",
"dateTime": "2026-06-09T06:00:00+03:00",
"stop": {
"stop_id": "DARESALAAM:SGR:0",
"stop_name": "Dar es Salaam (John Magufuli SGR Station)"
}
},
"arrival": {
"date": "2026-06-09",
"time": "07:40:00",
"dateTime": "2026-06-09T07:40:00+03:00",
"stop": {
"stop_id": "MOROGORO:SGR:0",
"stop_name": "Morogoro (Jakaya Kikwete SGR Station)"
}
},
"price": {
"pricingContext": "ONLINE",
"currency": "TZS",
"total": "22500",
"bookingFeeTotal": "2000",
"grandTotal": "24500",
"fees": [
{
"type": "TRANSPORTER",
"amount": "22500",
"recipient": "TRC"
},
{
"type": "FACILITATION",
"amount": "0",
"recipient": "SAFIRI_AGENT"
},
{
"type": "SAFIRI_COMMISSION",
"amount": "2000",
"recipient": "SAFIRI"
}
],
"otherCurrencies": {
"TZS": {
"amount": 22500,
"formatted": "TZS 22,500.00"
},
"USD": {
"amount": 8.57,
"formatted": "USD 8.57"
}
}
},
"stopTimes": [
{
"stopId": "DARESALAAM:SGR:0",
"stopSequence": 0,
"arrivalTime": "05:55:00",
"departureTime": "06:00:00"
},
{
"stopId": "MOROGORO:SGR:0",
"stopSequence": 5,
"arrivalTime": "08:35:33",
"departureTime": "08:40:33"
}
],
"train": {
"id": 8537,
"typeId": 13,
"type": "High Speed Train",
"name": "EXPRESS TRAIN",
"number": 1011,
"cars": [
{
"id": 417,
"name": "ROYAL CLASS KIOSK",
"orderNo": 1,
"carId": 19,
"carTypeId": 151,
"carType": "Royal Class",
"capacity": 44,
"standardCapacity": 44,
"disabledCapacity": 0,
"emptySeats": 0,
"rowCount": 12,
"columnCount": 5,
"hasSeat": true,
"isActive": true,
"isDisabled": false,
"prices": [
{
"ticketTypeId": 4,
"ticketTypeCode": 4,
"ticketTypeName": "Adult (13 and Above)",
"pricingContext": "ONLINE",
"total": "45500",
"bookingFeeTotal": "2000",
"grandTotal": "47500",
"fees": [
{
"type": "TRANSPORTER",
"amount": "45500",
"recipient": "TRC"
},
{
"type": "FACILITATION",
"amount": "0",
"recipient": "SAFIRI_AGENT"
},
{
"type": "SAFIRI_COMMISSION",
"amount": "2000",
"recipient": "SAFIRI"
}
],
"formattedPrice": "TZS 45,500.00",
"currency": "TZS",
"otherCurrencies": {
"TZS": {
"amount": 45500,
"formatted": "TZS 45,500.00"
},
"USD": {
"amount": 17.34,
"formatted": "USD 17.34"
}
},
"luggageTypeId": 5,
"luggageTypeName": "None",
"luggagePrice": 0
}
],
"seats": [
{
"id": "1020",
"label": "23",
"positionY": 6,
"positionX": 1,
"key": "1020",
"type": "SEAT",
"class": "ROYAL_CLASS",
"orientation": "DOWN",
"available": true
}
]
}
]
},
"bookingRequirements": {
"travelerRequirements": {
"travelerId": true,
"genderRequired": true,
"ageCategory": true,
"seatRequired": true,
"nationality": true,
"luggageRequired": false,
"emailAddressRequired": false,
"mobilePhoneNumberRequired": true
},
"nationalityOptions": [
{
"code": 20,
"id": 20,
"title": "Non-Resident",
"price": null
},
{
"code": 19,
"id": 19,
"title": "Resident",
"price": null
}
],
"luggageOptions": [
{
"id": 5,
"name": "None",
"description": "None",
"weight": 0,
"price": 0,
"isActive": true,
"configurationItemId": 98
}
],
"ticketTypeOptions": [
{
"code": 4,
"id": 4,
"title": "Adult (13 and Above)",
"price": 45500
},
{
"code": 3,
"id": 3,
"title": "Child (4 - 12 Years)",
"price": 45500
}
]
},
"transitAgency": {
"name": "Tanzania Railways Corporation",
"tradingName": "Tanzania Railways Corporation",
"tinNumber": "000000001",
"uuid": "204ea6a5-e374-4a22-a8b0-7a1d406c0514",
"phoneNumber": "+255800110042",
"email": "info@trc.co.tz",
"website": "https://sgrticket.trc.co.tz",
"type": "TRAIN",
"logos": []
}
}
}
Hold Train Seat
Hold one selected SGR seat before collecting passenger details. Call this endpoint when a customer selects a seat in the seat map.
Endpoint: POST /api/trip/train/seat/hold
Example Request:
POST /api/trip/train/seat/hold
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
departureStopPartnerId | number | Yes | TRC station ID from departure.stop.partners[].id. |
arrivalStopPartnerId | number | Yes | TRC station ID from arrival.stop.partners[].id. |
timeTableTrainSetId | number | Yes | trip.timeTableTrainSetId from train search/details. |
carId | number | Yes | Selected details car id. |
seat | number | Yes | Selected seat id. |
luggageId | number | No | Luggage option ID. Defaults to 5 (None). |
{
"departureStopPartnerId": 1,
"arrivalStopPartnerId": 12,
"timeTableTrainSetId": 8710,
"carId": 497,
"seat": 2119,
"luggageId": 5
}
Success Response
{
"meta": {
"provider": "TRC_SGR",
"links": {
"self": "https://booking-api.safiri.app/api/trip/train/seat/hold"
},
"createdAt": "2026-06-12T10:00:00.000Z"
},
"data": {
"provider": "TRC_SGR",
"hold": {
"key": "f70c1e0e-9849-41dd-b3b7-949eac159dc2",
"status": "HELD"
}
}
}
Seat Already Taken
If TRC reports that the seat is no longer available, the API returns 400.
{
"error": {
"name": "modal",
"seatId": 2119
},
"errorCode": "SEAT_ALREADY_BOOKED",
"errorMessage": "This seat is taken.",
"status": 400
}
Release Train Seat
Release a previously held SGR seat. Call this endpoint when a customer unselects a seat or abandons seat selection before reservation.
Endpoint: GET /api/trip/train/seat/release
Example Request:
GET /api/trip/train/seat/release?key=f70c1e0e-9849-41dd-b3b7-949eac159dc2
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Hold key returned by Hold Train Seat. |
Success Response
{
"meta": {
"provider": "TRC_SGR",
"links": {
"self": "https://booking-api.safiri.app/api/trip/train/seat/release?key=f70c1e0e-9849-41dd-b3b7-949eac159dc2"
},
"createdAt": "2026-06-12T10:02:00.000Z"
},
"data": {
"provider": "TRC_SGR",
"hold": {
"key": "f70c1e0e-9849-41dd-b3b7-949eac159dc2",
"status": "RELEASED"
}
}
}
This endpoint uses GET because it wraps the upstream TRC release endpoint. Responses include Cache-Control: no-store.
Reserve Held Train Seats
Convert held seats into a confirmed reservation and open a 15-minute Safiri payment window for the customer to complete payment.
Endpoint: POST /api/trip/train/seat/reserve
The authenticated JWT user is stored on each local train ticket as boughtByUser. bookingUserType is derived server-side from that user and the train operator, so clients should not send boughtByUser or bookingUserType.
Example Request:
POST /api/trip/train/seat/reserve
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer JWT for the user creating the reservation. |
x-authentication-context | No | Booking channel saved on the train ticket. Defaults to WEB_APP when omitted. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
outbound | object | Yes | Full selected train details object from Get Train Details, containing only the selected cars/seats. |
return | object or null | No | Full selected return train details object. Use null or omit for one-way bookings. |
passengers | array | Yes | Passenger and held-seat details. Supports multiple passengers. |
issuerDeviceInfo | object | No | Device/app metadata for the issuer, using the same shape as bus ticket issuerDeviceInfo. |
Each passenger must include the common passenger fields and outbound seat fields. Return seat fields are required only when return is supplied.
| Passenger Field | Type | Required | Description |
|---|---|---|---|
passengerName | string | Yes | Passenger name sent to TRC. |
identityNo | string | No | Passenger identity number. Use an empty string when not available. |
phone | string | Yes | Passenger phone number saved by Safiri. |
countryCode | string | No | Passenger phone country calling code, for example 255 or 44. Safiri derives and saves this from the parsed phone when countryCodeName is supplied. |
countryCodeName | string | No | Passenger ISO country code, for example TZ or GB. Required for local-format phone numbers when countryCode is omitted. Safiri saves the parsed country in lowercase, for example tz or gb. |
email | string | No | Passenger email address. Omit or use an empty string when not available. When provided, it must be a valid email format. |
nationalityTypeId | number | Yes | Value from bookingRequirements.nationalityOptions[].id. |
isDisabledPassenger | boolean | No | Whether the passenger is disabled. |
ticketTypeId | number | Yes | Value from bookingRequirements.ticketTypeOptions[].id, for example adult or child. |
outboundSeatHoldKey | string | Yes | Hold key returned by the outbound seat hold call. |
outboundCarId | number | Yes | Selected outbound details car id. |
outboundCarTypeId | number | Yes | Selected outbound details car carTypeId. |
outboundSeatId | number | Yes | Selected outbound seat id. |
returnSeatHoldKey | string | Return only | Hold key returned by the return seat hold call. |
returnCarId | number | Return only | Selected return details car id. |
returnCarTypeId | number | Return only | Selected return details car carTypeId. |
returnSeatId | number | Return only | Selected return seat id. |
Do not send payer information or luggage details. The API currently uses fixed payer information server-side and luggageTypeId: 5 (None) for TRC pricing and reservation.
Safiri validates the passenger phone against the submitted country information, then saves the passenger phone as provided plus the parsed calling code and lowercase ISO country. When submitting to TRC, non-Tanzania passenger numbers are replaced only in the TRC request because TRC does not support international phone numbers.
One-Way Example
{
"outbound": {
"provider": "TRC_SGR",
"trip": {
"id": 98,
"name": "DAR - DOM \\ EXPRESS",
"timeTableTrainSetId": 8710
},
"route": {
"id": 68,
"name": "DAR - DOM"
},
"departure": { "...": "full departure object from details" },
"arrival": { "...": "full arrival object from details" },
"train": {
"cars": [
{
"id": 497,
"carTypeId": 151,
"carType": "Royal Class",
"seats": [
{
"id": "2119",
"label": "23"
}
]
}
]
},
"transitAgency": { "...": "full transit agency object from details" }
},
"return": null,
"passengers": [
{
"passengerName": "ITULE",
"identityNo": "",
"phone": "0757713888",
"countryCodeName": "TZ",
"email": "abraham@itule.me",
"nationalityTypeId": 19,
"isDisabledPassenger": false,
"ticketTypeId": 4,
"outboundSeatHoldKey": "f70c1e0e-9849-41dd-b3b7-949eac159dc2",
"outboundCarId": 497,
"outboundCarTypeId": 151,
"outboundSeatId": 2119
}
],
"issuerDeviceInfo": {
"deviceId": "device-1",
"deviceBrand": "Apple",
"deviceModel": "iPhone",
"operatingSystem": "ios",
"operatingSystemVersion": "18.0",
"appName": "Safiri",
"appVersion": "1.0.0"
}
}
Return Journey Example
Use a separate selected details object and separate held seat for the return train.
{
"outbound": { "...": "selected outbound train details object" },
"return": { "...": "selected return train details object" },
"passengers": [
{
"passengerName": "ITULE",
"identityNo": "",
"phone": "0757713888",
"countryCodeName": "TZ",
"email": "abraham@itule.me",
"nationalityTypeId": 19,
"isDisabledPassenger": false,
"ticketTypeId": 4,
"outboundSeatHoldKey": "outbound-seat-hold-key",
"outboundCarId": 497,
"outboundCarTypeId": 151,
"outboundSeatId": 2119,
"returnSeatHoldKey": "return-seat-hold-key",
"returnCarId": 501,
"returnCarTypeId": 151,
"returnSeatId": 2120
}
]
}
Success Response
{
"meta": {
"provider": "TRC_SGR",
"links": {
"self": "https://booking-api.safiri.app/api/trip/train/seat/reserve"
},
"createdAt": "2026-06-12T10:05:00.000Z"
},
"data": {
"provider": "TRC_SGR",
"journeyType": "ONE_WAY",
"reservation": {
"pnr": "PNR00019e8aa4492c00",
"ticketNumbers": ["TRC00019e8aa449a500"],
"passengerCount": 1,
"segmentCount": 1
},
"bill": {
"id": "TRC-Pay062604545d07cf93a6ce4878913fb6fff8db2f76",
"expireDate": "2026-06-12T10:30:00.000+03:00",
"paymentRequired": true,
"controlNumber": "987264686974"
},
"paymentReference": "SGR1234567890123456",
"paymentWindow": {
"expiresInMinutes": 15,
"internalExpiresAt": "2026-06-12T10:20:00.000Z",
"expiresAt": "2026-06-12T10:20:00.000Z",
"providerExpiresAt": "2026-06-12T10:30:00.000+03:00"
},
"paymentItems": [
{
"identifier": "TRC00019e8aa449a500",
"journey": "OUTBOUND",
"passengerName": "ITULE",
"price": {
"pricingContext": "ONLINE",
"currency": "TZS",
"total": "45500",
"bookingFeeTotal": "2000",
"grandTotal": "47500",
"fees": [
{
"type": "TRANSPORTER",
"amount": "45500",
"recipient": "TRC"
},
{
"type": "FACILITATION",
"amount": "0",
"recipient": "SAFIRI_AGENT"
},
{
"type": "SAFIRI_COMMISSION",
"amount": "2000",
"recipient": "SAFIRI"
}
],
"otherCurrencies": {
"TZS": {
"amount": 45500,
"formatted": "TZS 45,500.00"
}
}
},
"trainTicketUuid": "c66b52a9-d5d3-4e58-bc15-7f2d91da18f9"
}
],
"workflowId": "SGR_TRAIN_SEAT_RESERVATION-PNR00019e8aa4492c00-passenger-group-id",
"trainTicketUuids": ["c66b52a9-d5d3-4e58-bc15-7f2d91da18f9"]
}
}
Reservation Retry Behavior
The reservation endpoint is designed to resume safely after partial upstream success:
- If the upstream provider already created the ticket for the held seat, the API looks up local
TrainTicketrecords byseatHoldKeyand resumes from the stored reservation data rather than creating a duplicate. - If a payment window was already opened for the booking, the API skips re-opening it and returns the existing payment details.
The endpoint can only resume an existing booking when matching local TrainTicket records are found. If the upstream provider reports an existing ticket but no local record is found, the API returns a controlled error.
Error Responses
When a train request fails, the API returns a standard error object:
{
"error": {},
"errorCode": "FROM_STOP_ID_REQUIRED",
"errorMessage": "Please provide from, to and date for SGR train search",
"status": 400
}
Common causes include:
- Missing
from,to, ordate - A stop ID that does not exist in the train GTFS data
- A stop that is not an SGR stop with TRC partner metadata
- Temporary unavailability from the upstream TRC search service
- A selected seat that no longer matches the selected train details payload
- A seat that has already been booked or held by another customer
- A reservation retry where no matching local booking record is found for the held seat