For backend integrations that act as themselves, with no end user
PrerequisitesKINDE_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 returns403on 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:
detail | What 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.
