Skip to main content

Train Booking Workflow

Use this workflow to discover SGR stations, search train options, retrieve cabin and seat details, hold selected seats, and create an SGR reservation with a 15-minute Safiri payment window.

Step 1: Get Train Stations

Fetch the station list from the train GTFS API.

GET https://train-gtfs-api.safiri.app/api/stops?mini=true

Use the stop_id field from the response as the station identifier in train search requests.

{
"stop_id": "DARESALAAM:SGR:0",
"stop_name": "Dar es Salaam (John Magufuli SGR Station)",
"stop_desc": "Kisutu, Ilala, Dar es Salaam, Tanzania",
"partners": [
{
"provider": "TRC",
"service": "SGR",
"id": 1,
"name": "Dar Es Salaam"
}
]
}
tip

For SGR train search, use stations that include TRC partner metadata with "service": "SGR".

Step 2: Search for Trains

Search with the origin stop ID, destination stop ID, and travel date.

GET /api/trip/train/search?from=DARESALAAM:SGR:0&to=MOROGORO:SGR:0&date=2026-06-10

Required query parameters:

ParameterDescription
fromOrigin station stop_id, for example DARESALAAM:SGR:0.
toDestination station stop_id, for example MOROGORO:SGR:0.
dateTravel date in YYYY-MM-DD format.

The response contains a meta object and a data array of train options.

{
"meta": {
"count": 6,
"provider": "TRC_SGR"
},
"data": [
{
"provider": "TRC_SGR",
"trip": {
"id": 98,
"name": "DAR - DOM \\ EXPRESS",
"number": "11902"
},
"departure": {
"date": "2026-06-10",
"time": "06:00:00"
},
"arrival": {
"date": "2026-06-10",
"time": "07:40:00"
},
"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"
}
}
}
}
]
}

Pricing and Wallet Charges

For third-party agent integrations, treat the returned price.grandTotal as the amount Safiri will deduct from your Safiri wallet for that train ticket. It is made up of:

ComponentFieldMeaning
Transporter feeprice.fees[] where type is TRANSPORTER; also price.totalThe TRC fare payable to the transporter.
Safiri feeprice.fees[] where type is SAFIRI_COMMISSIONSafiri's fee for processing the booking.
Wallet debitprice.grandTotalThe total amount Safiri charges your wallet for the ticket.

You may charge your own customer any additional service, agency, or handling fee above price.grandTotal. That extra customer-facing amount is not sent to Safiri in the reservation request and is not deducted from your Safiri wallet.

Example: if price.total is 22500, SAFIRI_COMMISSION is 2000, and price.grandTotal is 24500, Safiri deducts 24500 from your wallet. If you charge the customer 26000, the extra 1500 is your own markup outside Safiri.

Step 3: Get Train Details

After the customer selects a train option from data, send the full selected item to the details endpoint.

POST /api/trip/train/details
{
"trip": {
"provider": "TRC_SGR",
"trip": {
"id": 98,
"name": "DAR - DOM \\ EXPRESS",
"number": "11902"
},
"route": { "...": "full route object from search" },
"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" }
}
}

The response keeps the selected trip information and adds detailed train data.

{
"meta": {
"provider": "TRC_SGR"
},
"data": {
"provider": "TRC_SGR",
"trip": {
"id": 98,
"name": "DAR - DOM \\ EXPRESS",
"number": "11902"
},
"train": {
"id": 8537,
"type": "High Speed Train",
"name": "EXPRESS TRAIN",
"cars": [
{
"id": 417,
"name": "ROYAL CLASS KIOSK",
"carType": "Royal Class",
"emptySeats": 0,
"prices": [
{
"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"
}
],
"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
}
}
}
}

Step 4: Choose a Train Option, Cabin, and Seat

Present each item from the search data as a train option, then use the details response to show cabins, fares, and seats. Useful fields for the selection UI include:

FieldHow to use it
trip.name and trip.numberShow the train service name and train number.
departure.dateTime and arrival.dateTimeShow the local departure and arrival date-time.
route.name and trip.headsignShow the route name and major intermediate stops.
price.total, price.bookingFeeTotal, price.grandTotal, price.fees, price.currency, and price.otherCurrenciesShow the TRC fare, Safiri fee breakdown, Safiri wallet debit amount, and supported converted currencies. Add any customer-facing markup in your own system.
stopTimesShow the full route, intermediate stops, and stop-level timing.
train.carsShow available cabins such as Royal Class, Business, and Economy, plus capacity and available seats.
train.cars[].pricesShow passenger-type prices for the selected cabin, for example adult and child fares.
train.cars[].seatsRender the seat map and show which seats are available.
bookingRequirementsCapture the traveler fields and options needed by the reservation flow.
transitAgencyShow the train operator details when needed.

Step 5: Hold and Release Seats

When a customer selects a seat, hold it immediately.

POST /api/trip/train/seat/hold
{
"departureStopPartnerId": 1,
"arrivalStopPartnerId": 12,
"timeTableTrainSetId": 8710,
"carId": 497,
"seat": 2119,
"luggageId": 5
}

The response returns a hold.key. Store that key on the selected passenger/seat in your UI.

{
"data": {
"provider": "TRC_SGR",
"hold": {
"key": "f70c1e0e-9849-41dd-b3b7-949eac159dc2",
"status": "HELD"
}
}
}

If the customer unselects the seat, release it.

GET /api/trip/train/seat/release?key=f70c1e0e-9849-41dd-b3b7-949eac159dc2

Step 6: Reserve Held Seats

After passenger details are submitted, send the selected details object and passenger-held-seat mapping to the reservation endpoint.

POST /api/trip/train/seat/reserve

For one-way bookings, send return: null or omit return.

{
"outbound": { "...": "selected outbound train details object" },
"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
}
]
}

For return journeys, use a separate selected return train and separate return seat hold key.

{
"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
}
]
}

Reservations require a Bearer JWT. The API stores the authenticated user on each local train ticket as boughtByUser, derives bookingUserType server-side, and saves bookingChannel from x-authentication-context or WEB_APP when the header is omitted. Include issuerDeviceInfo in the request body when the issuing app has device metadata to record.

For third-party agents, make sure the authenticated user's Safiri wallet can cover the selected tickets' price.grandTotal values. Your own customer markup should stay outside the reservation payload.

The reservation response includes the TRC PNR, ticket numbers, bill ID, control number, a 15-minute Safiri payment window, payment items, and local train ticket UUIDs.

{
"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"
},
"trainTicketUuids": ["c66b52a9-d5d3-4e58-bc15-7f2d91da18f9"]
}
}
note

The reservation endpoint can resume safely after partial success. It looks up local train ticket records by seatHoldKey and, when a complete reservation is found (confirmed PNR, workflow, and payment reference), returns the existing data immediately without repeating any upstream steps.

Step 7: Payment Window

Use paymentReference to collect payment through Safiri. After reservation, a 15-minute payment window opens. If payment is not confirmed within the window, the local train tickets are marked CANCELLED. If payment is confirmed, they are marked PAID.

See the Train Booking API reference for full endpoint details, response field descriptions, and larger examples.