rocket

Intro

This document provides a general overview of the Rocket API, describing the overall process and usage to facilitate development and utilization of relevant endpoints for current and potential clients. The documentation aims to cover:

This general documentation does not include specific details about request and response fields, their types, or other technical specifications. For comprehensive information on request structures, response formats, possible error codes or used fields and their data types, refer to the relevant environment API schema. These schemas provide detailed technical information necessary for implementation and integration purposes.

Rocket API is written in GraphQL which allows dynamic fetching or relevant data per client need.

GraphQL is hosted on the following environments:

Each environment comes with Nitro web UI which nicely displays the GraphQL schema in the docs tab. Nitro web UI is hosted on /graphql, the graphql endpoint is hosted on /graphql.

Example of an allPlaces query with just a few fields fetched:

{
  allPlaces(purpose: ORIGIN)
  {
    name
    placeId
  }
}

GraphQL API operates under the same database as the old API so interoping with the old API for a simpler transition is possible.

Authentication

Authentication is done per-user basis with AWS Cognito. The API supports multiple apps per Cognito user pool. This can be used to have mobile and web for instance to connect to the same user pool with different authentication requirements.

To use Cognito the required SDK needs to be added to the project.

Documentation for integrating Cognito in the project can be access from https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-integrate-apps.html .

Example of SPA consuming cognito:

The cognito pool is created with only email as sign-in option and the default password policy. To create an application on the pool contact us. It is possible to create a public client, confidential client or custom. More can be seen in the cognito docs. Even if the auth is confidential, it would still be used for customer level authentication, not api to api.

For SPA, public auth should be used as not to reveal client secrets to the public.

To authenticate with the API, one must use the bearer authentication and the IdToken as the token (Cognito will return 3 tokens, Refresh for the refresh flow, Access not used by rocket and IdToken). In the GUI just select bearer as authentication and put the IdToken in the field. When doing a raw request set the authorization header as follows.

Authorization: Bearer <<id_token>>

Request ID

Rocket logs a request id with its log messages. This helps to match logs of the same request.

Rocket automatically generates a request id for the whole request. It is possible to supply the request id to rocket, so it can be correclated with the fronted.

To supply the request id add the following to the request headers

X-Request-ID: <UUID>

The id must be in UUID/GUID format, example:

X-Request-ID: b73fcad2-6b75-4a6c-a68f-cf8647facd69

If the request id is malformed, Rocket will create a new one internally. It is advisable to create a new ID for each call, as this makes logs easier to group.

Client identification

Multiple systems connect to the rocket api, as such we have multiple configurations that are loaded per request. The system resolves the correct configuration based on the origin or user credentials. The credentials take priority. If the user credentials match the origin does not matter.

The system has also segregated databases, so it is not possible to access data from a different configuration.

The origins that will be used must be reported to the rocket developers so that they can be added to the list or origins for origin resolution. When using Nitro the origin is set to the default one, to circumvent this, a person must be logged in with the cognito credentials.

Flow

Pre-Journey Search Requirements

To start searching for tickets the client first needs to know which railcards and places are available.

Get Railcards

We only support getting all railcards, as it is a rather light endpoint. Note, as it has no parameters the query railcard is invoked without parenthesis as it a field and not a method.

Example:

{
  railcards{
    code
    name
  }
}

Validate Railcards

Rocket API supports different railcard applications, depending on selected railcards and number of passengers:

Railcard Validation

Railcard validation is part of the standard journeySearch validation. Clients are not required to perform additional validation on their own. However, clients who prefer to validate applied railcards and passengers independently can do so by consuming the validateRailcards endpoint. This endpoint indicates whether the combination of a passenger set and applied railcard(s) is eligible/valid.

Railcard Limitations

Rocket supports a maximum of three different types of railcards per request. For example, multiple SRN, YNG, TST railcards can be applied and cover multiple or all passengers. Only one railcard can be applied per passenger, but it is not necessary that all passengers are covered by railcard(s).

Examples:

Adults Children Railcard #1 Railcard #1 Count Railcard #2 Railcard #2 Count Railcard #3 Railcard #3 Count Valid
1 0 YNG 1 / / / / True
0 1 YNG 1 / / / / False
1 0 YNG 2 / / / / False
2 0 YNG 2 / / / / True
2 0 YNG 1 SRN 1 / / True
2 0 YNG 1 2TR 1 / / False
2 1 YNG 1 2TR 1 / / False
3 0 YNG 1 2TR 1 / / True
3 1 YNG 1 2TR 1 / / True
4 3 YNG 1 2TR 1 / / True
4 3 YNG 2 2TR 2 / / False
6 3 YNG 2 2TR 2 / / True
3 0 YNG 1 SRN 1 TSU 1 True
3 1 YNG 1 2TR 1 TSU 1 False
4 3 YNG 2 SRN 1 TSU 1 True
4 3 YNG 2 SRN 2 TSU 2 False
6 3 YNG 2 2TR 2 TSU 1 False
6 3 YNG 1 2TR 2 TSU 1 True

Get Places/Stations

There are different endpoints for fetching a place or a list of them, based on the need.

Fetching a list of places

Fetching specific place

If client decides to cache places locally and implemented their own placeQuery method, then searchablePlaces should be used as not all places from allPlaces are valid for journeySearch. Please note that some places can change over time, so it is good to periodically call the endpoint and check. Daily cache refresh is advised.

NOTE: In order to simplify fetching a specific place we are planning (in the near future) to ‘merge’ following methods into single one: placeByCrs, placeByNlc, placeByUic, placeById.

Journey Planning

The Rocket API supports three different journey planning-related endpoints:

Initial Journey Search:

Subsequent Journey Searches:

Journey Details:

The journeySearch endpoint serves as the starting point for the journey planner functionality. It includes various validations and integrations with systems outside of Rocket.

Validation Process

The following validations are applied before fetching journey planner results:

All validation errors are aggregated and returned in the response at once.

Processing Journey Planner Results

  1. Journey Planner results are obtained based on search validated search query.

  2. Availability check is done against relevant journeys from the step 1

  3. Returning results:

    • first n* journeys (if any) per direction are taken from the available journeys (from step 2).
    • if there are no journeys and error is returned
    • if there are journeys, but an error occured on RARS/RARS Broker while checking availability, all services and fares with mandatory reservations are filtered out from result and a relevant error is raised (e.g. AVAILABILITY_ERROR).

* n represents the takeItems value set in the journeySearch request by the client. Default value is 5. If RARS Broker is used as a fare availability client, the value is limited to 5 (if bigger, it’ll be defaulted to 5).

Availability Handling

  1. Journey Set Preparation:
  1. Reservation Extraction criteria:
  1. Availability Check criteria:
  1. Error Handling:
  1. Response Flagging:

Returning JourneySearch Result

All journeys with available services and products are returned. If there are no journeys in the response, this could happen due to one or more of the following reasons:

Clients should always check for the error list in the response to handle the result correctly and inform customers that displayed results might be incomplete due to an error (e.g., AVAILABILITY_ERROR).

The journeySearchById endpoint returns journey planner results based on the original searchId. This allows customers to fetch a desired number of records (determined by client implementation) using pagination. Please note:

Similar to the journeySearch, this endpoint includes integration with the Availability system. However, no validation is needed on the Rocket side (except for the searchId value), and cached journey planner results are fetched (availability checks are always done on demand).

This endpoint typically offers better performance and is strongly recommended for journey search navigation/pagination.

JourneyDetails

The journeyDetails endpoint returns desired journey details from journey planner results based on the original searchId.

Background process and behavior are the same as for journeySearchById, with the addition of extracting only relevant journeys based on the provided journeyKeys list from the request.

Basket/Booking session handling

Rocket automatically expires trips based on specific conditions, which also determines when the entire booking expires.

  1. Trip Expiration: Individual trips expire and are automatically removed from the booking if:
  1. Booking Expiration: The entire booking expires and is automatically deleted when the last remaining trip in the booking expires (either by time or departure).

  2. Booking Expiry Time Calculation: The expiry time for the entire booking is determined by the earliest occurring of these two events across all trips currently in the booking:

  1. Web Client Behavior: If an expired trip was the last one in the booking (causing the entire booking to be deleted), web client must create a new, empty booking when the customer adds their next trip afterward.
flowchart TD
    Start([New Trip Added]) --> ClientType{Client Type}
    
    ClientType -->|Public| PublicTime[2-hour Window]
    ClientType -->|Corporate| CorpTime[24-hour Window]
    
    PublicTime --> CheckExpire{Expiration<br/>Check}
    CorpTime --> CheckExpire
    
    CheckExpire -->|Time Limit Reached| TimeExpire[Trip Expires]
    CheckExpire -->|Departure Passed| DepartExpire[Trip Expires]
    CheckExpire -->|Still Active| Active[Continue Monitoring]
    
    TimeExpire --> LastTrip{Last Trip<br/>in Booking?}
    DepartExpire --> LastTrip
    
    LastTrip -->|Yes| DeleteBooking[Delete Entire Booking]
    LastTrip -->|No| RemoveTrip[Remove Trip Only]
    
    RemoveTrip --> UpdateWindow[Update Booking Window]
    Active --> UpdateWindow
    
    UpdateWindow --> CheckExpire
    
    %% Styling
    classDef clientType fill:#f9f,stroke:#333,color:#000
    classDef decision fill:#bbf,stroke:#333,color:#000
    classDef timeWindow fill:#dfd,stroke:#333,color:#000
    classDef expiration fill:#fdd,stroke:#333,color:#000
    classDef activeState fill:#dfd,stroke:#333,color:#000
    classDef windowUpdate fill:#ddf,stroke:#333,color:#000
    
    class ClientType clientType
    class CheckExpire,LastTrip decision
    class PublicTime,CorpTime timeWindow
    class TimeExpire,DepartExpire,DeleteBooking expiration
    class Active activeState
    class UpdateWindow windowUpdate

Expiration Scenarios Explained

Scenario 1: Booking with Multiple Departures

Initial State:

Ideal: Customer confirms booking within 25 minutes.

Alternative Actions:

Scenario 2: Booking with Future Departure

Initial State:

Alternative Actions:


Booking Window Behavior

The booking system maintains a time-limited window (never longer than 24 hours) that advances only when trips expire based on their creation time, not departure time. Here’s how it works:

Key Principles

  1. Maximum Window Size
  1. Window Movement

Managing Reservations

When reservations are attached to trips:

Important Clarification

While bookings can remain active beyond 24 hours through continuous additions, each individual window never exceeds the time limit. This means:


Basket (trip) operations

When a customer adds tickets/fares for a specific journey to the basket, Rocket treats this as adding a trip to a booking. In Rocket terminology, a set of tickets/fares for a specific single/return journey is called a trip, where the basket is considered a booking.

If it’s a customer’s first trip, then a new booking is created by Rocket with the selected trip. From this point on, the client should base all actions on that booking until the booking is confirmed (sale was successful), deleted by the customer, or expired (and consequently deleted automatically by Rocket).

There are following Basket or Trip operations:

Customer operations

If the user wants to purchase tickets that will be tied to its account and view them, a customer needs to be created for them.

To create a customer, the user must be logged in via cognito. Before any other operations can be performed a customer needs to be created. This can be achieved by calling the CreateCustomer mutation. After this, separate fields can be updated and the whole customer with its cards can be queried.

If the customer wishes to purchase tickets without logging in, a customer does not need to be created, and it cannot be created. The flow is different from the APIv1 flow, where the customer is created in advanced. Here the customer details will just be passed on the anonymous checkout and the customer will be created behind the scenes.

Checkout flow

Once the user is happy with the booking, they can proced to purchase it.

To purchase(checkout) the user needs to call either guest checkout or checkout. The payment input object can take multiple different payment options. This is an input union type. Only one can be set. The payment information is detailed in the previous step. Before checking out, the booking will have a list of possible fulfilment types. This can be either eticket or tod. Some bookings will have restrictions that will allow only one type. The user must choose which fulfilment type they want. Etickets are digital, can be either printed by the user or saved on a digital device. ToDs can be collected on Ticket vending machines on stations. Not all stations have the machies and a credit/debit card is required to retrieve them. Whether a statis has a TVM can be found out via the the place endpoints.

If the user wants to get a confirmation email with the the tickets if eticket fulfilment is chosen or the transaction reference in case of a tod, they must select notify during checkout, or they can call notify separatelly later.

The checkout mutation returns the whole booking. If a guest user is doing a guest checkout, they will only be able to get the booking information from this endpoint once the booking is confirmed. It might be a good idea to query as much information as possible and display it to the customer in this step, as the cusotmer might have misstyped their email.

Other restrictions apply to guest customers, a booking can only be queried by a guest customer if it is not confirmed or has no user associated with it. Same applies for adding trips to a booking. A guest customer can only do it, if a booking has no user associated with it. If a user logs in after working on basked, the system will automatically claim the booking as the users on the next operation(GetBooking, AddTrip, AddReservation, DeleteTrip, Checkout).

If a booking was already paid in stripe, the GetStripeSession will return an error, “PAYMENT_ALREADY_PAID”, the frontend should in this case just call checkout. This can happen if some issues occur during Stripe confirmation and checking out.

Refunding

The system currently does not allow partial refunds, as such the only mutation is RequestRefund, which will request a refund for the whole booking.

Payment

We currently support only Stripe. On account payment is only usable with corporate clients that have set up the payment for the company. Mock can only be used on test and staging, it is stripped from the production binaries. It also must be added to the configuration to be able to use.

Mock

When paying with the mock, the mock payment must be set to true. Everything else will automatically work.

example:

mutation Checkout(
  $bookingId: String!
  $fulfilmentType: FulfilmentType!
  $ipAddress: String!
  $notify: Boolean!
) {
  checkout(
    input: {
      bookingId: $bookingId
      fulfilmentType: $fulfilmentType
      ipAddress: $ipAddress
      notify: $notify
      paymentInput: { mock: true }
    }
  ) {
    booking {
      id
    }
  }
}

Stripe

To start working with Stripe we need to provide you with the publishable key, which will be used to display the UI. Contact us to get the key.

You will also need to get the JavaScript library working https://docs.stripe.com/js . We use payment intents as the payment method, all details will be provided via the API.

In order to pay with Stripe a session must be initiated, to create a session call the createStripeSession as shown below.

mutation {
  createStripeSession(input: { bookingId: "XYZ" }) {
    stripePaymentSession {
      customerSecretSession
      sessionId
    }
  }
}

If any parameter of the booking changes the session will not be valid anymore and the backend will reject the payment. To avoid this, call createStripeSession each time a change happens.

If the booking did not change the createStripeSession will return cached data. It is safe to call as many times as needed to render the UI.

After the JS part is loaded and the payment has been accepted, you can call the checkout mutation specifying it is a Stripe payment.

mutation Checkout(
  $bookingId: String!
  $fulfilmentType: FulfilmentType!
  $ipAddress: String!
  $notify: Boolean!
) {
  checkout(
    input: {
      bookingId: $bookingId
      fulfilmentType: $fulfilmentType
      ipAddress: $ipAddress
      notify: $notify
      paymentInput: { stripe: true }
    }
  ) {
    booking {
      id
    }
  }
}

Stripe test cards can be found on https://docs.stripe.com/testing .

On Account

To use the on account user, the client must be a corporate client and the company must have the on account payment enabled.

To check if the user’s company has the on account payment enabled call the get company query.

Example:

{
  company {
    ... on Company {
      hasOnAccountPayment
    }
  }
}
{
  "data": {
    "company": {
      "hasOnAccountPayment": true
    }
  }
}

If the result is true, it is possible to pay with the on account payment method.

On account payment can have an additional fee, this can be seen when calling the organisation query.

{
  organisation {
    ... on Organisation {
      onAccountBookingFee
    }
  }
}
{
  "data": {
    "organisation": {
      "onAccountBookingFee": 3
    }
  }
}

If onAccountBookingFee has a value, a fee will be added to the payment. The fee will always be bigger than 0 and cannot be zero.

To pay with it, just execute the checkout mutation like the following example shows.

mutation Checkout(
  $bookingId: String!
  $fulfilmentType: FulfilmentType!
  $ipAddress: String!
  $notify: Boolean!
) {
  checkout(
    input: {
      bookingId: $bookingId
      fulfilmentType: $fulfilmentType
      ipAddress: $ipAddress
      notify: $notify
      paymentInput: { onAccount: true }
    }
  ) {
    booking {
      id
    }
  }
}

Corporate

Travel policies

Travel policies are restrictions on travel that can be enforced on a set of users based on their department or globaly on the company level if the department has no travel policies attached to it. When applying travel policies, we first check the department level, if the department has no travel policies, the company ones are checked, if no policies are defined on the company level, no additional restrictions apply. Company owner role is exempt of Travel policy restrictions, even if restrictions are assigned to the company.

The only restrictions that are always applied on corporate regardles of travel policies or Company Owner overrides are:

The list of all travel policies is available through the company object and ce be fetched via the company endpoints.

All rules in a travel policy are nullable(optional). When a rule is set, the rule will be populated. If the rule is disabled the rule will not be shown. So in short, if the rule is there it exist and it is valid.

Creating travel policies can be done with the ‘addTravelPolicy’ mutation, an example of this is

mutation {
  addTravelPolicy(
    input: {
      companyId: "c63a9754-e905-447e-8806-bebfd2d90603"
      name: "Travel Policy Staging 2"
      rules: {
        firstClassIfCheaperThanStandard: true
        fareCap: { priceCap: 100 }
      }
    }
  ) {
    company {
      name
      travelPolicies {
        id
        name
        rules {
          firstClassAdvanceCheaperThanStandard
          firstClassIfWeekend
          firstClassIfCheaperThanAnytime
          firstClassIfCheaperThanStandard
          firstClassIfLongerJourney {
            journeyDuration
          }
          firstClassIfWeekDaysBeforeAfter {
            afterTime
            beforeTime
          }
          firstIfPricierPercentage {
            percentage
          }
          fareCap {
            priceCap
          }
        }
      }
      costCenterId
      travelPolicyId
    }
    errors {
      __typename
    }
  }
}

More complex rules are set by setting their field, simple one are set, by setting their boolean flag to true. This is however true only for adding the travel policy, updating is different.

Updating travel policies is done with updateTravelPolicy mutation. The mutation input is an input union type or as in the HotChocollate terminology OneOf. The OneOf allows for only one field to be populated at once, the same applies to the rules field, which allows to set only one rule.

The rules are a collection of commands to execute on the travel policy. One such example would be

setFareCap:{
   priceCap: 200
}

And the inverse

disableFareCap: true

Due to some limitation of the GraphQL standard, a type cannot be declared without a scalar. Because of this simple travel policies need to be enabled with a boolean flag and disabling of policies is done with a boolean flag. Thus, not defining any value(null), no changes are applied to the rule, if a field is set, the command will be issued.

For convenience enabling and updating a rule is merged into a set command, which can be executed for updatading and inserting, thus simplifying the process.

An example of a full request would be.

mutation {
  updateTravelPolicy(
    companyId: "c63a9754-e905-447e-8806-bebfd2d90603"
    id: "7e711792-5dde-43c9-9fe3-6c2119a78953"
    input: {
      rules: {
        firstClassAdvanceCheaperThanStandard: true
    }}
  ) {
    company {
      name
      costCenterId
      travelPolicyId
    }
    errors {
      __typename
    }
  }
}

While we allow only one field to be updated at once, it is possible to batch multiple mutations in one request. This allows the client to either update all fields at once for example with a save changes button or doing it interactively when the user enables/disables a rule.

Deleting a rule can be done by simply calling deleteTravelPolicy An example would be:

mutation {
  deleteTravelPolicy(
    input: {
      companyId: "c63a9754-e905-447e-8806-bebfd2d90603"
      id: "a3bfe93c-69b0-44a7-a2dd-22894b2561a1"
    }
  ) {
    company {
      name
      costCenterId
      travelPolicyId
    }
    errors {
      __typename
    }
  }
}

Users

Users are structured into three groups:

Super admins are the root of the system, they are not part of any entity. They extend org admin permissions by allowing to create and delete organisations and manipulated other super admins.

Org admins are administrator of a giving organisation. They have permissions to manipulate the organisation, to invite/edit/delete other org admins, create/edit/delete companies and invite company owners, which is a role withing corporate users.

Corporate users are part of a different set of permissions than the previously mentioned users. They are always part of a company. Sometimes they can be part of a department. Their role reflects what they can do on the level of company/department/user. All corporate users can book travels, some can book for others and sometimes they can have limitations set upon travel.

Inviting users

A user can invite another user, if the user has permission to invite and if the user has permission on the specific resource.

A user can be invited if it is not already part of any entity or a superadmin on the system. If the user already has a cognito login, the user will just be added. The user will not receive an invite from cognito as it is already there.

If the corporate user has previously been removed, the customer data is still in there and it will be relinked to the user, (bookings etc).

The invite mutations for org admins and corporate users have some parameters set as optional. When a user is inviting another user, and data can be inherited from the context, that data is then inherited and used in the context. An example would be an org admin inviting a company owner. OrganisationId does not need to be supplied as it is inherited from the org admin. The user can provide those parameters, but they must match, for example, a company owner cannot provide a company id that does not match the one he is assigned to. In such cases an error is returned.

Fetching users

To get own user, you must call the user endpoint, this will also give the information of what type of user the logged in user is. This can help to decide how to display the UI.

When querying the system for corporate users via the corporateUsers query, the parameters are implicitly inherited the same way they are in he invite flow. The user can still restrict the search more, but it cannot search outside its scope.

Due to super admin and org admins are being able to invite a company owner, they are able to call the corporateUsers endpoint, but they will only see company owners there.

User management

Updating a corporate user can be done via the updateCorporateUser mutation. A user can update their own user or if permissions allow other users. A super admin or organisation admin can only update a user with the role of Corporate Owner. To update the user not only the role must be right, but also it must belong to the same company/department.

Deleting a user requires permission to delete on that level and require to match the user parameters. It is not possible to delete a lower role from another company. Only from the assigned company. A user also cannot delete itself. Deleting a user does not currently delete the cognito login as it makes it easier to test, it can be implemented later on if needed.

Email update

To update an email the user must call the update method for their type SuperAdmin, OrgAdmin or CorporateUser. Setting the email address field with the new email will prompt the user to get and email with a code to their email. The user must then call the udpate method setting the field email address cahnge confirm code with the code received via email. If everything checks out it will change the user email.

The code is valid for one hour. It is not possible to change to an email tha already exists in the system.

Company management

Moving companies

A company can be moved only by super admins. A company can be move to another orgnisation of that organiation exists. MoveCompany mutation move the company along with all the users and updates the bookings to show the current organisation.

Public signup

It is possible for a user that does not exist in the system yet to create its own company and be the company owner of that company. The company will be assigned to the default organisation.

The frontend must register the user via cognito, the user must not be added as a corporate user to another company. Once the user is created, the user must call the publicSignup mutation with all the required information to create the user and the company.