Skip to content

Authentication

We use OAuth 2.0 client credentials. You exchange a client id and secret for a bearer token, then send that token on every request.


EnvironmentBase URLToken endpoint
Productionhttps://api.diller.apphttps://api.diller.app/connect/token
Sandbox (prerelease)https://api.prerelease.dillerapp.comhttps://api.prerelease.dillerapp.com/connect/token

Build against sandbox. Credentials are not shared between the two — a sandbox client id will not authenticate against production.

OpenID discovery is available at /.well-known/openid-configuration.


Credentials belong to the merchant, not to you. The merchant finds them in their own Diller account:

Settings → Our Details → API Details

That screen shows:

FieldValue
OAuth token endpointhttps://api.diller.app/connect/token
OAuth flowClient Credentials
Scopesapi.retailer
Client IDidentifies you at the token endpoint
Client Secretproves it. Treat it as a password

Ask the merchant to send you both, over something safer than email.

The store id that goes into every URL comes from the API once you have a token — see Finding your store id.


POST /connect/token, form-encoded — not JSON.

POST /connect/token HTTP/1.1
Host: api.prerelease.dillerapp.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=api.retailer&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET

You may instead send the credentials as HTTP Basic:

Authorization: Basic base64(client_id:client_secret)

If both are present, the header wins.

The response contains exactly four fields:

{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"scope": "api.retailer",
"expires_in": 3600,
"token_type": "Bearer"
}

expires_in is seconds. One hour is typical; treat the value as authoritative rather than hardcoding 3600.

ScopeGrants
api.retailerRead/write across members, coupons, stamps, transactions. This is the one you want.
read:transactionRead transactions only
ConditionResponse
grant_type is not exactly client_credentials400 — “Invalid grant type. Please ensure the grant_type is set to ‘client_credentials’”
Wrong id or secret400
Right credentials, wrong environment400

The token endpoint answers errors with a plain-text body, not JSON. Read it with res.text(); res.json() throws. Every other endpoint returns JSON errors — see the error shapes.


GET /api/v2.0/stores/{storeId}/members/search?phone=%2B4799900001 HTTP/1.1
Host: api.prerelease.dillerapp.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Content-Type: application/json

Every resource URL carries a {storeId} path segment. Read it from the API:

GET /api/v2.0/stores HTTP/1.1
Host: api.prerelease.dillerapp.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
[
{
"id": "Ab3xK_9pQrLm",
"store_name": "Nordmann Sport Oslo",
"currency": "NOK",
"time_zone": "Europe/Oslo"
}
]

The array always holds exactly one store: the one your credentials belong to. Its id matches your client id, and a request with any other store id in the path is refused with 403. Take id and keep it in configuration next to the base URL and the client id. Do this once, at setup — not on every request. Keep time_zone too; coupon validity and naive timestamps are both read in the store’s timezone.

Store ids are case-sensitive. Ab3xK_9pQrLm and ab3xk_9pqrlm are not the same store. Do not lowercase them anywhere in your config.


Do not fetch a token per request. Cache it, reuse it until it is nearly expired, and refresh on demand. Also retry once on a 401 — a token can be invalidated before its stated expiry.

sequenceDiagram autonumber participant App as Your server participant Cache as Token cache participant D as Diller App->>Cache: get token alt cached and not near expiry Cache-->>App: token else missing or expiring App->>D: POST /connect/token D-->>App: access_token, expires_in App->>Cache: store with expiry end App->>D: GET /api/v2.0/... (Bearer) alt 401 Unauthorized App->>D: POST /connect/token (force refresh) D-->>App: new access_token App->>D: retry once D-->>App: 200 else 200 D-->>App: 200 end

A minimal implementation:

let cached = null; // { token, expiresAt }
async function getToken({ force = false } = {}) {
const skewMs = 60_000; // refresh a minute early
if (!force && cached && cached.expiresAt - skewMs > Date.now()) {
return cached.token;
}
const res = await fetch(`${BASE}/connect/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'api.retailer',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
signal: AbortSignal.timeout(5_000),
});
// Token errors are plain text, not JSON.
if (!res.ok) throw new Error(`Token request failed: ${res.status} ${await res.text()}`);
const body = await res.json();
cached = {
token: body.access_token,
expiresAt: Date.now() + body.expires_in * 1000,
};
return cached.token;
}
export async function dillerFetch(path, init = {}, { timeoutMs = 2_000, retrying = false } = {}) {
const token = await getToken();
const res = await fetch(`${BASE}${path}`, {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(timeoutMs),
});
if (res.status === 401 && !retrying) {
await getToken({ force: true });
return dillerFetch(path, init, { timeoutMs, retrying: true });
}
return res;
}

The two-second default suits calls on the cart and checkout path, where a slow answer must not hold up the page. Pass a longer timeoutMs for background work such as posting transactions from a queue. A timeout throws a TimeoutError — catch it and degrade gracefully.


flowchart LR subgraph browser["Browser — untrusted"] F["Signup / checkout form"] end subgraph server["Your server — trusted"] H["Your endpoint"] K[["client_id<br/>client_secret<br/>access_token"]] end subgraph diller["Diller"] A["api.diller.app"] end F -->|"POST own-origin,<br/>session + CSRF"| H H --> K H -->|"Bearer token"| A A -->|"member data"| H H -->|"only what this<br/>customer may see"| F

Practical rules:

  • Expose your own endpoints (/api/loyalty/…) and call us from behind them.
  • Authorise every request against the logged-in session. Never let the browser pass a member id you then trust — a customer could enumerate other members’ coupons and points.
  • Store the secret in your secrets manager or environment, not in the repo.
  • We rotate the secret for you. Ask before you need it, not during an incident.