Lifecycle of a Trip#

The journey of a Customer starts with the need for a Trip to bring them from the requested origin to the requested destination. Using the Trip API, we can request Offers for Trips based on the Customer’s travel needs and order one for them.

Create the client#

tripClient := tripv1.NewTripStateServiceClient(conn)
  const client = clientFactory.create(TripStateServiceDefinition, channel);
trait TripService:
  def requestTripOffers(customerId: String): Task[Seq[Offer]]
  def orderTrip(customerId: String, offerToken: String): Task[String]
  def getTrip(customerId: String, tripId: String): Task[Trip]
  def cancelTripAsCustomer(customerId: String, tripId: String): Task[Unit]
  def cancelTripAsIntegrator(tripId: String): Task[Unit]

Requesting Offers for a Trip#

First, we request Offers for a potential Trip. This Offer request includes the origin and destination (using NamedLocation), the rider(s) or object(s) that will be part of the Trip (using Load) and finally either the preferred time of departure or the preferred time of arrival (using google.protobuf.Timestamp).

The following example details a RequestTripOfferRequest for a single adult (no wheelchair needed) wishing to go from Point A to Point B with a departure at 13:30 on the 05.08.2024.

grpcurl \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Customer-Id: ${CUSTOMER_ID}" \
  -import-path ./protos/ \
  -proto "moia/trip/state/v1/trip_state.proto" \
  -proto "google/rpc/error_details.proto" \
  -d "{\"origin\":{\"location\":{\"latitude\":53.6344823,\"longitude\":10.0555216},\"primary_address\":\"Stüberedder 14\",\"secondary_address\":\"22337 Hamburg\",\"primary_poi_name\":\"PointA\",\"secondary_poi_name\":\"Hamburg, Germany\"},\"destination\":{\"location\":{\"latitude\":53.6342303,\"longitude\":10.0741517},\"primary_address\":\"Eckerkamp 38\",\"secondary_address\":\"22391 Hamburg,\",\"primary_poi_name\":\"PointB\",\"secondary_poi_name\":\"Hamburg, Germany\"},\"load\":[{\"adult\":{}}],\"departure_time\":\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\",\"approach_speed\":\"APPROACH_SPEED_MEDIUM\",\"visual_impairment_support\":\"false\"}" \
  "$API_URL" moia.trip.state.v1.TripStateService/RequestTripOffers
// define origin and destination location details
primaryAddressOrigin := "Stüberedder 14"
secondaryAddressOrigin := "22337 Hamburg"
primaryPoiNameOrigin := "Point A"
secondaryPoiNameOrigin := "Hamburg, Germany"
primaryAddressDestination := "Eckerkamp 38"
secondaryAddressDestination := "22391 Hamburg"
primaryPoiNameDestination := "Point B"
secondaryPoiNameDestination := "Hamburg, Germany"

tripOfferRequest := &tripv1.RequestTripOffersRequest{
	Origin: &tripv1.NamedLocation{
		Location: &types_v1.LatLon{
			Latitude:  53.6344823,
			Longitude: 10.0555216,
		},
		PrimaryAddress:   &primaryAddressOrigin,
		SecondaryAddress: &secondaryAddressOrigin,
		PrimaryPoiName:   &primaryPoiNameOrigin,
		SecondaryPoiName: &secondaryPoiNameOrigin,
	},
	Destination: &tripv1.NamedLocation{
		Location: &types_v1.LatLon{
			Latitude:  53.6342303,
			Longitude: 10.0741517,
		},
		PrimaryAddress:   &primaryAddressDestination,
		SecondaryAddress: &secondaryAddressDestination,
		PrimaryPoiName:   &primaryPoiNameDestination,
		SecondaryPoiName: &secondaryPoiNameDestination,
	},
	Load: []*tripv1.Load{
		{
			LoadType: &tripv1.Load_Adult{
				Adult: &tripv1.Adult{
					Wheelchair: tripv1.Wheelchair_WHEELCHAIR_NOT_NEEDED,
				},
			},
		},
		{
			LoadType: &tripv1.Load_Child{
				Child: &tripv1.Child{
					Wheelchair: tripv1.Wheelchair_WHEELCHAIR_NOT_NEEDED,
					ChildSeat:  tripv1.ChildSeat_CHILD_SEAT_NOT_NEEDED,
				},
			},
		},
	},
	Time: &tripv1.RequestTripOffersRequest_DepartureTime{
		// departure in 1 minute
		DepartureTime: timestamppb.New(time.Now().Add(time.Minute * 1)),
	},
	ApproachSpeed:           tripv1.ApproachSpeed_APPROACH_SPEED_MEDIUM,
	VisualImpairmentSupport: false,
}
// replace with the ID of the customer who is requesting trip offers
ctxWithCustomer := metadata.AppendToOutgoingContext(ctx, "customer-id", currCustomer)
tripOffersResponse, err := tripClient.RequestTripOffers(
	ctxWithCustomer, tripOfferRequest,
)
if err != nil {
	log.Fatalf("Could not request trip offers %v", err)
}
log.Printf("Received %d Trip Offers", len(tripOffersResponse.Offers))
  const offers = await client.requestTripOffers(
    {
      origin: {
        location: {
          latitude: 53.6344823,
          longitude: 10.0555216,
        },
        primaryAddress: "Stüberedder 14",
        secondaryAddress: "22337 Hamburg",
        primaryPoiName: "Point A",
        secondaryPoiName: "Hamburg, Germany",
      },
      destination: {
        location: {
          latitude: 53.6342303,
          longitude: 10.0741517,
        },
        primaryAddress: "Eckerkamp 38",
        secondaryAddress: "22391 Hamburg",
        primaryPoiName: "Point B",
        secondaryPoiName: "Hamburg, Germany",
      },
      load: [
        {
          adult: {
            wheelchair: Wheelchair.WHEELCHAIR_NOT_NEEDED,
          },
        },
        {
          child: {
            wheelchair: Wheelchair.WHEELCHAIR_NOT_NEEDED,
            childSeat: ChildSeat.CHILD_SEAT_NOT_NEEDED,
          },
        },
      ],
      departureTime: new Date(),
      approachSpeed: ApproachSpeed.APPROACH_SPEED_MEDIUM,
      visualImpairmentSupport: false,
    },
    {
      metadata: new grpc.Metadata({
        // replace `currCustomerId` with the ID of the customer who is requesting trip offers
        "customer-id": currCustomerId,
      }),
    },
  );
  console.log(offers); // { offers: [ ... ] }
def requestTripOffers(customerId: String): Task[Seq[Offer]] = ZIO.scoped:
  for
    accessToken <- tokenManager.getAccessToken()
    authMetadata = GrpcClientService.createAuthMetadata(accessToken, Some(customerId))
    tripClient  <- ZioTripState.TripStateServiceClient.scoped(managedChannel, metadata = authMetadata)
    request      = RequestTripOffersRequest(
                     origin = Some(
                       NamedLocation(
                         location = Some(LatLon(latitude = 53.5511, longitude = 9.9937)),
                         primaryAddress = Some("Hauptbahnhof"),
                         secondaryAddress = Some("Hamburg, Germany")
                       )
                     ),
                     destination = Some(
                       NamedLocation(
                         location = Some(LatLon(latitude = 53.5755, longitude = 10.0153)),
                         primaryAddress = Some("Airport"),
                         secondaryAddress = Some("Hamburg, Germany")
                       )
                     ),
                     load = Seq(
                       Load(Load.LoadType.Adult(Adult(wheelchair = Wheelchair.WHEELCHAIR_NOT_NEEDED)))
                     ),
                     time = RequestTripOffersRequest.Time.DepartureTime(Timestamp(Instant.now().plusSeconds(30 * 60))),
                     approachSpeed = ApproachSpeed.APPROACH_SPEED_MEDIUM
                   )
    response    <- tripClient.requestTripOffers(request)
    _           <- ZIO.logInfo(s"Received ${response.offers.length} trip offers")
  yield response.offers

To receive Offers based on a preferred arrival time, we simply replace the field departure_time in the payload with arrival_time:

{
  ...

  "arrival_time": "2024-08-05T13:30:00Z"
}

To receive Offers for multiple Riders or with physical objects, we add multiple Loads in the payload. Here we are requesting Offers for one adult with a wheelchair and one child with a booster seat:

{
  ...

  "load": [
    {
      "adult":
        {
          "wheelchair": "WHEELCHAIR_NEEDED"
        }
    },
    {
      "child":
        {
          "child_seat": "CHILD_SEAT_BOOSTER"
        }
    }
  ]
}

To steer how quickly a Customer is approaching stops, the approach_speed field can be included in the request payload. For example, to allocate more time for a given approach distance:

{
  ...

  "approach_speed": "APPROACH_SPEED_SLOW"
}

We receive a response of type RequestTripOffersResponse which contains a list of Offer. The list is not ordered or ranked in any particular way. The list of Offers may include Offers for varying Service Classes and may also contain multiple options per Service Class.

Ordering an Offer#

Given the list of Offers, we now select the preferred Offer for the Customer and order it. To do so we need to call the OrderTrip RPC with a payload of type OrderTripRequest which contains the token of the Offer you want to order.

The token is a simple non-human readable string.

# export OFFER_TOKEN="<replace with token>"

grpcurl \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Customer-Id: ${CUSTOMER_ID}" \
  -import-path ./protos/ \
  -proto "moia/trip/state/v1/trip_state.proto" \
  -proto "google/rpc/error_details.proto" \
  -d "{\"offer_token\":\"$OFFER_TOKEN\"}" \
  -emit-defaults \
  "$API_URL" moia.trip.state.v1.TripStateService/OrderTrip
// replace with the ID of the customer who is ordering the trip
ctxWithCustomer = metadata.AppendToOutgoingContext(ctx, "customer-id", currCustomer)
orderTripResponse, err := tripClient.OrderTrip(
	ctxWithCustomer,
	&tripv1.OrderTripRequest{
		// replace with the token of the selected offer
		OfferToken: offerToOrder,
	},
)
if err != nil {
	log.Fatalf("Could not order trip %v", err)
}
log.Printf("Successfully ordered trip: %v", jsonMarshalOptions.Format(orderTripResponse)) // { tripId: '7bc0118b-748e-4c13-bc5e-bb2164f04927' }
  const orderResponse = await client.orderTrip(
    {
      // replace `selectedOfferToken` with the token of the selected offer
      offerToken: selectedOfferToken,
    },
    {
      metadata: new grpc.Metadata({
        // replace `currCustomerId` with the ID of the customer who is ordering the trip
        "customer-id": currCustomerId,
      }),
    },
  );
  console.log(orderResponse); // { tripId: '7bc0118b-748e-4c13-bc5e-bb2164f04927' }
def orderTrip(customerId: String, offerToken: String): Task[String] = ZIO.scoped:
  for
    accessToken <- tokenManager.getAccessToken()
    authMetadata = GrpcClientService.createAuthMetadata(accessToken, Some(customerId))
    tripClient  <- ZioTripState.TripStateServiceClient.scoped(managedChannel, metadata = authMetadata)
    orderRequest = OrderTripRequest(offerToken = offerToken)
    response    <- tripClient.orderTrip(orderRequest)
    _           <- ZIO.logInfo(s"Successfully ordered trip: ${response.tripId}")
  yield response.tripId

The response of type OrderTripResponse contains a single field trip_id. At this point in time our order has been processed and the status of our trip is set to STATUS_ORDERED. We will use the trip_id from the response to make other calls to the API to get more details on our trip and follow the many state updates it will go through throughout its lifecycle.

{
  "trip_id": "7bc0118b-748e-4c13-bc5e-bb2164f04927"
}

Getting Details of a Trip#

Given an ordered Trip, we may get the details of it. To do so we need to call the GetTrip RPC with a payload of type GetTripRequest which contains the trip_id of the Trip we ordered.

# export TRIP_ID="<replace with trip id>"
grpcurl \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -import-path ./protos/ \
  -proto "moia/trip/state/v1/trip_state.proto" \
  -proto "google/rpc/error_details.proto" \
  -d "{\"trip_id\":\"$TRIP_ID\"}" \
  "$API_URL" moia.trip.state.v1.TripStateService/GetTrip
// replace with the ID of the customer
ctxWithCustomer = metadata.AppendToOutgoingContext(ctx, "customer-id", currCustomer)
getTripResponse, err := tripClient.GetTrip(
	ctxWithCustomer,
	&tripv1.GetTripRequest{
		TripId: tripId, // replace with tripId to get details for
	},
)
if err != nil {
	log.Fatalf("Could not get trip %v", err)
}
log.Printf("Successfully got trip: %v", jsonMarshalOptions.Format(getTripResponse)) // { trip: { ... } }
  const getTripResponse = await client.getTrip(
    { tripId: orderResponse.tripId },
    {
      metadata: new grpc.Metadata({
        // replace `currCustomerId` with the ID of the customer who is cancelling the trip
        "customer-id": currCustomerId,
      }),
    },
  );
  console.log(getTripResponse); // { trip: { ... } }
def getTrip(customerId: String, tripId: String): Task[Trip] = ZIO.scoped:
  for
    accessToken   <- tokenManager.getAccessToken()
    authMetadata   = GrpcClientService.createAuthMetadata(accessToken, Some(customerId))
    tripClient    <- ZioTripState.TripStateServiceClient.scoped(managedChannel, metadata = authMetadata)
    getTripRequest = GetTripRequest(tripId = tripId)
    response      <- tripClient.getTrip(getTripRequest)
    trip          <- ZIO.fromOption(response.trip).mapError(_ => new Throwable(s"Trip not found: $tripId"))
    _             <- ZIO.logInfo(s"Retrieved trip: ${trip.id}, state: ${trip.state}, customer: ${trip.customerId}")
  yield trip

The response of type GetTripResponse contains a Trip. For more information about a Trip, check the fundamentals.

You may poll this RPC consecutively to get updates about a Trip.

Cancelling a Trip as a Customer#

Given an ongoing Trip, the Customer can cancel it if they decide not to take the trip as long they haven’t yet been picked up. To do so we need to call the CancelTripAsCustomer RPC on behalf of the Customer with a payload of type CancelTripAsCustomerRequest. The payload contains the trip_id of the Trip we want to cancel and the Customer’s ID must be set in the request header.

# export TRIP_ID="<replace with trip id>"

grpcurl \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Customer-Id: ${CUSTOMER_ID}" \
  -import-path ./protos/ \
  -proto "moia/trip/state/v1/trip_state.proto" \
  -proto "google/rpc/error_details.proto" \
  -d "{\"trip_id\":\"$TRIP_ID\"}" \
  "$API_URL" moia.trip.state.v1.TripStateService/CancelTripAsCustomer
// replace with the ID of the customer who is canceling the trip
ctxWithCustomer = metadata.AppendToOutgoingContext(ctx, "customer-id", currCustomer)
_, err = tripClient.CancelTripAsCustomer(
	ctxWithCustomer,
	&tripv1.CancelTripAsCustomerRequest{
		TripId: tripId, // replace with tripId to cancel
	},
)
if err != nil {
	log.Fatalf("Could not cancel trip as customer %v", err)
}
log.Print("Successfully canceled trip as customer")
  const cancelAsCustomerResponse = await client.cancelTripAsCustomer(
    { tripId: orderResponse.tripId },
    {
      metadata: new grpc.Metadata({
        // replace `currCustomerId` with the ID of the customer who is cancelling the trip
        "customer-id": currCustomerId,
      }),
    },
  );
  console.log(cancelAsCustomerResponse); // {} empty response
def cancelTripAsCustomer(customerId: String, tripId: String): Task[Unit] = ZIO.scoped:
  for
    accessToken  <- tokenManager.getAccessToken()
    authMetadata  = GrpcClientService.createAuthMetadata(accessToken, Some(customerId))
    tripClient   <- ZioTripState.TripStateServiceClient.scoped(managedChannel, metadata = authMetadata)
    cancelRequest = CancelTripAsCustomerRequest(tripId = tripId)
    _            <- tripClient.cancelTripAsCustomer(cancelRequest)
    _            <- ZIO.logInfo(s"Successfully canceled trip: $tripId")
  yield ()

The response of type CancelTripAsCustomerResponse is an empty message which acknowledges the successful cancellation.

Cancelling a Trip as an Integrator#

Given an ongoing Trip, we can cancel it as long as the Customer hasn’t yet been picked up. This RPC should only be called for technical reasons, such as a payment failure. If the Customer wants to step back from the Trip, we should call the CancelTripAsCustomer RPC instead. To do so we need to call the CancelTripAsIntegrator RPC with a payload of type CancelTripAsIntegratorRequest. The payload contains the trip_id of the Trip we want to cancel.

# export TRIP_ID="<replace with trip id>"

grpcurl \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -import-path ./protos/ \
  -proto "moia/trip/state/v1/trip_state.proto" \
  -proto "google/rpc/error_details.proto" \
  -d "{\"trip_id\":\"$TRIP_ID\"}" \
  "$API_URL" moia.trip.state.v1.TripStateService/CancelTripAsIntegrator
_, err = tripClient.CancelTripAsIntegrator(
	ctx,
	&tripv1.CancelTripAsIntegratorRequest{
		TripId: tripId, // replace with tripId to cancel
	},
)
if err != nil {
	log.Fatalf("Could not cancel trip as integrator %v", err)
}
log.Print("Successfully canceled trip as integrator")
  const cancelAsIntegratorResponse = await client.cancelTripAsIntegrator({
    tripId: orderResponse.tripId,
  });
  console.log(cancelAsIntegratorResponse); // {} empty response
def cancelTripAsIntegrator(tripId: String): Task[Unit] = ZIO.scoped:
  for
    accessToken  <- tokenManager.getAccessToken()
    authMetadata  = GrpcClientService.createAuthMetadata(accessToken)
    tripClient   <- ZioTripState.TripStateServiceClient.scoped(managedChannel, metadata = authMetadata)
    cancelRequest = CancelTripAsIntegratorRequest(tripId = tripId)
    _            <- tripClient.cancelTripAsIntegrator(cancelRequest)
    _            <- ZIO.logInfo(s"Successfully canceled trip as integrator: $tripId")
  yield ()

The response of type CancelTripAsIntegratorResponse is an empty message which acknowledges the successful cancellation.