Ketendo

Authentication

Getting a token, and keeping it alive through a long load.

UAT https://uat-ketendo-api.azurewebsites.net  ·  Production https://ketendo-api.azurewebsites.net  ·  Last updated 2026-09-21

Every call to the integration API needs Authorization: Bearer <token>. This applies to every module - there is nothing transport-specific here.

Logging in

Log in once with POST /v1/Authentication/Login:

POST /v1/Authentication/Login
{
  "username": "integration@example.com",
  "password": "<secret>",
  "systemTypeId": 1,
  "identifier": "FABRIC"
}

The response carries both tokens:

{
  "result": {
    "success": true,
    "token": {
      "access_token": "eyJhbGciOi...",
      "refresh_token": "k7Qp2f...",
      "expires": 900,
      "valid_until": "2026-08-27T09:15:00+00:00"
    }
  }
}
Access tokens now expire after about 15 minutes. A long-running load that logs in once at the start will start failing partway through with an authentication error - the records already sent are saved, the rest are not. If you are looping over a few thousand records, you must refresh the token during the run.

Rather than hardcoding 15 minutes, read expires (seconds) or valid_until from the login response and refresh a minute or two before that.

Refreshing the token

POST /v1/Authentication/RefreshToken. Send the expired access token together with the refresh token - the expired one is still needed, it is validated ignoring its lifetime to identify you:

POST /v1/Authentication/RefreshToken
{
  "AccessToken": "<the access_token you were just using, even though it has expired>",
  "RefreshToken": "<the refresh_token from your last Login or RefreshToken call>",
  "SystemTypeId": 1,
  "Identifier": "FABRIC",
  "CurrentContextCompanyId": 101
}
FieldTypeRequired?Notes
AccessTokenstringRequiredYour current access token, expired or not
RefreshTokenstringRequiredThe most recent refresh_token you were given
SystemTypeIdintRequiredMust match the value you logged in with (1), or the refresh token won't be found
IdentifierstringRequiredMust match your login identifier (e.g. FABRIC)
CurrentContextCompanyIdintOptionalThe company context for the new token - send the same CompanyId you are loading against
CurrentContextCountryId, CurrentContextProvinceIdintOptionalRarely needed for integration
The refresh token rotates. Every successful refresh returns a new refresh_token and invalidates the previous one. You must store the new value and use it on your next refresh - reusing an old refresh token fails with Invalid Refresh Token.
The response shape is different to Login. Login nests the token under result.token; RefreshToken returns it directly under result. So it's result.access_token and result.refresh_token, not result.token.access_token. If you reuse your Login parse step here you will get an empty token.
{
  "result": {
    "access_token": "eyJhbGciOi...",
    "refresh_token": "9dRt4x...",
    "expires": 900,
    "valid_until": "2026-08-27T09:30:00+00:00"
  }
}
Simplest option for a batch loader: check the elapsed time at the top of each loop iteration (or each batch of records) and refresh when you're within a couple of minutes of expiry. Re-running Login instead of RefreshToken also works and is easier to wire up - the refresh flow just avoids sending the password repeatedly.
Once you have a token, see Platform for the conventions every endpoint follows, then your module guide for the records themselves.