Preiserhöhung Deutschlandticket - Price Change of Deutschlandticket


0. Introduction: Price Change and New Requirements

Due to the upcoming price adjustment for the Deutschlandticket starting from 01.01.2025, the price of the Deutschlandticket will change to 58,00€ per month. This has been decided by the federal and state transport ministers.

Therefor a new product variant needs to be introduced into the mobilitybox. The current Deutschlandticket product variants will no longer be supported to ensure that users use the new product.

It will be a requirement to collect the first and last name, the birth date and the postal code of the passenger.

There are several changes in the Mobilitybox API helping to support this price change and the switch from the old products to the new one. To ensure smooth transitions for the end-user consider the following adjustments.


1. What happens?

  • new Deutschlandticket products will be introduced early November
    can be bought from 01.11.2024 and activated from 01.01.2025
  • old Deutschlandticket products can be bought and activated until 31.12.2024
  • active subscriptions will automatically convert to the new product for the january cycle and can be reactivated normaly
    (check out 2. to see how migrate data for subscriptions where not all necessary data was collected before)


2. Data Migration for Existing Products and Users

Existing users who are subscribed to some of the old Deutschlandticket products variants will not have all the necessary data for the new product version.

What is the issue?

From 01.01.2025 the following user data is required:
- first_name
- last_name
- birth_date
- german_postal_code

If that data is not collected for an active subscription, it is not possible to reactivate the coupon to receive the January Ticket. Therefor a data migration is needed.

Products which are affected:
- mobilitybox-product-b59e8476-6ba0-4260-91fd-9e456341065d - birth_date and german_postal_code is missing
- mobilitybox-product-cbe3ec49-b9ad-42f8-b694-8a41e3b8e9f8 - german_postal_code is missing

The product mobilitybox-product-358abaeb-81d7-4d65-9d8a-420ee739c066 will automatically transfer, without anything to do on your side.

Please checkout your active subscriptions if there are affected product variants or not.

There are different options to pass the missing data to the Mobilitybox:

Option 1 - Restore

The first option is to restore the coupon and activate the new restored coupon with necessary data.

Attention: This only works if the activation of the restored coupon happens after 01.01.2025

Option 2 - Pass Postal Code before Reactivation

To pass the missing german_postal_code you can use the POST /ticketing/coupons/{id}/tariff_settings.json endpoint (API Docs).

Option 3 - Pass Data on Reactivation

It is possible to pass the identification_medium and tariff_settings additionally to the reactivation_key on the reactivation. If the already collected data misses something it will pick it from the passed data.


3. API Changes: Introduction of API Version v7

To support the price update, API version v7 will be introduced. This new version will offer improved functionality, including the ability to track which product is booked for each subscription cycle. Key changes include:

  • product_id for each Subscription Cycle:

    • The new API introduces a field to differentiate between old and new product variants within a user's subscription cycle.
  • earliest_activation_start_datetime for a Product:

    • Defines what is the earliest possible validity start time for a ticket. (earliest activation_start_datetime passed in coupon activation or earliest time when to activate the coupon without passing activation_start_datetime)
    • Example: The new Deutschlandticket product can already be bought in November/December, but it can only be activated from January
  • earliest_activation_start_datetime for a Coupon: (like product)

    • Defines what is the earliest possible validity start time for a ticket. (earliest activation_start_datetime passed in coupon activation or earliest time when to activate the coupon without passing activation_start_datetime)
    • Example: The new Deutschlandticket product can already be bought in November/December, but it can only be activated from January
  • latest_activation_start_datetime for a Coupon:

    • Defines what is the latest possible validity start time for a ticket.
    • The old Deutschlandticket product can be bought until 31.12.2024 and also can only be activated until then. Attention: Tickets activated for the 31.12.2024 will only be valid for 1 day and then has to be reordered again.
  • product for a Coupon:

    • On subscriptions: the product shown in the coupon data is set to the product of the current subscription cycle
    • Example: for a already active subscription, in december the product will be the old product, from 1st January the product attribute will contain the data of the new product
  • Backward Compatibility:

    • The existing API versions (v6 and below) will remain active and also support the new products. But the new attributes will only be available in API v7, so consider to upgrade.


Fastlink will update the products automatically. There will be the new product in checkout from 01.01.2025 and in December the customers will see a notice stating out, that there will be a new product and they are able to buy the new product if they want to use it in January.

If you want to skip this notice, you can add a new flag to the URL. For more details, see Fastlink Implementation

There will be a notice on the subscription manage page which shows the price change.

5. Mobility Wallet

The Mobility Wallet will implement and use the changes from the SDK updates. Users using the Mobility Wallet will receive an alert when the reactivation of their ticket failed and given the option to enter the missing options by themself.

If you are using the Mobility Wallet for your customers make sure noticing them to update the App before 01.01.2025.


6. SDK Changes for iOS and Android

To accommodate the product changes, the native SDKs for both iOS and Android will receive updates. Below are the changes, along with implementation examples for each platform.

iOS SDK Changes:

  • Update SDK Version: The iOS SDK must be updated to version 7.0.0 or higher to support the features.
  • Identify Reactivation Issue: there will be two new MobilityboxError types:
    • MobilityboxError.identification_medium_not_valid
    • MobilityboxError.tariff_settings_not_valid
  • New Data Attributes:
    • MobilityboxCoupon added: public var earliest_activation_start_datetime: String?
    • MobilityboxCoupon added: public var latest_activation_start_datetime: String?
    • MobilityboxSubscriptionCycle added: public var product_id: String?
  • New MobilityboxProductCode: to fetch a MobilityboxProduct
  • MobilityboxIdentificationView used for Reactivation: you can pass ticket: MobilityboxTicket? to the identification view. The identification view will then use the tickets coupon_reactivation_key and the entered user data to reactivate the ticket. (Like Option 3 in 2.)

Example Implementations:

  • check reactivation error codes
import Mobilitybox

func reactivateTicket(ticket: MobilityboxTicket) {
    ticket.reactivate(onSuccess: { reactivatedTicketCode in
        // implement your normal reactivation success
    }) { error in
        if error == .identification_medium_not_valid || error == .tariff_settings_not_valid {
            // ==> her implement function to open popup or some kind of notification for the user stating out that it was not possible to reactivate the ticket and the user has to enter more information
        }
    }
}
  • ticket reactivation failed alert example (open identification view with passed ticket):
import SwiftUI
import Mobilitybox

struct TicketReactivationFailedAlertView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @State var ticket: MobilityboxTicket
    @State var coupon: MobilityboxCoupon? = nil
    @State var showIdentificationView = false


    func reactivateTicketCallback(coupon: MobilityboxCoupon, ticketCode: MobilityboxTicketCode) {
        // implement whatever your app does when you receive the reactivated ticket code
        // example:
        //   - fetch ticket from ticket code
        //   - add ticket to list
        //   - close alert
    }

    var body: some View {
        VStack(alignment: .leading) {
            // add some texts with description thats not possible to reactivate the coupon
            if (coupon != nil) { // if coupon ready, show button
                MobilityboxNavigationLink(linkType: .modal, showDestinationView: $showIdentificationView) {
                    Button("Fehlende Daten hinterlegen", systemImage: "rectangle.and.pencil.and.ellipsis") {
                        self.showIdentificationView = true
                    }.buttonStyle(.borderedProminent)
                } navigationDestination: {
                    VStack {
                        MobilityboxIdentificationView(
                        coupon: Binding($coupon)!,
                        activateCouponCallback: reactivateTicketCallback,
                        ticket: $ticket
                        ).navigationBarTitleDisplayMode(.inline)
                    }
                }
            } else {
                ProgressView()
            }
            Spacer()
        }
        .padding(.horizontal, 20)
        .onAppear {
            // fetch Coupon if not already available in your app (needed for the Identification View)
            MobilityboxCouponCode(couponId: self.ticket.coupon_id).fetchCoupon { coupon in
                self.coupon = coupon
            } onFailure: { error in
                // close view
            }
        }
    }
}

Android SDK Changes:

  • Update SDK Version: The Android SDK must be updated to version 7.0.0 or higher to support the features.
  • Identify Reactivation Issue: there will be two new MobilityboxError types:
    • MobilityboxError.IDENTIFICATION_MEDIUM_NOT_VALID
    • MobilityboxError.TARIFF_SETTINGS_NOT_VALID
  • New Data Attributes:
    • MobilityboxCoupon added: var earliest_activation_start_datetime: String?
    • MobilityboxCoupon added: var latest_activation_start_datetime: String?
    • MobilityboxSubscriptionCycle added: val product_id: String?
  • New MobilityboxProductCode: to fetch a MobilityboxProduct
  • MobilityboxIdentificationFragment used for Reactivation: you can pass a MobilityboxTicket to the identification fragment. You can also use MobilityboxBottomSheetFragment and pass a coupon and ticket to open a BottomSheet with the Identification View which reactivates the ticket. The identification view will then use the tickets coupon_reactivation_key and the entered user data to reactivate the ticket. (Like Option 3 in 2.)

Example Implementations:

  • check reactivation error codes
import com.vesputi.mobilitybox_ticketing_android.*

fun reactivateTicket(ticket: MobilityboxTicket) {
    ticket.reactivate({ reactivatedTicketCode ->
        // implement your normal reactivation success
    }) { error ->
        if (error == MobilityboxError.IDENTIFICATION_MEDIUM_NOT_VALID || error == MobilityboxError.TARIFF_SETTINGS_NOT_VALID) {
            // ==> her implement function to open popup or some kind of notification for the user stating out that it was not possible to reactivate the ticket and the user has to enter more information
        }
    }
}
  • ticket reactivation failed alert example (open identification view with passed ticket):
import com.vesputi.mobilitybox_ticketing_android.*

class TicketReactivationFailedAlertBottomSheet : BottomSheetDialogFragment() {
    var ticket: MobilityboxTicket? = null
    private var coupon: MobilityboxCoupon? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            ticket = it.get("ticket") as MobilityboxTicket?
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(
            R.layout.fragment_ticket_reactivation_failed_alert_bottom_sheet,
            container,
            false
        )
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        initTexts(view)
        initReactivationButton(view)

        val closeSheetButton = view.findViewById<ImageView>(R.id.closeSheetButton)
        closeSheetButton.setOnClickListener {
            activity?.supportFragmentManager?.setFragmentResult("closeReactivationFailedAlert", bundleOf())
            dismiss()
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        activity?.supportFragmentManager?.setFragmentResult("closeReactivationFailedAlert", bundleOf())
    }

    private fun initTexts(view: View) {
        // add some texts with description thats not possible to reactivate the coupon
    }

    private fun initReactivationButton(view: View) {
        val ticketReactivationAlertContentContainer = view.findViewById<View>(R.id.ticket_reactivation_alert_content)
        if (this.ticket != null) {
            fetchCoupon(view)
            ticketReactivationAlertContentContainer.visibility =  View.VISIBLE

            val reactivateTicketButton = ticketReactivationAlertContentContainer.findViewById<RelativeLayout>(R.id.reactivateTicketButton)
            // edit Button

            this.activity?.runOnUiThread(updateReactivateButtonViewRunnable)

            reactivateTicketButton?.setOnClickListener {
                openIdentificationViewBottomSheet()
            }
        } else {
            dismiss()
        }
    }

    private var updateReactivateButtonViewRunnable = Runnable {
        val reactivateTicketButton = this.view?.findViewById<RelativeLayout>(R.id.reactivateTicketButton)
        val loadingReactivateButton = this.view?.findViewById<ProgressBar>(R.id.loadingReactivateButton)

        if (this.coupon == null) {
            reactivateTicketButton?.visibility = View.GONE
            loadingReactivateButton?.visibility = View.VISIBLE
        } else {
            reactivateTicketButton?.visibility = View.VISIBLE
            loadingReactivateButton?.visibility = View.GONE
        }
    }

    private fun fetchCoupon(view: View) {
        if (this.ticket?.coupon_id != null && this.coupon == null) {
            val couponCode = MobilityboxCouponCode(this.ticketElement!!.ticket!!.coupon_id)
            couponCode.fetchCoupon({
                this.coupon = it
                this.activity?.runOnUiThread(updateReactivateButtonViewRunnable)
            }) {
            // handle error
            }
        }
    }

    private fun openIdentificationViewBottomSheet() {
        var identificationViewBottomSheet = MobilityboxBottomSheetFragment.newInstance(this.coupon!!, this.ticketElement!!.ticket!!, this.ticket!!.id!!)
        identificationViewBottomSheet.show(childFragmentManager, "Identification View")
    }

    companion object {
        @JvmStatic
        fun newInstance(ticket: MobilityboxTicket) =
            TicketReactivationFailedAlertBottomSheet().apply {
                arguments = Bundle().apply {
                    putParcelable("ticket", ticket)
                }
            }
    }
}


7. Summary of Changes

  • New Product: A new Deutschlandticket product introduced with the updated pricing.
  • Data Migration: Users on some of the old product variants will need to have their data migrated
  • API Changes: A new API version (v7) is introduced, with endpoints that support the new product structure.
  • SDK Updates: Both iOS and Android SDKs received some updates to handle API v7 changes.