API Reference

The Salling Group API is organized around REST. Our API has resource-oriented URLs, and uses HTTP response codes for indicating API errors. We use built-in HTTP features, like HTTP authentication and verbs, which any HTTP client understands.

We support cross-origin resource sharing allowing you to interact securely with our API from a client-side web application (never share your secret API key in a public website's client-side code). All API endpoint responses return JSON including errors.

Quick Start

Integrating the Salling Group APIs in your app or website can begin as soon as you have a bearer token or an API secret following these two steps:

  1. Obtain your API secret or token by writing an email stating your use case to apisupport@sallinggroup.com so that we can authenticate your API Requests
  2. Make API requests to confirm that everything is up and running

Step 1: Obtain your API key

Salling Group authenticates your API requests using your account's secret / token. If you do not send proper authentication information on your API requests we will return an error.

Step 2: Make a Request

To check that your credentials are working make an API request using either your bearer token or create a JWT for your request.

Bearer token request:

curl -X GET https://api.sallinggroup.com/v1/stores -H 'Authorization: Bearer <YOUR-TOKEN>'

JWT request:

curl -X GET https://api.sallinggroup.com/v1/stores -H 'Authorization: JWT <YOUR-TOKEN>'

If you get back a HTTP 200 status and a response with store data, then you are to start your integration with the Salling Group API.

Postman

Get all the APIs as a Postman collection.

Run in Postman

All you need is to set the Postman environment variables with your credentials.

Stores

The Stores API gives you access to location information and opening hours on all of Salling Group's ~1.500 stores in Denmark, Poland, Germany and Sweden for Netto, føtex, Bilka, Carls Jr and Salling.

Stores data include:

  • Name
  • Brand
  • Address
  • Coordinates
  • Distance (for proximity searches)
  • Opening hours

Request

curl -X GET https://api.sallinggroup.com/v1/stores -H 'Authorization: Bearer <YOUR-TOKEN>'

Partial Response

[
  {
    "id": "a431c9ff-1393-4b2f-84fd-0a849c8f0533",
    "created": "2015-12-10T07:04:09.943",
    "modified": "2017-03-14T14:00:40.37",
    "name": "Netto Falkoner Allé",
    "brand": "netto",
    "address": {
      "street": "Falkoner Allé 1-3, St.",
      "city": "Frederiksberg",
      "zip": "2000",
      "country": "DK",
      "extra": null
    },
    "coordinates": [
      12.533126,
      55.678764
    ],
    "hours": [
      {
        "date": "2017-11-22",
        "open": "2017-11-22T00:00:00",
        "close": "2017-11-23T00:00:00",
        "closed": false,
        "type": "store"
      }
    ]
  }
]

Specific Store

You can get a single store by its id.

curl -X GET https://api.sallinggroup.com/v1/stores/a431c9ff-1393-4b2f-84fd-0a849c8f0533 -H 'Authorization: Bearer <YOUR-TOKEN>'

Fields

By default you get all fields / attributes back when asking for your stores. If you're only interested in parts of the objects, you can use the fields parameter to specify exactly the fields your interested in. The fields parameter can be used in combination with all other parameters and also work when getting a store by its id.

curl -X GET https://api.sallinggroup.com/v1/stores?fields=name,address&per_page=1 -H 'Authorization: Bearer <YOUR-TOKEN>'
[
    {
        "name": "Bilka Tilst",
        "address": {
            "city": "Tilst",
            "country": "DK",
            "extra": null,
            "street": "Agerøvej 7",
            "zip": "8381"
        }
    }
]

Proximity Requests

You can make proximity requests by giving a geocode and a radius to get stores nearby. Radius is in kilometers and defaults to 10 kilometers if not provided.

curl -X GET https://api.sallinggroup.com/v1/stores?geo=55.1,10.2&radius=20 -H 'Authorization: Bearer <YOUR-TOKEN>'

Filters

The Stores API supports request filters on a lot of the attributes. The following filters are supported:

  • zip
  • city
  • country ("dk", "se", "de", "pl")
  • street (exact match only)
  • brand ("netto", "bilka", "foetex", "salling", "carlsjr")
curl -X GET https://api.sallinggroup.com/v1/stores?brand=netto&country=de&city=berlin&per_page=1 -H 'Authorization: Bearer <YOUR-TOKEN>'

curl -X GET https://api.sallinggroup.com/v1/stores?brand=foetex&country=dk&zip=2000&per_page=1 -H 'Authorization: Bearer <YOUR-TOKEN>'

Jobs

The Jobs API gives you access to all of Salling Group's available job postings in Denmark, Poland, Germany and Sweden for Netto, føtex, Bilka, Salling, Starbucks, Carl's Jr and Salling Group HQ.

Job data include:

  • Title
  • Description
  • Brand
  • Job details such as hours, start date etc.
  • Address
  • Application and public posting link
  • Job Categories and more...
curl -X GET https://api.sallinggroup.com/v1/jobs -H 'Authorization: Bearer <YOUR-TOKEN>'

Partial Response Example

[
  {
    "address": {
        "city": "Aarhus C",
        "country": "DK",
        "street": "Thorvaldsensgade 22",
        "zip": "8000"
    },
    "brand": "netto",
    "title": "Salgsassistent",
    "description": "<h3 align=\"left\">Salgsassistent</h3>\r<br><p>Vi s&oslash;ger en salgsassistent til Netto</p>\r<br><p>Som salgsassistent er du med til at sikre, at vores kunder f&aring;r en god indk&oslash;bsoplevelse. Din hverdag er varieret, og ikke to dage er ens.</p>\r<br><p><strong>Dine opgaver er blandt andet</strong></p>\r<br><ul>\r<br><li>Kassebetjening</li>\r<br><li>Vareopfyldning</li>\r<br><li>Ansvarlig for eget omr&aring;de - eksempelvis frugt- og gr&oslash;ntafdelingen</li>\r<br><li>Andet forefaldende butiksarbejde</li>\r<br></ul>\r<br><p><strong>Kvalifikationer</strong></p>\r<br><p>Som salgsassistent er det vigtigt, at du er positiv og serviceminded, s&aring; b&aring;de kolleger og kunder oplever butikken som et rart sted at v&aelig;re, n&aring;r du er p&aring; arbejde.</p>\r<br><p>Du er udadvendt og god til at kommunikere med alle typer af kunder. Naturligvis er det en fordel, hvis du har kendskab til butiksarbejde, men vi s&oslash;rger for grundig opl&aelig;ring.</p>\r<br><p><strong>Personalegoder </strong>Som ansat i Netto har du mulighed for at benytte en lang r&aelig;kke personalegoder.</p>\r<br><ul>\r<br><li>Personalerabat i Bilka, f&oslash;tex, Netto og&nbsp;Salling</li>\r<br><li>Medarbejderrabat p&aring; Nettos eget mobilabonnement - Nettalk</li>\r<br><li>Frisk frugt og gr&oslash;nt p&aring; arbejdspladsen hver dag</li>\r<br><li>Rabat p&aring; fitnessmedlemsskab</li>\r<br><li>... i alt omkring 100 - interne og eksterne - rabatordninger</li>\r<br></ul>\r<br><p><strong> Ans&oslash;gningen </strong></p>\r<br><p>Kan du se dig selv i stillingen som salgsassistent i Netto, s&aring; s&oslash;g jobbet via linket her p&aring; siden. Vi behandler ans&oslash;gningerne l&oslash;bende.</p>\r<br>",
    "start": "2018-04-16",
    "hours": "8",
    "jobType": null,
    "applicationLink": "https://career5.successfactors.eu/career?career_ns=job_listing&company=DSG&navBarLevel=JOB_SEARCH&rcm_site_locale=da_DK&career_job_req_id=5922",
    "created": "2018-05-28T08:49:20.304Z",
    "modified": "2018-05-28T08:49:20.304Z",
    "id": "3c5058f6-960b-440b-9535-18e1c1df7405",
    "published": "2018-04-11T22:48:45.000Z",
    "premium": false,
    "unsolicited": false,
    "trainee": false,
    "country": "DK",
    "region": "midtjylland",
    "categories": [
        "salesGeneral"
    ],
    "url": "https://sallinggroup.com/job/ledige-stillinger/job/?id=3c5058f6-960b-440b-9535-18e1c1df7405",
    "coordinates": null,
    "type": null
  }
]

Specific Job

You can get a single job by its id.

curl -X GET https://api.sallinggroup.com/v1/jobs/3c5058f6-960b-440b-9535-18e1c1df7405 -H 'Authorization: Bearer <YOUR-TOKEN>'

Fields

By default you get all fields / attributes back when asking for jobs. If you're only interested in parts of the objects, you can use the fields parameter to specify exactly the fields your interested in. The fields parameter can be used in combination with all other parameters and also work when getting a job by its id.

curl -X GET https://api.sallinggroup.com/v1/jobs?fields=title,url,start&per_page=1 -H 'Authorization: Bearer <YOUR-TOKEN>'
[
  {
    "title": "Salgsassistent",
    "url": "https://sallinggroup.com/job/ledige-stillinger/job/?id=3c5058f6-960b-440b-9535-18e1c1df7405",
    "start": "2018-04-16"
  }
]

Filters

The Jobs API supports request filters on a lot of the attributes. The following filters are supported:

  • zip
  • city
  • country ("dk", "se", "de", "pl")
  • brand ("netto", "bilka", "foetex", "salling", "carlsjr", "starbucks", "sallinggroup")
  • category
  • region
curl -X GET https://api.sallinggroup.com/v1/jobs?brand=netto&country=de&city=berlin&per_page=1 -H 'Authorization: Bearer <YOUR-TOKEN>'

curl -X GET https://api.sallinggroup.com/v1/jobs?brand=foetex&country=dk&zip=2000&per_page=1 -H 'Authorization: Bearer <YOUR-TOKEN>'

curl -X GET https://api.sallinggroup.com/v1/jobs?country=dk&region=hovedstaden&per_page=1 -H 'Authorization: Bearer <YOUR-TOKEN>'

curl -X GET https://api.sallinggroup.com/v1/jobs?country=de&category=businessDevelopment&per_page=1 -H 'Authorization: Bearer <YOUR-TOKEN>'

List of Supported Categories and Regions

Categories can be filtered by language ("en", "da", "sv", "de", "pl").

curl -X GET https://api.sallinggroup.com/v1/jobs/categories?language=en -H 'Authorization: Bearer <YOUR-TOKEN>'

Regions can be filtered by country ("dk", "se", "de", "pl").

curl -X GET https://api.sallinggroup.com/v1/jobs/regions?country=dk -H 'Authorization: Bearer <YOUR-TOKEN>'

Events

Our Events API provides information on in-store events in the Salling Group Bilka stores. Events can be competitions, product demos, concerts, product events, etc.

All events are in Danish. All requests must include a storeId.

Store events data include:

  • Title
  • Description
  • Category: "family", "kids", "adults"
  • Location at store
  • Store id
  • Application and public posting link
  • Date and time of event
curl -X GET https://api.sallinggroup.com/v1/events?storeId=d4c4e2e4-f67f-4aec-b249-e3a4ad3fc820 -H 'Authorization: Bearer <YOUR-TOKEN>'

Example Response

[
  {
      "id": "359736110f2763e9548bbb647018b5b9",
      "published": true,
      "title": "NESCAFÈ Dolce Gusto Piccolo maskiner i din lokale Bilka",
      "categoryId": "family",
      "description": "Demonstration af NDG kampagne kaffe maskiner og udvalgte NDG kapsler",
      "location": "I home afdelingen",
      "storeId": "d4c4e2e4-f67f-4aec-b249-e3a4ad3fc820",
      "date": "2018-08-03T14:00:00Z",
      "endDate": "2018-08-03T20:00:00Z"
  }
]

Holidays

Our Holidays API allows you to get Danish holidays in a given timeframe in years both in the future and in the past. It can also help you check if a given date is a holiday.

Dates used in this API should be in the following format YYYY-MM-DD.

Note: Multiple holidays may occur on the same date.

Holidays - The next 12 months

You can get all holidays within the upcoming year from today like so:

curl -X GET https://api.sallinggroup.com/v1/holidays -H 'Authorization: Bearer <YOUR-TOKEN>'

Holidays - The next 12 months from a date

You can get all holidays within the upcoming year from a given date like so:

curl -X GET https://api.sallinggroup.com/v1/holidays?startDate=2018-01-01 -H 'Authorization: Bearer <YOUR-TOKEN>'

Holidays - Until a given date

You can get all holidays between today and a given date like so:

curl -X GET https://api.sallinggroup.com/v1/holidays?endDate=2020-01-01 -H 'Authorization: Bearer <YOUR-TOKEN>'

Holidays - Specific period in time

You can get all holidays between two dates like so:

curl -X GET https://api.sallinggroup.com/v1/holidays?startDate=2018-01-01&endDate=2018-04-30 -H 'Authorization: Bearer <YOUR-TOKEN>'
[
    {
        "date": "2018-01-01",
        "name": "Nytårsdag",
        "nationalHoliday": true
    },
    {
        "date": "2018-01-06",
        "name": "Helligtrekongersdag",
        "nationalHoliday": false
    },
    {
        "date": "2018-02-11",
        "name": "Fastelavn",
        "nationalHoliday": false
    },
    {
        "date": "2018-02-14",
        "name": "Valentinsdag",
        "nationalHoliday": false
    },
    {
        "date": "2018-03-25",
        "name": "Palmesøndag",
        "nationalHoliday": false
    },
    {
        "date": "2018-03-29",
        "name": "Skærtorsdag",
        "nationalHoliday": true
    },
    {
        "date": "2018-03-30",
        "name": "Langfredag",
        "nationalHoliday": true
    },
    {
        "date": "2018-04-01",
        "name": "Påskedag",
        "nationalHoliday": true
    },
    {
        "date": "2018-04-02",
        "name": "2. påskedag",
        "nationalHoliday": true
    },
    {
        "date": "2018-04-27",
        "name": "Store bededag",
        "nationalHoliday": true
    }
]

Holidays - Check if a single date is a holiday

You can check if a given date is a holiday like so:

curl -X GET https://api.sallinggroup.com/v1/holidays/is-holiday?date=2018-11-11 -H 'Authorization: Bearer <YOUR-TOKEN>'
true

Authentication

We support multiple types of authentication, some APIs may only allow one or the other. As a general rule, all APIs support JWT authentication while bearer token is only allowed for some select APIs.

To decide which type of authentication you should use, you can read our guide.

JWT Authentication

We use JSON Web Tokens(JWT) for authentication based on the JWT spec. You can learn more about JWT on the official website.

Generating the JWT on your side

To authenticate the request, include an Authorization HTTP header containing the string JWT and the generated JSON Web Token.

The token should consist of the three following strings, combined with a .(dot) separator.

Header:

The header is a JSON object with metadata about how to calculate the signature.

  • alg (algorithm) – The algorihm used for signing the token. We only support HS256, HS384 and HS512.
  • typ (token type) – The type of the token. We only support JWT.

Payload:

The payload is a JSON object with information authenticating the request to be sent.

  • iss (issuer) – The users email address.
  • sub (subject) – The path and query of the request being authenticated. (example: /v1/stores?brand=foetex)
  • exp (expiration time) – Current time or time of expiration for authentication of the current request. (Formatted as a unix timestamp, seconds since 1970-01-01T00:00:00Z in the UTC timezone)
  • mth (method) – The HTTP Method of the request.

Signature

The signature is generated based on the header and payload following this pseudo-code algorithm:

encodedHeader = BASE64URLENCODE(header) // Not the same as regular base64 read the note after this code block.
encodedPayload = BASE64URLENCODE(payload) // Not the same as regular base64 read the note after this code block.

unencodedToken = JOIN_STRING(encodedHeader, '.', encodedPayload)

signature = HASH_FUNCTION(unencodedToken) // Use the hash function defined by the name in header.alg

NOTE: base64url is NOT the same thing as base64 encoding, to read more go to RFC4648.

This signature is then appended to the unencodedToken to validate that it has not been tampered with, to generate a full JWT of the form:

encodedHeader.encodedPayload.signature

based on the values generated in the pseudocode above.

Example

For example a token with the following fields:

header = {
  'alg': 'HS256',
  'typ': 'JWT',
};

payload = {
  'exp': 1451635200,
  'iss': 'user@domain.com',
  'mth': 'GET',
  'sub': '/v1/stores?brand=foetex',
};

would turn into the following HTTP header using secret as the signature key.

Authorization: JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NTE2MzUyMDAsImlzcyI6InVzZXJAZG9tYWluLmNvbSIsIm10aCI6IkdFVCIsInN1YiI6Ii92MS9zdG9yZXM_YnJhbmQ9Zm9ldGV4In0.-Gb5XKWd4xMrS412Y7B8m6jO-aH4Hik0pLdOPA294Vg

Bearer Authentication

The Bearer authentication model is very simple, but less secure.

Simply include the token/key you have been given from us in the HTTP Authorization header along with the string Bearer.

Example

If your token is 4b8c9202-9bae-11e8-abfa-ff5bd6029dd8, simply include this HTTP header with your request:

Authorization: Bearer 4b8c9202-9bae-11e8-abfa-ff5bd6029dd8

SDKs

SDKs have been created in order to simplifiy your workflow. These packages make it possible to quickly start using our APIs without much preliminary work.

Node.js

The Node.js SDKs are distributed through NPM. Their documentation can be seen on their respective NPM pages. You will need access to the respective APIs used by the SDKs.

Here is a list of the currently available SDKs:

Base SDK

The Base SDK creates an Axios instance that handles authorization for you. This is ideal if you want to use an API, which has no SDK, or just prefer more control. It is used internally by the other SDKs.

Stores SDK

The Stores SDK makes it easy to query Salling Group's stores through the Stores API. Through this you can query stores in a lot of ways and fetch information such as opening hours, address, and more.

Jobs SDK

The Jobs SDK makes it easy to query Salling Group's open job positions through the Jobs API. Through this you can query open job positions in a lot of ways and fetch information such as hours per week, title, description, and more.

Holidays SDK

The Holidays SDK makes it easy to query Danish (as of now) holidays through the Holidays API. Through this you can get all holidays within a range, and check if a given date is a holiday.

Pagination

Requests that return multiple items can be paginated by default. You can specify further pages with the page parameter. For some resources, you can also set a custom page size with the per_page parameter. Note that not all endpoints support the page and per_page parameters, see events for example.

Also the general page parameter is supported starting at 1. Omitting this parameter will give you results from the first page.

curl https://api.sallinggroup.com/v1/stores?page=2&per_page=10

Note that page numbering is 1-based and that omitting the ?page parameter will return the first page.

Requesting pages out of range will give you a HTTP 400 response.

Link Header

The pagination info can be included in the response Link header. It is important to follow the link header values instead of constructing your own URLs to ensure you follow API pagination standard.

Example from the stores API:

Link: <https://api.sallinggroup.com/v1/stores?page=1&per_page=10>; rel="first",
      <https://api.sallinggroup.com/v1/stores?page=3&per_page=10>; rel="next",
      <https://api.sallinggroup.com/v1/stores?page=1&per_page=10>; rel="previous",
      <https://api.sallinggroup.com/v1/stores?page=134&per_page=10>; rel="last"

The possible rel values are:

Name Description
next Shows the URL of the immediate next page of results.
last Shows the URL of the last page of results.
first Shows the URL of the first page of results.
prev Shows the URL of the immediate previous page of results.

Client errors

Errors are returned using proper HTTP error codes. Error responses will contain a API specific error code as well as a description on how to solve the error and a suggested user message.

Example:

{
  "errorCode": 1234,
  "developerMessage": "Your API rate limit exceeded. See http://developer.sallinggroup.com/api-reference/#topics-rate-limiting for details.",
  "userMessage": "The service is currently busy. Try again in 156 seconds",
  "moreInfo": "https://developer.sallinggroup.com/api-reference/#topics-client-errors"
}

Resource specific error codes follows the same convention, but can return resource specific errors. These are mentioned in the resource documentation.

Choosing authentication type

We offer two types of authentication, JSON Web Token (JWT) and Bearer tokens, and they are each described in the Authentication section.

In general, the preferred authentication type is JWT, but in an environment that can not be trusted to keep a secret the entire encoding and signature of a JWT is cumbersome and essentially unnecessary, so we also offer Bearer tokens, with a much simpler model that is then less secure.

Bearer tokens

We use bearer tokens for all environments that can not be trusted to keep a secret. If people get hold of such a bearer token, they can use it in all the ways you can use it, but at least they don't learn anything about you. No emails, names, or anything else is visible to the attacker apart from the opaque token.

This is probably the best we can do in an environment with no secrets. If a token is systematically abused we reserve the right to revoke it.

JSON Web Tokens

We use JSON Web Tokens in environments where secrets can be kept, even where network traffic might be compromised. If you have a backend server, that backend server can sign a JWT which includes precisely the resource you're trying to access and the point in time you're accessing it, and this token can then either be used directly by your backend or given to an end user, which will make the request. This requires the ability for your server to sign the JWT in secrecy, and then give the signed JWT to a potentially untrusted party.

What happens if such a JWT falls into the hands of a malicious user? They now have a token, which is valid for ~15 minutes, and which grants access to precisely the resource you signed off on. If you signed a JWT for requesting all Danish Netto stores, then this is what the attacker will be able to do for a short time.

This model significantly reduces the possible abuse resulting from a leaked JWT compared to a leaked Bearer token.

Conclusion

Go with the JWT solution if that's possible and convenient for you. We offer some SDKs to take care of the signing for you.

Go with Bearer tokens if secrecy is impossible or too cumbersome for your app.

JWT Authentication in Postman

If you are using Postman, to interact with our API, you can use the following script for authenticating towards the API using JWT.

var removeIllegalCharacters = function(input) {
    return input
        .replace(/=/g, '')
        .replace(/\+/g, '-')
        .replace(/\//g, '_');
};

var base64object = function(input) {
    var inputWords = CryptoJS.enc.Utf8.parse(JSON.stringify(input));
    var base64 = CryptoJS.enc.Base64.stringify(inputWords);
    var output = removeIllegalCharacters(base64);
    return output;
};

var url = request.url;
var slashIndex = url.toLowerCase().startsWith('http') ? 8 : 0;
var path = url.substring(url.indexOf('/', slashIndex), url.length);

var exp = Date.now() / 1000 | 0;
var iss = '<your email address goes here>';
var mth = request.method;
var sub = path;
var header = { 'alg': 'HS256', 'typ': 'JWT' };
var payload = { 'exp': exp, 'iss': iss, 'mth': mth, 'sub': sub };

var unsignedToken = base64object(header) + "." + base64object(payload);

var signatureHash = CryptoJS.HmacSHA256(unsignedToken, '<your secret goes here>');
var signature = CryptoJS.enc.Base64.stringify(signatureHash);
var token = unsignedToken + '.' + signature;

postman.setGlobalVariable('authToken', removeIllegalCharacters(token));

Add the script in the Pre-request Script section for your request.

Remember to replace the place-holder values:

  • <your email address goes here>: The e-mail address we have on file for you,
  • <your secret goes here>: The shared secret you have been supplied from us, when given access to services requiring JWT authentication.

To actually send the generated token, you should then add the following HTTP header to your request.

Authorization: JWT {{authToken}}

If you are implementing JWT outside of Postman, we recommend taking a look at the many great libraries available to make working with JWT much easier.

Support

We're here to help you. Contact us at API support.