Guides

Table of Contents

  1. Complete your first sale
    1. Get trip pattern
    2. Get offer
    3. Create order
    4. Update order
    5. Reserve offer
    6. Create payment
    7. Create a payment transaction
    8. Create terminal
    9. Capture transaction
    10. Generate and distribute ticket distributions
      1. Generate and send PDF-tickets
    11. Custom Entur properties in NOD
  2. Searching for Orders
  3. Refunding
    1. Get refund options
    2. Refund order line
  4. Manual refunding
  5. Cancellation
  6. Concurrent order changes
    1. Updating a locked order
    2. Order Locking Overview
  7. Period ticket
  8. Zero ticket
  9. Payment methods
    1. Pay with Vipps
    2. Pay with PayPal
    3. Pay with gift card
    4. Pay with invoice
    5. Imported payments
    6. Fees
  10. Accessing the external Kafka Cluster
    1. Prerequisites
    2. Quickstart
    3. Security
    4. Testing Client Configuration
    5. Topic Descriptions
    6. Naming conventions
    7. Topics
    8. Schema Registry
  11. Add Recurring Payment
    1. Create Recurring Payment
    2. Create Terminal
    3. Authorize Recurring Terminal
  12. Usage of Recurring Payments
    1. Get recurring payments for customer
  13. Strong Customer Authentication Exemption
  14. Error handling
    1. Payment

Complete your first sale with the sales API

(back to table of contents)

This guide shows how to create an order, populate it with an offer, pay for the order, and distribute tickets. Each code step contains a request body (if needed) and the response. If the request body is missing then the request can or should be executed without a body. It is recommended that beginners of the API try this example first.

Get trip pattern

(back to table of contents)

Journey Planner API is used to perform a travel search and get a trip pattern representing the trip.

Get offer

(back to table of contents)

POST /offers/v2/search/trip-pattern
See the API reference on the Swagger Petstore

Get an offer for a trip by constructing leg(s) representation as input to the Offer Search API, from the trip pattern response from the Journey Planner API.

  • travelDate is the operatingDay of the DatedServiceJourney representing the specific trip
  • fromStopPlaceId is the id of the StopPlace where the leg starts
  • toStopPlaceId is the id of the StopPlace where the leg ends
  • serviceJourneyId is the id of the ServiceJourney that in combination with travelDate represents the specific trip

travellers represent who are performing the trip. In this example, we have one adult.

Create Order

(back to table of contents)

POST /sales/v1/orders

Create an order to which you want to add offers to. This is done by sending a post request with an empty body. An order will reset after 20 minutes if no further changes are made, or if it is not confirmed.

Update Order

(back to table of contents)

PUT /sales/v1/orders

This endpoint updates an existing order with the provided details. Additionally, when the order is updated, the reset time is automatically extended by 20 minutes from the time of the update.

Reserve offer

(back to table of contents)

POST /sales/v1/reserve-offers

Reserve the chosen offer and add it to the specified order

Create Payment

(back to table of contents)

Create a payment to the order.

POST /sales/v1/payments

It is also possible to add a transaction to the payment in this call, by adding a transaction request to the payment request.

Create a Payment Transaction

(back to table of contents)

Create a payment transaction to the specified payment

POST /sales/v1/payments/{paymentId}/transactions

Create Terminal

(back to table of contents)

POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/terminal

Create a terminal to execute the transaction. Use the paymentId and the transactionId from the previous requests. It is possible to avoid the capture call by setting autoSale: true in the terminal request. To be able to use the autoSale feature, callback must be activated for your merchant. You can request that Entur enables callback for your merchant. The default value for autoSale is false.

Capture Transaction

(back to table of contents)

PUT /sales/v1/payments/{paymentId}/transactions/{transactionId}/capture

When the transaction is executed successfully we need to mark the transaction as completed. Use the paymentId and transactionId from the previous requests.

Generate and Distribute ticket distributions

(back to table of contents)

Ticket Distributions are automatically generated based on order events, and a request to this endpoint can be made to distribute them to NOD. Use the orderId and orderVersion from the previous requests. orderVersion is required and has to be the newest version of the order, otherwise, it will fail.

It is possible to specify a deliverAfter in order to specify the earliest time it should be possible to pick up the ticket. This is useful if you for example are making a PDF ticket so you need to pick up the QR code immediately.

You can also specify a new start date for a specific order line. This is only allowed for certain products (with the usage trigger SPECIFIED_START_DATE), and it will return Bad Request for other products. If the tickets already were distributed it will cancel the existing ones and create and distribute new ticket distributions with the new start date. Bad Request is returned if the tickets already were picked up from NOD.

Any updates to the order – order lines added or removed, or supplement products added or removed to an order line – after the ticket has been distributed, will cause changes in the ticket distributions for that order. Because of this, the client should periodically verify using this endpoint that the tickets displayed to the user remain in the same order line version and that its status is still active. The field distributionStatus in the ticket distribution should remain in DISTRIBUTED status as long as the ticket is valid. If the order line version has been updated, a new ticket needs to be acquired from NOD to get the updated QR code. Tickets with distributionStatus set to CANCELLED is no longer valid, and should be removed.

POST /sales/v1/ticket-distribution-groups/distribute

PDF-Tickets

POST /sales/v1/pdf-tickets/{orderId}/send

As with regular tickets, Ticket Distributions for PDF-tickets are automatically generated based on order events and a request to this endpoint will produce a PDF-representation of the ticket and send it to a provided email address.

A PdfTicketRequest needs to specify travellers for all the generated TicketDistributions that exist for an order and the email address where the tickets are to be sent. Optionally one can provide the desired language one would like the ticket to be created in. Supported languages are for now Norwegian (nob , default) and English (eng).

Custom Entur properties in NOD

(back to table of contents)

When tickets are distributed to NOD, a set of custom properties are used as described in the table below. Note that the properties used are dependent on the type of ticket distributed. If the ticket is a zone ticket, from/to tariff zone name, the number of zones, and validity in all zones flag will be used. If the ticket is not a zone ticket, to/from place name and departure/arrival time is used.

Also note entur_combinedTicket which is set to true for through fare tickets where the price is not given by individual tickets, but rather the price of all the parts of the combined ticket.

See the NOD (National Order Database) section for more information.

KeyTypeComment
entur_ticketDistributionIdstringReference to ticket distribution.
entur_orderIdstringReference to order.
entur_purchaseDatedateTimePurchased date of ticket.
entur_amountstringAmount paid for this ticket.
entur_taxAmountstringTax amount paid for this ticket.
entur_authoritystringReference to authority.
entur_reservationsjsonJSON Node detailing reservation and seats for each leg of the journey.
entur_supplementsstringString with names of each supplement product on this journey.
entur_taxRatestringTax rate used to calculate the tax amount for this ticket.
entur_fromTariffZoneNamestringFrom tariff zone name for this ticket. Only if a zone ticket.
entur_toTariffZoneNamestringTo tariff zone name for this ticket. Only if a zone ticket.
entur_numberOfZonesintNumber of tariff zones ticket is valid for.
entur_fromPlaceNamestringFrom stop place name this ticket. Only if journey between stop place.
entur_departureTimedateTimeDeparture time for this journey. Only if journey between stop place.
entur_toPlaceNamestringTo stop place name this ticket. Only if journey between stop place.
entur_arrivalTimedateTimeArrival time for this journey. Only if journey between stop place.
entur_fareProductNamestringName of fare product bought
entur_userProfileNamestringName of the user profile which was used for this ticket
entur_groupNamestringName of group ticket.
entur_groupDescriptionstringDescription of group ticket.
entur_ticketTypestringWhat type of ticket this is. Possible values are SINGLE and PERIODIC
entur_validAllZonesbooleanIf ticket should be valid in all zones in system, only in special case
entur_combinedTicketbooleanIf the ticket is part of 2 or more tickets that do not have individual prices
entur_combinedTicketNamestringIf combinedTicket=true, the name for the package is shown here

Searching for Orders

(back to table of contents)

GET /sales/v1/orders?

Searching for orders can be accomplished by using multiple filter queries to narrow down a search. Complex filters can be created by using multiple query parameters, even by name. All query parameters are joined together using the operator AND.

In the example below a complex query is created by using multiple query parameters to find all orders in status DRAFT, version 2 which has a total amount greater or equal to 100 and lower than 200.

GET /sales/v1/orders?status=DRAFT&version=gt:1
    &totalAmount=gte:100.00&totalAmount=lt:200.00

Refunding

(back to table of contents)

By performing a refund of an order, the affected order lines and reservations will be cancelled along with the ticket. The balance after performing the refund, according to the refund rules, will be credited back to the customer.

To refund an order, either partially or in its entirety, the following conditions must be met:

  • The latest version of the order is CONFIRMED. This should be the case after completing the sales steps leading to and including capturing transaction.
  • The affected order lines are CONFIRMED and not CANCELLED
  • The affected order lines have active refund options

To cancel an order item that is not refundable, you must manually cancel and balance the order yourself. See cancellation guide.

Get refund options

(back to table of contents)

GET /sales/v1/refunds/options/{orderId}

Get all refund options that are active now for a given order, with optional orderLineIds query parameter. Omitting the orderLineIds query param will return all refund options for all order lines on the latest version of the order. Refund options, at the time of writing, will only give you a maximum of one RefundOption per order line. This may be expanded in the future so one could refund parts of an order line. This corresponds to the fact that the refundOperation-field in PerformRefundEntry is a list.

The fee must be subtracted from refundAmount to get the final amount available for refund. A negative amount can not be refunded.

GET /sales/v1/refunds/options/{orderId}?orderlineIds=ec943c0e-04a2-4585-a7b4-4f76d9bfa8e2,fb8b7a0d-390c-41da-9034-0541599db970

Refund order line

(back to table of contents)

PUT /sales/v1/refunds/{orderId}

To refund one or more order lines, the request body must contain a list of refund entries with reference to the order line, the refund option, and the refund rule that should be used. These fields are available by fetching refund options for the order.

To refund the entire order, a refundOption per order line must be selected. There is no shortcut to this because different products have different rules for whether a fee must be added to the order.

PUT /sales/v1/refunds/{orderId}

Manual Refunding

(back to table of contents)

It is not always possible to use the Refund-API to refund an order. For example when an order consists of products from a third party source. In these cases, it is possible to perform a manual refund process.

When performing a manual refund, the tickets (ticket-distributions) will automatically be cancelled.

To perform a manual refund, the following steps must be taken:

  1. Cancel the order lines that should be refunded, and add a fee if there should be a cancellation fee. See section 1. and 2. under cancellation

  2. Create a credit with amount equal to remaining amount to be credited (after fees have been added):

    POST /v1/credits

  3. Execute the credit:

    POST /v1/credits/{creditId}/execute

Cancellation

(back to table of contents)

This guide shows you how to cancel order lines. This can be used, for example, when an order line has non-refundable products.

  1. Manually cancel desired order lines on the order:

    POST /v1/orders/{orderId}/orderlines/{orderLineId}/cancel

  2. Add a fee corresponding to the price of the order lines cancelled, so that the balance equals zero. Always set type as MANUAL (see example request):

    POST /v1/orders/{orderId}/fees

    Let's say we want to cancel an order with two order lines. If the first costs 199 NOK and the second costs 200 NOK, then the price of the fee must be as shown below 399 NOK.

  3. Confirm the order

    POST /v1/orders/{orderId}/confirm

Concurrent order changes

(back to table of contents)

In general, there are no mechanisms to prevent concurrent changes to an order, even by different clients. However, a locking mechanism is in place to prevent order changes while processing a payment or credit. Locking an order does not mean the payment is locked; i.e., you can still edit a payment while an order is locked. While an order is locked, some changes to the order cannot be made until it is unlocked.

Unsuccessful attempts to make changes to a locked order will return 423 LOCKED.

An order will automatically unlock after 3 minutes.

Updating a locked order

(back to table of contents)

To modify a locked order (i.e., where a payment or credit have been created) you must deactivate the cause of the lock. While the payment/credit is not active, you can edit an order as you please. After the order has been satisfactorily changed, you must reactivate the payment/credit by setting it to active and updating it with the new total amount of the order.

  1. Deactivate the cause of the lock, either/or

    1. Payment by POST-ing to /v1/payments/{paymentId}/deactivate with an empty body
    2. Credit by POST-ing to /v1/credits/{creditId}/deactivate with an empty body
  2. Wait for the order to be unlocked

    1. For Entur applications: wait for a OrderUnlocked kafka event with the correct orderId
    2. For external applications: it shouldn't take tooo long :)
  3. Edit your order as normal

  4. Reactivate the payment with the new total amount of the order and active set to true

    PATCH /sales/v1/payments/{paymentId}


    If the new totalAmount sent does not match the balance of the order a 400 Bad Request is returned.

Order Locking Overview

(back to table of contents)

Events

The following kafka events relate to order locking

ActionOrder stateSource
Create paymentLockedPayment
Create creditLockedPayment
Payment activatedLockedPayment
Credit activatedLockedPayment
Order confirmed after paymentUnlockedPayment
Order confirmed after creditUnlockedPayment
Payment deactivatedUnlockedPayment
Credit deactivatedUnlockedPayment
Order LockedLockedOrder
Order UnlockedUnlockedOrder
Order ResetUnlockedOrder
Payment added to orderUnlockedOrder
Credit added to orderUnlockedOrder
TimeoutUnlockedSPM

Operations

How the following changes to an order behaves in relation to order locking.

OperationAllowed while locked
Add credit to orderYes
Add payment to orderYes
Add reservationsYes
Add traveller to orderYes
Confirm orderYes
Remove reservationYes
Remove traveller from orderYes
Reset orderYes
Update orderYes
Update reservationYes
Update travellerYes
Add feeNo
Add order linesNo
Cancel order linesNo
Import orderNo
Remove feeNo
Remove order lineNo
Replace order linesNo
Undo replace order linesNo
Update order lineNo

Period Ticket

(back to table of contents)

The sales process for period tickets is mostly the same as the sales process for regular single tickets, differing in how offers are requested and optional start time. For more information about how to construct the request to get offers, consult the Offers guides.

Zero Ticket

(back to table of contents)

A zero ticket is an order with totalAmount equal to zero. This can be the case for personal tickets or alternative transport where the operator has to create a new ticket if the original journey did not take place. When creating a zero ticket;

  • You do not need to create a payment
  • You have to manually confirm the order: POST /sales/v1/orders{orderId}/confirm

Get Offer

(back to table of contents)

POST /offers/v2/search/zones
See the API reference on the Swagger Petstore

This example requests an offer for an adult seven days ticket over two zones. Here the request contains recommendationConfig with typesOfRecommendation set to CHEAPEST, durationTypes set to WEEKLY_PASS and onlyIncludeRecommendedOffers set to true. This combination will return the cheapest weekly pass offer, as the request would otherwise return all available offers between the two zones for an adult (eg. Single ticket, 7 days pass etc.).

The Offers API supports other ways to obtain period tickets, such as between specific stop places or for a specific authority. For more information, check the Offers guides.

Reserve offer and handle payment

(back to table of contents)

The next steps to complete a sale for period tickets are the same as for an offer with a trip, from creating an order to capturing transaction.

Distribute ticket

(back to table of contents)

When distributing period tickets it is (for some of the products) possible to provide a specific start time. As described in the Distribute ticket distribution section, the usage trigger of the products determines if this is possible.

startOfValidity can be specified for every order line/ticket and sets when the ticket should be valid from. Note that deliverAfter can not be set to after startOfValidity.

POST /sales/v1/ticket-distribution-groups/distribute

Send Receipt

(back to table of contents)

Zero ticket receipt
When sending receipt for zero tickets you always have to do a manual POST to /v1/receipts/send with header receiptType set to SALE and turn OFF automatic sending of receipt.

Automatic sending of receipt does not work for zero tickets because the app will, due to missing payment and credit, believe that it is a receipt of type CHANGE. Because of this you always have to do a manual POST to /v1/receipts/send with header receiptType set to SALE when sending receipt for zero tickets and make sure that automatic sending of receipt is turned OFF for the given combination of orgId, dci and pos.

Payment Methods

(back to table of contents)

In order to be able to complete payments using the Sales API, each clientId must have configured which Payment Methods they should have access to.

Each Payment Type must be configured for the client by Entur. To get a list of all payment methods enabled for the client, use: GET /sales/v1/payment-methods. Please contact your Partner Assistant if you need any Payment Methods

The payment methods are differentiated between imported payments which are payments that have already been executed and payments executed by the API. PaymentMethods are defined by two parameters. PaymentTypeGroup and optionally PaymentType:

PaymentTypeGroupPaymentType
PAYMENTCARDVISA, MASTERCARD, AMEX
MOBILEVIPPS
CASH
ECARD
AGENT
REMITTED
GIFTCARDGIFTCARD
REQUISITION
INVOICECOLLECTOR, COLLECTOR_B2B, XLEDGER
PAYPALPAYPAL

Payment card

(back to table of contents)

See above

Sequence diagrams

We've created some sequence diagrams to illustrate the ideal way to create and handle payments.

Async with callback and auto-sale

With this payment flow, you don't rely on the synchronized answer from the API-call. Instead, you'll listen and react to different events.

Recommended payment flow for Nets

If you, as a client, should time out before the payment is completed, the following diagram illustrates how we want you to handle the payment flow.

Recommended payment flow for Nets with timeout

Async without auto-sale

If you want more control, it is possible to omit the autoSale from the transaction-call. This requires that the capture-call is performed.

Async payment flow without auto sale

We also recommend that if you time out before the payment is completed, that you try to refund the order when you receive events that indicate the payment has finished.

Standard synchronized

It is a possibility to do the whole process synchronously, but we do not recommend it.

Synchronized payment flow

Because this flow is synchronized, if you time out, you'll have no way of knowing if a cleanup is necessary (unless you also listen to events).

Vipps

(back to table of contents)

Using Vipps as payment method requires configuration of Vipps for your client. If this is not properly set up, please refer to the Payment Methods section before continuing. Vipps can be used both for mobile and browser clients. Each client type needs to be configured as its own distribution channel so that our backend can configure each type of client properly. This is important as the redirectUrl will be different depending on which type of client you are using.

Some of the set up will be documented by Vipps, and there are external links for this where appropriate.

Setup VippsMT

To be able to test your integration fully, you have to set up the Vipps test app. Documentation for this can be found on the Vipps test environment section on Github. We have some test users you can use in this environment:

Mobile numberPIN
480594941236
480595681236
480595691236
480595701236
480595711236
480595721236
480595071236
480595061236

Create payment for Vipps

Create a payment as you would usually do. The same orderId is used here as in the general guide, which means that these steps should be switched out with the corresponding steps in the guide above.

POST /sales/v1/payments

Create a Vipps transaction

Create a payment transaction for the Vipps payment type to the specified payment. Notice the difference in the _links-field from the general guide.

POST /sales/v1/payments/{paymentId}/transactions

Create an app claim

Instead of creating a virtual payment terminal, you now have to create an app-claim to be able to send the customer to the Vipps app. The redirectUrl format depends on the type of client you are using. For browser clients, this is a regular https address, and for native app clients this should be a deep link URL to your own app. More on this in the next section.

Note: For mobile app clients, the phone number is not relevant, and will not be taken into consideration. The actual phone number logged into the Vipps app on the customer's phone is always used. There does seem to be a bug in the API which means clients need to set this field to something, but it doesn't matter what.

POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/app-claim

Redirect to Vipps

For the actual integration between your client and the Vipps frontend, we point you to the Vipps documentation on App integration. All backend related integration is fully handled by the Entur payment service. According to the Vipps documentation, it is not viable to rely on either the fallback nor callback URL, or session cookies in the browser for a successful payment. After the customer has finished its interaction with the Vipps app, it is therefore recommended that the client either poll the payment transaction or consume PaymentEvents. Out of these two alternatives we recommend consuming PaymentEvents. This is because consuming PaymentEvents handles exceptional cases with longer than expected response times in a more convenient manner than the other alternative. How to perform these actions are described in the two following sections.

Consuming PaymentEvents

To be able to consume kafka events, please see Accessing the external Kafka Cluster. The relevant events for the use case are PaymentEvents, specifically PaymentEvents with the following types: PaymentTransactionCaptured, PaymentTransactionCancelled and PaymentTransactionRejected. When a transaction is successfully captured, a PaymentTransactionCaptured-event will be produced. The clients consuming this event must verify that remainingAmountToCapture in transactionSummary is equal to "0.00". If something unexpected happens during the capture-process, a PaymentTransactionCancelled-event or PaymentTransactionRejected-event will be produced.

Poll payment transaction

As the capture operation will be handled asynchronously between the Vipps and Entur backends, the client can poll the payment transaction for updates. There is a set time frame where the transaction is live in the Vipps system, and the Entur payment service will keep track of the transaction status throughout this time frame. This means that all clients will have to poll for updates for at least the same amount of time. The precise amount of time used by Vipps is described in detail in the Vipps documentation.

When polling the payment transaction, the client needs to check the operationLog for a successful CAPTURE operation for the same amount as the whole transaction. This is also visible in the summary as remainingAmountToCapture should be equal to "0.00".

GET /sales/v1/payments/{paymentId}/transactions/{transactionId}

Paypal

(back to table of contents)

See guide for payment card, but use value PAYPAL as paymentMethod.

Gift card

(back to table of contents)

The sales API supports payment with gift cards generated by the Benefits API. To pay with a gift card the customer needs to use the secret gift card code created by the Benefits API.

Flow for paying with a gift card:

  1. Create payment

  2. Create transaction with paymentGroup GIFTCARD

  3. Use POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/gift-card-claim to capture the payment

To get information about the gift card, use the Benefits API

This payment method is refundable by using refund.

However, the refunded money will be inserted into a new gift card with a new code printed on the receipt.

Invoice

(back to table of contents)

Invoice of type Collector is offered through Netaxept and requires a manual step where the user fills in contact information. The contact information form is pre-filled with the information registered on the customers' profile. The minimum required information registered on the profile is an email address.

Flow for paying with invoice type Collector:

  1. Create payment
  2. Create transaction with paymentGroup INVOICE and paymentType COLLECTOR
  3. Use POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/invoice to crate a terminal in Netaxept
  4. Capture Transaction

Example request body:

POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/invoice

{
  "redirectUrl" : "https://www.entur.org/",
  "languageCode" : "NOB"
}

This payment method is refundable by using refund.

Imported payment methods

(back to table of contents)

Import payments follow the same flow but require different additional fields when executing an import request. These are the steps needed to make an import payment:

  1. Create payment
  2. Create transaction
  3. Import payment, see the following sections for how to.

Payment card

There are three fields required for importing a payment card payment:

  • paymentType: Name of the specific payment type used
  • rrn: Reconciliation reference number used to track a transaction through different economy systems
  • terminalId: Unique identifier for a physical terminal

Example request for import:

POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/import

{
  "transactionConfirmedAt": "2018-03-07T12:20:46Z",
  "paymentType": "VISA",
  "rrn": "4",
  "terminalId" : "578000"
  "transactionData": {
    "additionalProp": "testProp"
  }
}

Cash

Example request for import:

POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/import

{
  "transactionConfirmedAt": "2018-03-07T12:20:46Z",
  "transactionData": {
    "additionalProp": "testProp"
  }
}

E-card

There are three fields required for importing an e-card payment:

  • mediaSerialNumberId: A 10 digit e-Card identifier
  • companyId: Issuer OIO id
  • envNetworkId: County code for issuer

Example request for import:

POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/import

{
  "transactionConfirmedAt": "2018-03-07T12:20:46Z",
  "mediaSerialNumberId": "0067340023",
  "companyId": "4",
  "envNetworkId" : "578000"
  "transactionData": {
    "additionalProp": "testProp"
  }
}

Agent

Example request for import:

POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/import

{
  "transactionConfirmedAt": "2018-03-07T12:20:46Z",
  "transactionData": {
    "additionalProp": "testProp"
  }
}

Remitted

Example request for import:

POST /sales/v1/payments/{paymentId}/transactions/{transactionId}/import

{
  "transactionConfirmedAt": "2018-03-07T12:20:46Z",
  "transactionData": {
    "additionalProp": "testProp"
  }
}

Fees

(back to table of contents)

Some Payment Methods have fees related to them. The payment total amount will be automatically updated with the fee amount once the transaction is added (after a POST /sales/v1/payments/{paymentId}/transactions/). To see whether a Payment Method has a fee, look for the 'paymentTypeFee' in the PaymentMethod response. The fees are also specified in the payment transaction response.

Accessing the external Kafka Cluster

(back to table of contents)

Prerequisites

(back to table of contents)

In order to access the external Kafka Cluster the following things need to be prepared:

  • A username and a password for the Kafka cluster
  • Organization-specific topics for your organization needs to be created

If you don't have these things in order, please contact your Partner Assistant in order to obtain access. You will not be able to connect and test your implementation without this.

Before starting implementation we recommend that you familiarize yourselves with the following documentation:

Quickstart

(back to table of contents)

As the Confluent documentation shows, there isn't that much configuration required to get started with a Kafka client. We have highlighted the few configuration properties required to be able to connect and communicate with Entur's external Kafka cluster.

bootstrap.servers

Use the following URLs to connect to the clusters:

Staging:

bootstrap.test-ext.kafka.entur.io:9095

Production

bootstrap.prod-ext.kafka.entur.io:9095

Note: The standard client port is not open, as we require every client to use a secure connection via SSL and SASL – see Security for more information.

group.id

Collective identification of all clients that belong to the same application. All instances of the same application must have the same group id. This means it should be set specifically and preferably to a descriptive name to make it easier to identify which user should have access to each group.

schema.registry.url

URL used to connect to the Avro schema storage.

Staging:

http://schema-registry.test-ext.kafka.entur.io:8001

Production:

http://schema-registry.prod-ext.kafka.entur.io:8001

Health checks

We provide a health check topic you can use to test client configuration where random test data is published without a schema – see Testing for more information.

Security

(back to table of contents)

Our Kafka cluster is setup with in-flight message encryption, using one-way SSL for all communication. The certificates are issued by Let's Encrypt, and should be trusted by most JVMs / operating systems.

  1. Client security configuration:
security.protocol=SASL_SSL
sasl.mechanism=SCRAM-SHA-512
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required \
     username="username" \
     password="$USER_PASSWORD";

Example Java code with staging addresses (change to production from the list above to connect to the production environment):

Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "bootstrap.test-ext.kafka.entur.io:9095");
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "my-application-name");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, KafkaAvroDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, KafkaAvroDeserializer.class);
properties.put("specific.avro.reader", true);
properties.put("schema.registry.url", "http://schema-registry.test-ext.kafka.entur.io:8001");

// Security
properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");

properties.put(SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-512");
properties.put(SaslConfigs.SASL_JAAS_CONFIG, String.format("org.apache.kafka.common.security.scram.ScramLoginModule required\nusername=\"%s\"\npassword=\"%s\";", config.getSasl().get("username"), config.getSasl().get("password")));

producer = new KafkaConsumer<>(properties);

Before this client can start consuming from Kafka, the user credentials need to be registered. All access control is based on SASL (Simple Authentication and Security Layer) using SCRAM (Salted Challenge and Response Authentication Mechanism). Upon registration with our Kafka cluster, each user will be given a username and a password, which should not be shared with anyone else. This user will be given consumer rights to the needed topics for your organization and every consumer group ( group.id ) used to read from these topics. By default, every resource in the cluster will deny everyone unless the provided credentials match the ACL for that resource.

Testing Client Configuration

(back to table of contents)

Before setting up a connection to the schema registry and downloading the appropriate Avro schema etc., there is a health check topic where random test data is published. Every user registered on our Kafka platform should have access to consume messages from this topic, with a provided consumer group, as long as the necessary security configuration mentioned above is set up correctly. The topic can be used as a simple way to get started with Kafka and to test that client configuration meets the requirements without having to think about any business logic on the consumer side. Below is all the information you need to get started:

  1. topic name: healthcheck-staging
  2. consumer group: <username>-consumer-staging

Testing access to topics can also be done, without having to implement your own Kafka clients, by using a tool called kafkacat. Information about installing and using this tool can be found here: https://github.com/edenhill/kafkacat An example command using all necessary security configurations for the staging cluster:

BOOTSTRAP_SERVERS=bootstrap.test-ext.kafka.entur.io:9095
TOPIC=sales-transaction-summary-staging-20
SECURITY_PROTOCOL=SASL_SSL
SASL_MECHANISMS=SCRAM-SHA-512
SASL_USERNAME=<your provided username>
SASL_PASSWORD=<password>
kafkacat -C -b $BOOTSTRAP_SERVERS -t $TOPIC -X security.protocol=$SECURITY_PROTOCOL -o beginning -X sasl.mechanisms=$SASL_MECHANISMS -X sasl.username=$SASL_USERNAME -X sasl.password=$SASL_PASSWORD

Please note that records will be consumed with this tool, which means that you should NOT use the same consumer group as you plan to use in your actual application.

Topic Descriptions

(back to table of contents)

Naming conventions

(back to table of contents)

All data on the external cluster is segregated on topics that only 1 organization has access to. As such, your organization can only access data applicable to your organization id, and no other organization will be able to read this data. Each topic will have an organization id in the name that matches the id of the data owner's organization. This id is appended to every topic on the cluster and drives the access rights to the topic.

All topic names (except the health check topic) follow the same convention:

<content description>-<environment>-<organisationId>
Example: payments-staging-1

Topics

(back to table of contents)

NameDescription#partitionsretention
healthcheck-stagingHealth check topic61 h
inventory-reservations-staging-<orgid>All reservations of stock to be read by the organization97 d
inventory-seat-reservations-staging-<orgid>All seat reservations performed in Seating to be read by the organization97 d
payments-staging-<orgid>Completed payments with payment transactions for the organization1528 d
payment-events-staging-<orgid>Events that happened to a payment. E.g. created, captured, rejected1528 d
credits-staging-<orgid>Completed credits with credit transactions for the organization1528 d
ticket-distribution-group-events-staging-<orgid>Events that happened to a ticket distribution group to be read by the organization. E.g. created, picked up, distributed, cancelled1528 d
sales-transaction-summary-staging-<orgid>Confirmed sales including all affected order line versions where the organization is either the owner of the product or the sales are completed in an organization-owned channel1528 d
given-consent-changed-staging-<orgid>All consents that have been changed by users from this organization.9compacted
customer-changed-staging-<orgid>All customers that have been changed for this organization.9compacted

Schema Registry

(back to table of contents)

All schemas can be found using the REST API for the Schema Registry. This registry will always be versioned and up-to-date.

For security reasons, we only allow READ access to external clients. This means that HTTP GET methods are the only valid requests from the schema registry REST API documentation.
docker run --rm -p 8000:8000 -e "SCHEMAREGISTRY_URL=http://schema-registry.test-ext.kafka.entur.io:8001" -e "PROXY=true" landoop/schema-registry-ui
http://localhost:8000
docker run --rm -p 8000:8000 -e "SCHEMAREGISTRY_URL=http://schema-registry.prod-ext.kafka.entur.io:8001" -e "PROXY=true" landoop/schema-registry-ui
http://localhost:8000

Add Recurring Payment

(back to table of contents)

This guide shows you how to create a recurring payment, create a terminal to input card details for the recurring payment, and authorize the card details that that were used as input when creating the terminal.

A recurring payment is a payment that is repeated. By creating a recurring payment, the payment details (credit/debit card details) are saved. This makes it easier to perform a transaction later because it is not necessary to input card details.

Create Recurring Payment

(back to table of contents)

POST /sales/v1/recurring-payments

Creates a new recurring payment with status CREATED and couples it to the customerNumber sent in the request.

Create Terminal

(back to table of contents)

POST /sales/v1/recurring-payments/{recurringPaymentId}/terminal

Registers the transaction with Nets and returns the location of the payment terminal where the user will provide card information. The terminalUri in the response is where the user has to input card information. Use the recurringPaymentId from the previous request.

Authorize Recurring Terminal

(back to table of contents)

PUT /sales/v1/recurring-payments/{recurringPaymentId}/authorize

Authorize the card details provided by the customers in the terminal were valid and accepted by Nets and by calling Nets with a process call. Use the recurringPaymentId from the previous request.

Use Recurring Payments

(back to table of contents)

When creating a recurring payment, the payment details (credit/debit card details) are saved for a customer.

To use a recurring payment, the following must be done:

  1. A payment must be created: create payment

  2. Get all recurring payments for a customer: get recurring payments for customer

  3. Add a payment transaction to the payment: create a payment transaction with a request body that contains a reference to one of the customers recurring payments. The field that has to be set in the request is recurringPaymentId. See the example request for recurring payment transactions below.

  4. Create a terminal: create a terminal

  5. Capture transaction: capture transaction

Get recurring payments for customer

(back to table of contents)

GET /sales/v1/recurring-payments?customerNumber={customerNumber}&includeExpired={includeExpired}

Gets all saved recurring payment (credit/debit cards) for a customer

Strong Customer Authentication Exemption

(back to table of contents)

Strong Customer Authentication (SCA) is required for transactions made online in the European Economic Area (EEA). However, a transaction may be exempt from SCA under certain circumstances. Currently, Entur only supports exempting low value transaction; i.e., transaction with a value of less than 30 euro.

Follow these steps to try to exempt a transaction from SCA:

Before using SCA exemption, the client should first understand the extra risk it is accepting, e.g., related to fraud.

  1. Create a payment as usual: Create payment

  2. Create a payment transaction as usual, however this time include the field scaExemption with the value of LOW_AMOUNT, it is the only currently supported exemption.

    The LOW_AMOUNT exemption will only be honoured when the total transaction amount is under 30 euro (roughly 300 NOK). If the total amount is greater than 30 euros SCA exemption will not be attempted.

    POST /sales/v1/payments/{paymentId}/transactions

    An example response can be found in the Create a Payment Transaction Guide.

  3. Create a terminal and redirect the customer to the terminal url.

  4. The rest of the payment-process is the same if the SCA exemption request succeeds. However, the SCA exemption request will eventually fail. When this happens the capturePayment-call will return with http status 409 - CONFLICT and the response body will contain an errorReason with the value SOFT_DECLINE.

  5. The correct way for a client to handle SOFT_DECLINE is to redirect the customer to same terminal url created in step 3. This will trigger SCA.

Error handling

(back to table of contents)

Payment

(back to table of contents)

When interacting with the Payment API, errors might happen either in the application itself, or upstream. In most of these cases the Payment API will return either a PaymentError or a ValidationError

PaymentError

A PaymentError contains the following values:

These values can be used to determine what went wrong, and then react accordingly.

ValidationError

A validation error contains exactly the same values as a PaymentError, and a list of errors.

ErrorReason

The ErrorReason-value exists to make it easier for clients to react to different errors. More might be added in the future, so your code must handle unknown error reasons.

The following ErrorReasons exist as of writing:

SOFT_DECLINE

When the ErrorReason-value is SOFT DECLINE, a transaction attempted with the Strong Customer Authentication (SCA) exception is declined. This means the customer's bank did not allow the transaction to be captured without SCA.

The correct way to react to this error is described in Strong Customer Authentication Exemption.

REFUSED_BY_ISSUER

The payment was denied by the customer's bank or payment method service provider. One possible response is to suggest that the consumer contact their bank or payment method service provider to resolve the issue.

RESOURCE_BUSY

Another process is currently utilising the requested resource. As of now, this only applies to Penalty Fare bills. Whenever this happens, wait a few seconds before making the same API request again.

USER_ERROR

The USER_ERROR error reason is an extension of the REFUSED_BY_ISSUER error reason. However, when this reason is given it is because the user was the source of the error.

Typical cases which might result in this error reasons are:

  • Insufficient funds
  • Invalid card number
  • Invalid CVV/CVN

The intended response to this error reason is to advise the customer to double-check their submitted data and to ensure that sufficient funds are available to cover the transaction. If all else fails, the fallback option is to ask the customer to contact their bank or payment method service provider if the problem persists.

Timeouts

(back to table of contents)

Capture payment client timeout

If a client has a self-declared timeout value lower than 60 seconds, the client might miss information about the transaction being captured. Depending on how the client handles these situations, this might cause the customer to purchase the same travel multiple times when they only intend to purchase the travel once.

So how should a client ideally handle the capture-payment-step when a self-induced timeout occurs?

The preferred way to handle this is to listen to the payment-events kafka topic and react to PaymentTransactionCaptured-events. To be able to consume kafka events, please see Accessing the external Kafka Cluster

When receiving a PaymentTransactionCaptured-Event after timing out, the client should clean up by crediting the transaction. Please see the Refunding section. All other events can safely be ignored.

Another solution is to start polling the transaction GET /sales/v1/payments/{paymentId}/transactions/{transactionId}, and check the status of the transaction, and when the status is captured clean up as stated above. We do not recommend this approach.