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"
}
]
}
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:
| Parameter | Description |
|---|---|
from | Origin station stop_id, for example DARESALAAM:SGR:0. |
to | Destination station stop_id, for example MOROGORO:SGR:0. |
date | Travel 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:
| Component | Field | Meaning |
|---|---|---|
| Transporter fee | price.fees[] where type is TRANSPORTER; also price.total | The TRC fare payable to the transporter. |
| Safiri fee | price.fees[] where type is SAFIRI_COMMISSION | Safiri's fee for processing the booking. |
| Wallet debit | price.grandTotal | The 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:
| Field | How to use it |
|---|---|
trip.name and trip.number | Show the train service name and train number. |
departure.dateTime and arrival.dateTime | Show the local departure and arrival date-time. |
route.name and trip.headsign | Show the route name and major intermediate stops. |
price.total, price.bookingFeeTotal, price.grandTotal, price.fees, price.currency, and price.otherCurrencies | Show the TRC fare, Safiri fee breakdown, Safiri wallet debit amount, and supported converted currencies. Add any customer-facing markup in your own system. |
stopTimes | Show the full route, intermediate stops, and stop-level timing. |
train.cars | Show available cabins such as Royal Class, Business, and Economy, plus capacity and available seats. |
train.cars[].prices | Show passenger-type prices for the selected cabin, for example adult and child fares. |
train.cars[].seats | Render the seat map and show which seats are available. |
bookingRequirements | Capture the traveler fields and options needed by the reservation flow. |
transitAgency | Show 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"]
}
}
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.
Related Reference
See the Train Booking API reference for full endpoint details, response field descriptions, and larger examples.