Benefits
Concepts
Howtos
Concepts
In this section:
Loyalty programs
A loyalty program within the Entur systems is an extension to a product.
Thus, a loyalty program will always have a unique product attached.
The product may give fare reductions, but this is managed elsewhere in the Entur systems.
In these cases, this document will use the term "discount rights", as in "the customer has discount
rights from the XX product"
Loyalty programs are divided into three main types:
- time-limited
- coupon-limited
- points-limited
All types have a first date of validity and an expiration date (even if it may be null).
Time-limited loyalty programs
Identifier: "TIMED"
An example of a time-limited loyalty program is "a customer card". For this type of loyalty program
Entur supports registering any discounts the contract has been used to garner. It is not possible to
register points-transactions or coupons on contracts for a time-limited loyalty program.
Coupon-limited loyalty programs
Identifier: "COUPONS"
An example of a coupons-limited loyalty program is "a 10-coupon daily reduction", where a customer
is allowed 10 separate days of discount rights. In this case, the coupons-limited loyalty program
is configured to create a contract for a time-limited loyalty program for a given period (here, 24h)
from first purchase. On contracts for this type of loyalty program, neither points-transactions or
discounts may be registered. However, if configured, usage will be registered on the created
coupons-contracts.
Points-limited loyalty programs
Identifier: "POINTS"
This type of loyalty program is currently used for gift cards. Contracts for a Points-limited
loyalty program can contain a list of points-transactions. The type of the points is specified
on the initial deposit, and can not be mixed. It is not possible to register discounts or coupons on
a contract for a points-based loyalty program.
Loyalty program versions
Loyalty programs are versioned by date. For each version, these fields can be changed:
Product version. If a new product version is available in the products db, you can create a new
loyalty program version to match.
Version start and end times. A Loyalty Program is valid if there exists a version with startDate
before now and endDate after now. If multiple versions have overlapping time periods, the one
with the highest versionNumber is considered the CURRENT version.
If the startDate is in the future, it is considered to be in DRAFT status. Creating new versions
will overwrite the current DRAFT version.
If the endDate is in the past, the version is considered DEPRECATED and no new contracts can be
created for this version. If all versions are DEPRECATED, no new contracts can be created and no
existing contracts will be accessible.
In short:
DRAFT means it is possible to save the loyalty program for further edits or another publish date, but it is not possible to create contracts on the program.
CURRENT mean it is possible to create contracts.
DEPRECATED means that it is no longer possible to create contracts
Descriptions. If the descriptions are not altered when a new version is created, they are copied
to the new version.
For COUPONS based loyalty programs, you can adjust the couponConfig by making a new version.
Call to create a new version:
POST https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms/{loyaltyProgramId}/versions
{
"descriptions": [
{
"languageCode": "NOB",
"description": "Reis Kundekort gir deg 20% rabatt på alle kjøp"
}
],
"startDate": "2007-12-03T10:15:30+01:00",
"endDate": "2007-12-03T10:15:30+01:00",
"usageValidityPeriod": "P3DT12H30M5S",
"defaultCouponsLimit": 10,
"productVersion": "ENT:Version:V1"
}
Response:
{
"id": 2324,
"organisationId": 20,
"internalDescription": "Customer card with 15 % discount",
"productId": "string",
"productVersion": "string",
"startDate": "2019-12-20T09:40:18.213Z",
"endDate": "2019-12-20T09:40:18.213Z",
"usageValidityPeriod": "P3DT12H30M5S",
"defaultCouponsLimit": 10,
"versionNumber": 2,
"status": "DRAFT",
"descriptions": [
{
"languageCode": "NOB",
"description": "Reis Kundekort gir deg 20% rabatt på alle kjøp",
"createdAt": "2007-12-03T10:15:30+01:00",
"createdBy": "123e4567-e89b-12d3-a456-426655440000",
"lastChangedAt": "2007-12-03T10:15:30+01:00",
"lastChangedBy": "123e4567-e89b-12d3-a456-426655440000"
}
]
}
Usage validity
A loyalty program may define a usageValidityPeriod, governing how long any attached contracts are valid.
To avoid ambiguity, only Day and Time units are allowed.
Contracts
A Contract is the connection from one or more customers to a Loyalty Program. A Contract can be
considered an instance of a loyalty program. So, for instance, if the loyalty program is points-based
and relates to the fictional product ENT:PointsBasedProduct:GiftCard, each Contract can be considered
a single gift card. Another example, if we have the product ENT:EntitlementProduct:levelA1 (personnel
ticket, silver) and there exists a loyalty program with this product, each Contract can be considered
an entitlement for one specific employee or family member.
There are some limitations imposed on the types of thing you can register on a Contract based on the
type of the loyalty program it belongs to. Read more about this in the section about LoyaltyProgram types.
In particular, only POINTS-based Contracts may have transactions, only TIMED Contracts may have OrderLineEvents
and only COUPONS-based Contracts may have child Contracts - each representing a used coupon.
Contracts may have Consumers. A Consumer is a Customer, identified by customerNumber and organisationId.
The endpoint also accepts customerRef which may be useful for identifying external customers down
stream in the sales process. Consumers have two flags:
- isContractHolder: Whether this consumer is the current contract holder.
Only one contract holder can exist for a given contract.
Contract holders must be from the same organisation that owns the Contract.
It is not possible to add Consumers with isContractHolder=false unless there exists a Consumer
where isContractHolder=true.
- isBlocked: If a Consumer is marked as blocked, it will not show up as a valid contract for this
customer when searching. At the same time, since there exists a Consumer for that customer, they
can not create a new Consumer. This effectively blocks this customer from using this contract.
A Contract may be created with a set of policies. Policies are values that need to be supplied with
correct value to add a consumer to the Contract. For instance, if a Contract is created with a policy
that requires any consumers to be called "Anders", only customers supplying this information will
be able to consume the contract.
First, we create the Contract:
curl -X POST https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms/6/contracts -H"Authorization: Bearer $PARTNER_TOKEN" -H'Content-Type: application/json' -d'
{
"consumableFrom": "2019-11-01T10:15:30+01:00",
"policies": [{"key":"name","value":"Anders"}],
"expirationDate": "2040-11-01T10:15:30+01:00"
}
'
Response:
{
"uuid": "f1d419bc-e88e-43c9-bf16-6943921a9580",
...
"policies": [
{
"id": 969,
"key": "name"
}
],
"contractConsumers": null,
...
}
Attempt to consume with wrong name:
curl -X POST "https://api.staging.entur.io/customers/v2/benefits/contracts/f1d419bc-e88e-43c9-bf16-6943921a9580/contract-consumer" -H"Authorization: Bearer $PARTNER_TOKEN" -H'Content-Type: application/json' -d'
{
"customerNumber": 1234567,
"customerRef": "1234567",
"customerOrganisationId": 1,
"isContractHolder": true,
"policyValidationProperties": {
"name": "Benny"
}
}'
Response:
{
"timestamp": "2020-03-13T09:29:04Z",
"status": 401,
"error": "Unauthorized",
"path": "/benefits/contracts/f1d419bc-e88e-43c9-bf16-6943921a9580/contract-consumer",
"message": "Add contract consumer for contract f1d419bc-e88e-43c9-bf16-6943921a9580 failed. The contract policies didn't match the policies provided in the request.",
"correlationId": "9d54fd0c-8207-43f7-a4b9-7fb5aa333b46"
}
Attempt to consume with correct name:
curl -X POST "https://api.staging.entur.io/customers/v2/benefits/contracts/f1d419bc-e88e-43c9-bf16-6943921a9580/contract-consumer" -H"Authorization: Bearer $PARTNER_TOKEN" -H'Content-Type: application/json' -d'
{
"customerNumber": 1234567,
"customerRef": "1234567",
"customerOrganisationId": 1,
"isContractHolder": true,
"policyValidationProperties": {
"name": "Anders"
}
}'
Response:
{
"customerOrganisationId": 1,
"customerNumber": 1234567,
"customerRef": "1234567",
"isBlocked": false,
"isContractHolder": true,
"createdAt": "2020-03-13T09:31:24Z",
"createdBy": "6BiN61j5CQBeU7w4fa72x6pIqdmhcEO6",
"lastChangedAt": "2020-03-13T09:31:24Z",
"lastChangedBy": "6BiN61j5CQBeU7w4fa72x6pIqdmhcEO6"
}
Response codes
Calling contracts/validate-consumptions allows the caller to check if the given contracts are available for the given customers on the given day.
The relevant response codes are:
|
| 3101 | Ok. All are valid. |
| 3102 | Some of the passed contracts are unknown. |
| 3103 | Some of the passed contracts are unavailable on the date of travel. |
| 3104 | Some of the passed contracts are not available for the customer. |
| 3105 | Some of the passed contracts are blocked for the customer. |
| 3106 | Some of the passed contracts are out of coupons and no non-coupon contract was found on this date. |
Examples and Howtos
In this section:
How to create a loyalty program
Creating a new Loyalty Program is usually only to be done after some communication with Entur, since
it needs to be connected to a product. Here's how it's done then:
- Find out which product and version you need to target
- Decide on which kind of Loyalty Program you want to create. Read more in the section about Loyalty
Program types.
- Decide when the Loyalty program should be available. Typically, Loyalty programs have long lives.
- Decide how long a contract should be available as default. It may be several years, for instance for
customer cards.
- Decide if other organisations should be allowed to connect their customers to your contracts. This
will make it easier to adapt your loyalty program for new users, but will not drive customers to
your sales channels. Only your customers can own a contract, regardless of what you decide here.
- Decide on the text you want to use to represent this loyalty program to your customers.
Then, call the createLoyaltyProgram endpoint:
curl -X POST "https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms" -H"Authorization: Bearer $PARTNER_TOKEN" -H"Content-Type: application/json"
-d'
{
"loyaltyProgramType": "TIMED",
"productId": "ENT:DemoProduct:Demo",
"productVersion": "ENT:ProductVersion:V1",
"internalDescription": "Free coffee program",
"startDate": "2020-03-13T10:26:44.118Z",
"endDate": "2030-11-22T01:55:56+00:00",
"usageValidityPeriod": "P365D",
"descriptions": [
{
"languageCode": "ENG",
"description": "Show your ticket, get free coffee",
"displayName": "Demo Coffee Program"
},
{
"languageCode": "NOB",
"description": "Kaffeprogrammet til Entur gir deg gratis kaffe hos lokfører!",
"displayName": "Demo kaffe-program"
}
]
}'
Provide a loyalty program code if you want users to redeem the program.
This creates a program that your company can manage in Entur Partner.
curl -X POST "https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms" -H"Authorization: Bearer $PARTNER_TOKEN" -H"Content-Type: application/json"
-d'
{
"loyaltyProgramType": "TIMED",
"productId": "ENT:DiscountProduct:20",
"productVersion": "ENT:ProductVersion:V1",
"internalDescription": "20% discount coffee program",
"startDate": "2020-03-13T10:26:44.118Z",
"endDate": "2020-11-22T01:55:56+00:00",
"loyaltyProgramCode": "20Coffee"
"descriptions": [
{
"languageCode": "ENG",
"description": "Get 20% off a coffee!",
"displayName": "Demo coffee 20% discount"
},
{
"languageCode": "NOB",
"description": "Kaffeprogrammet til Entur gir deg 20% rabatt på kaffe hos lokfører!",
"displayName": "Demo kaffe 20% rabatt"
}
]
}'
How to create a contract
Before creating a Contract, make sure you know what you want to create:
- Do you know which customer is going to be the owner?
If so, find the customerNumber and customerReference.
- Do you know if there is a limiting factor, such as "may only be consumed by my customers" or
"Only Jan Petersen may consume this"? If so, add the necessary policies.
- Do you have a reference you would like to show up in the sales transaction summary if this
contract is used in a sale?
- When should the customers be able to use this contract? Set consumableFrom to this time.
- Does this contract have a specific expiration date or will the default work? Set expirationDate if it is specific.
- Is the default coupons configuration ok? If not, specify another number of coupons.
Only a few of the fields are required for creating a contract.
Contracts are then created using the createContract endpoint
Minimum:
curl -X POST "https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms/6/contracts" -H"Authorization: Bearer $PARTNER_TOKEN" -H"Content-Type: application/json" -d'
{
"consumableFrom": "2020-01-01T00:00:00+01:00",
}'
When we know more:
curl -X POST "https://api.staging.entur.io/customers/v2/benefits/loyaltyprograms/6/contracts" -H"Authorization: Bearer $PARTNER_TOKEN" -H"Content-Type: application/json" -d'
{
"consumableFrom": "2020-01-01T00:00:00+01:00",
"customerNumber": 12341567,
"customerRef": "SomeCustomer",
"externalRef": "ABCD-EFGH-1234",
"policies": [
{
"key": "email",
"value": "kari.normann@example.com"
}
],
"expirationDate": "2020-12-31T23:59:59+01:00"
}'
In the example above, the first contract is open to all, not connected to any consumers and only
accessible by uuid. The second is already connected to one consumer (as contract holder), and has
a policy that only allows a customer who supplies a specific email when attempt to consume. This
second contract can also be found via its externalRef or its consumer. The external reference is
propagated through the sales channels when used.
How to connect a profile to a contract
A profile can be connected to a Contract in two ways:
- It can be the contract owner. This must be a profile from the same organisation that owns the loyalty program and contract.
- It can be a contract consumer. This can be a profile from any organisation.
Common for both types of connections is that they must conform to the policy that has been set up for the Contract when the connection is performed.
Also, a major difference here is that a contract only can have one owner. If the owner is removed
from the contract, it will no longer be accessible.
To connect to a contract, use the addContractConsumer endpoint:
POST https://api.staging.entur.io/customers/v2/benefits/contracts/2c27bd7e-2b4d-4144-715b-fcbc8fe3807f/contract-consumer
{
"customerOrganisationId": 20, #1
"customerNumber": 1234567, #2
"customerRef": "073R41D4R", #3
"isContractHolder": true, #4
"policyValidationProperties": { #5
"email": "customer@email.example" #6
}
}
Output:
{
"customerOrganisationId": 20,
"customerNumber": 1234567,
"customerRef": "073R41D4R",
"isBlocked": false,
"isContractHolder": true,
"createdAt": "2019-10-14T10:15:30+01:00",
"createdBy": "clientId",
"lastChangedAt": "2019-10-14T10:15:30+01:00",
"lastChangedBy": "clientId"
}
The input values are:
- The organisation of the profile you are connecting
- The customer number of the profile.
- A customer reference. This field is not required but can be provided. If present, it will be echoed in the responses.
- Whether you are trying to register a profile as the Contract Holder. Only one such is possible.
- An object containing values for validating your claim to this contract. Depending on the contract, it may have a set of values you need to match to be able to consume it. The keys must match the keys used when creating the contract. Note, the contents of these fields are stored and compared as hashed values, so they may contain gdpr data without issues.
- For instance, if the contract requires a specific email, it is sent here.
To get the uuid for this url, you may want to look up via the search endpoint.
How to connect a profile to a personnel ticket right
Personnel tickets are treated slightly different than other contracts. All contracts for personnel ticket
loyalty programs are created by the personnel ticket system. They simultaneously create a customer
profile and connect it as owner of the contract. All of this happens with the organisation ID 29
(the Norwegian Public Rail Administration) and therefore by default inaccessible for all partners.
To mitigate this, a couple of helper-services have been created. To connect a customer to a
personnel ticket contract, there is a separate endpoint. The documentation for this endpoint can
be found here.
It is somewhat simpler and will make sure customers are connected correctly in both the new and old
personnel ticket regimes. It will look up the customer based on customerNumber, and connect it to
the corresponding contracts.
curl -X POST "https://api.staging.entur.io/customers/v2/benefits/contracts/claim-personnel-ticket" -H"Authorization: Bearer $PARTNER_TOKEN" -H"Content-Type: application/json" -d'
{
"externalReference": "ABCD-EF12-3456-GH45",
"customerNumber": 1234567
}'
Using contracts for entitlement products in the sales process
To use entitlement products in the sales flow, the customer must exist in Entur customers and it
must be connected to one or more entitlement products through loyalty program contracts (see the
previous two sections). To use the entitlement products in the sale process three things must be
added/changed in the sales process:
- Fetch entitlements
- Present the entitlements to the end user, and have them choose which to use
- Use selected entitlement to get discounted offer
- Accept the offer and payment
This section will guide you through the steps one by one.
Fetching entitlements
Example: Currently logged in customer is Anders Jensen. He has customerNumber 1234567. His customer
profile is connected to his personnel ticket (personnel ticket reference "ABCD-DEFG-GH12") and his
daughters' (Josefine, reference "1234-ABCF-3456")
To fetch entitlements, we call the endpoint:
GET https://api.entur.io/customers/v2/benefits/entitlements/1234567/by-customer-number
[
{ // Sølv, fritidsbillett
"productId" : "ENT:EntitlementProduct:levelA2",
"productVersion" : "ENT:Version:1",
"contractUUID" : "058a9dfa-187e-4443-8660-29d8879c146a",
"contractExternalRef" : "ABCD-DEFG-GH12",
"contractOwnerCustomerNumber" : 4567891,
"contractOwnerDisplayName" : "Anders",
"contractOwnerBirthDate" : {
"year": 1986,
"month": 11,
"day": 23
},
"contractValidFrom" : "2020-01-01T00:00:00.000",
"contractValidTo" : "2020-01-01T00:00:00.000",
},
{ // sølv, tjenestereisebillett
"productId" : "ENT:EntitlementProduct:levelA4",
"productVersion" : "ENT:Version:1",
"contractUUID" : "058a9dfa-187e-4443-8660-29d8879c146a",
"contractExternalRef" : "ABCD-DEFG-GH12",
"contractOwnerCustomerNumber" : 4567892,
"contractOwnerDisplayName" : "Anders",
"contractOwnerBirthDate" : {
"year": 1986,
"month": 11,
"day": 23
},
"contractValidFrom" : "2020-01-01T00:00:00.000",
"contractValidTo" : "2020-01-01T00:00:00.000",
},
{ // sølv, fritidsreisebillett
"productId" : "ENT:EntitlementProduct:levelA2",
"productVersion" : "ENT:Version:1",
"contractUUID" : "47db2f5f-bf14-4c34-9856-51ba4a213cbe",
"contractExternalRef" : "1234-ABCF-3456",
"contractOwnerCustomerNumber" : 4585692,
"contractOwnerDisplayName" : "Josefine",
"contractOwnerBirthDate" : {
"year": 2006,
"month": 3,
"day": 2
},
"contractValidFrom" : "2020-01-01T00:00:00.000",
"contractValidTo" : "2020-01-01T00:00:00.000",
}
]
Please note in the example output:
- Employees hav at least two entitlements. One for private journeys and one for work travel. These
have the same externalRef.
- Family members and pensioners have just one personnel ticket entitlement - for private travel.
- The Contract Owner for the personnel ticket entitlements are owned (isContractHolder=true) by a
customer profile administrated by the personnel ticket system. This means that the
ownerCustomerNumber typically will not refer to the current customer.
Present the entitlements to the end user
At this point, the sales client must present the relevant choices to the end user and offer an option
to choose which ticket rights to use, if any. For each, this is also where it is connected to the
userType - ADULT, CHILD, etc. - for each traveller. This list should be filtered based on traveldate
and the validity dates of the contracts.
In our example, we could choose to present this as a list of choices:
|
| Voksen | 0 | - + |
| Barn | 0 | - + |
| Anders, Adult, private | | + |
| Anders, Adult, corporate | | + |
| Josefine, Child, private | | + |
Also, this is the place to determine if any of the travellers get senior citizens rebate.
Use selected entitlement to get discounted offer
Once the end user has selected who is travelling, you can query offers with the entitlement products.
In the example, the end user has chosen two private travels, one for himself and one for his daughter.
Offers v2
Request:
POST https://api.entur.io/offers/v2/trip-pattern
...the rest of the offers query...,
"travellers": [
{
"id": "travelerAdultUniqueId",
"userType": "ADULT", // OR "age": 40
"productIds": ["ENT:EntitlementProduct:levelA3"]
},
{
"id": "travelerChildUniqueId",
"age": 11, // OR "userType": "CHILD"
"productIds": ["ENT:EntitlementProduct:levelA2"]
}
],
"tripPattern": {
"legs": ...
}
... rest of offers query...
Response:
"offers": [
{
"id": "b39036b9-1c4e-4a22-8ddb-1c7d4bd0fa87",
...
"preassignedProducts": [
{
...
"discountRight": {
...
"originatingFromProductId": "ENT:EntitlementProduct:levelA3"
}
}
],
"travellerMapping": [
{
"travellerIds": [
"2d0ac23b-78aa-49df-a372-9de5abad6faa"
],
"maxNumberOfTravellers": 1,
"minNumberOfTravellers": 1,
"userType": "ADULT"
}
],
},
{
"id": "e601e29f-598b-4a5c-9ba5-0218d90ec6f2",
...
"preassignedProducts": [
{
...
"discountRight": {
...
"originatingFromProductId": "ENT:EntitlementProduct:levelA2"
}
}
],
"travellerMapping": [
{
"travellerIds": [
"2d0ac23b-78aa-49df-a372-9de5abad6faa"
],
"maxNumberOfTravellers": 1,
"minNumberOfTravellers": 1,
"userType": "CHILD"
}
],
...
},
Since the offers service has no concept about a customer we need to pass the entitlements for the
correct travellers to get the correct prices. Internally, the entitlement product we send in has a
connection to a SalesDiscountRight which is applied to the prices before returning from offers.
When passed an entitlement product, offers will locate valid SalesDiscountRights and apply them.
Which SalesDiscountRights were applied can be read directly from the returned offer in the section
called FareProductConfiguration. Any calculated discounts can be found at jpath
offers[].salesPackageConfig.fareProducts[].discountRight.parameters.entitlementGiven[]
We recommend the savings are shown to the end user:
|
| Oslo S - Bergen, Vy | |
| |
| Normal price | 1579 |
| Personnel ticket discount (*) | -1579 |
| Seat 45B | 59 |
| Total | 59 |
Here, the (*) may show some text about this kind of discount being reported to the Tax Authorities.
Accepting the offer and payment
Once the offer above is accepted, the procedure for creating the order requires some small changes
when using entitlements.
To begin, we need to know if the current logged in user is allowed to use those entitlements. To do
this, we need to have that person as creator of the Order:
POST https://api.entur.io/sales/v1/orders
{
... other order-values ...,
"contactInfo": {
"createdBy": {
"id": 1234567
}
}
}
Then, the offer is converted to an order. Here, we need to include who is travelling (so names on
personnel tickets can be correct). Also, which contracts and entitlement product were actually
chosen for the different travellers. This is done when calling reserve-offers with an appropriate
offerConfiguration:
POST https://api.entur.io/sales/v1/reserve-offer
"offerConfigurations": [
{
"offerId": "8b2cfd32-836a-40f8-b244-d7bef739144d",
"customers": [
{
"customerId": "4567891",
"entitlements": [
{
"contractId": "058a9dfa-187e-4443-8660-29d8879c146a",
"entitlementProductRef": {
"id": "ENT:EntitlementProduct:levelA2"
},
"externalEntitlementRef": {
"id": "ABCD-DEFG-GH12"
}
}
]
}
]
},{
"offerId": "8b2cfd32-836a-40f8-b244-d7bef739144e",
"customers": [
{
"customerId": "4585692",
"entitlements": [
{
"contractId": "47db2f5f-bf14-4c34-9856-51ba4a213cbe",
"entitlementProductRef": {
id": "ENT:EntitlementProduct:levelA1"
},
"externalEntitlementRef": {
"id": "1234-ABCF-3456
}
}
]
}
]
}
]
Note about the example: Each traveller gets their own offerConfiguration. For each element we
specify who the traveller is through the customer object (with customer number from the entitlement),
and which contract and entitlement product were used.
This is then used internally to check if the creator is allowed to use the entitlements they are
presenting. Note that these credentials and the activation time are checked a second time when
activating the ticket.
NOTE: An entitlement must be valid for the customer on the date of travel when creating the order
and, for period tickets, both when attempting to activate the ticket and on the expiry date.