Machine-to-Machine (Client Credentials)

For backend integrations that act as themselves, with no end user

🚧

Prerequisites

KINDE_HOST: The base domain to use for Kinde requests, this should be provided by Latner. By default it is 'https://auth.latner.app' for Prod, or 'https://auth.latner-uat.app' for UAT.

KINDE_CLIENT_ID: The client_id of the M2M application setup by Latner for your App.

KINDE_CLIENT_SECRET: The client_secret of the M2M application setup by Latner for your App.

A service account provisioned by Latner in each tenant you need to reach. See Latner has to provision your app below - without it, every request is rejected.

Use this flow when your integration acts as itself rather than on behalf of a
person: a nightly sync, a backend service, a data pipeline. There is no browser, no
login screen and no redirect_uri.

If a real user signs in and your app acts for them, use the
Authorization Code flow
instead.

Getting an access token

Machine-to-machine uses the OAuth client_credentials grant. It is a single request -
there is no authorization code to exchange first.

sequenceDiagram
    autonumber
    participant App as Your App
    participant Kinde
    participant Latner as Latner API
    App->>Kinde: POST /oauth2/token (client_credentials)
    Kinde-->>App: access_token
    App->>Latner: GET /api/external/v1/... + Bearer token
    Latner-->>App: 200 (or 403)

Make a POST request to {KINDE_HOST}/oauth2/token with the following payload:

client_id={KINDE_CLIENT_ID}
&client_secret={KINDE_CLIENT_SECRET}
&grant_type=client_credentials

If Latner has told you your application needs an audience parameter, include it in
that same payload.

The response contains an access_token. There is no refresh_token in this flow, and
you do not need one - when the token expires, request another the same way. Cache the
token until close to its expiry rather than requesting one per API call.

Calling the API

Send the token as a bearer token against your tenant's subdomain:

GET https://{tenant}.latner.app/api/external/v1/equipment/
Authorization: Bearer {access_token}

The subdomain is not cosmetic. It selects the tenant your request runs against, and
that tenant is what your token is checked against.

Latner has to provision your app

This is the step that catches most integrations, because nothing about your Kinde
credentials hints at it.

A machine-to-machine token identifies an application, not a person. For that token
to reach a tenant's data, Latner must have provisioned an active service account in
that tenant, matched to your application. A token that is perfectly valid - correctly
signed, unexpired, right issuer - is still rejected if the tenant it is pointed at has
no matching service account.

Two consequences worth planning around:

  • Per tenant, not once. Access to one tenant grants nothing anywhere else. Every
    additional tenant needs its own service account provisioned before your token works
    against it.
  • Permissions are separate from authentication. The service account carries its own
    permissions, and those decide which endpoints it can reach. A token that authenticates
    successfully still returns 403 on an endpoint the service account was not granted.

So when you ask Latner to set up an M2M application, tell them which tenants and which
endpoints you need. Both are configured on Latner's side.

When requests are rejected

Every authentication failure returns 403, not 401 - including expired tokens.
The detail field distinguishes them:

detailWhat it means
Invalid or expired token.Signature, expiry or issuer check failed. Check you used the right KINDE_HOST and that your cached token has not expired.
No Active Service Account Found for that Kinde User Id.The token is valid, but this tenant has no active service account for your application. Ask Latner to provision one.
Org Code does not match current tenant.The token is bound to a different organisation than the subdomain you called. Check you are calling the right tenant.

A 403 carrying a permissions message rather than one of the above means the service
account authenticated but lacks permission for that endpoint.