Ketendo

Conventions

The rules every endpoint follows, whichever module you are using.

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

What the integration API is for

These endpoints let an external system - a scheduling platform, a reservations system, an ops tool - push data into Ketendo and read back the reference lists it needs to do so. Everything on this page applies to every module. Pick your module from the nav for the records themselves.

Before you start

The golden rule: everything is an upsert

Every Save… endpoint takes an ExternalId -your own unique key for that record (e.g. your own record id, reference number, or whatever key that record already has in your system). Send the same ExternalId again with different data and Ketendo updates the existing record instead of creating a duplicate. This makes every call safe to retry.

Never reuse the same ExternalId for two different real-world records - Ketendo has no way to tell them apart.

CompanyId is required on every call

Every single Save… request below now needs a top-level CompanyId (int) - which company the record belongs to. This changed: it used to be inferred silently from your token's home company; it is no longer inferred at all. If your integration user has access to more than one company, send whichever CompanyId the record actually belongs to on every call.

If CompanyId is missing, zero, or negative, the call fails immediately with CompanyId is required. - there is no fallback to a default/home company any more. Update any existing integration you have against this API to start sending it.

Fields the server fills in for you

Don't send these - if you do, they're overwritten:

Two ways to send data

WayEndpoint patternBest forRecords per callHandles dependencies for you?
Single record/v1/integration/transportation/Save…Adding or updating one thing at a time1No - you must save things in the right order yourself
Batch/v1/integration/sync/batchSending many related records together (10-500 typical)ManyYes - you tell it what depends on what, and it saves them in the correct order automatically

Start with single-record calls while you're testing. Move to batch once you're sending real volumes.

Reading the errors

Every failure comes back in the same envelope, with a code that is more useful to branch on than the HTTP status:

SituationHTTPcodeWhat to do
Body is not valid JSON, or a field is the wrong type 400- Rejected before it reaches us, so you get the framework's validation response rather than the envelope below. Fix the payload shape.
A required field is missing or empty 500InternalError The message names the field, for example CompanyId is required. Fix it and resend - see the note below about the status.
You referenced a record that does not exist yet 500InternalError The message says what is missing and how to fix it, for example "No coach/part exists in Ketendo with PartExternalId 'X' for CompanyId 101. Save it with SavePart first, or send PartId directly." Save the dependency, then retry. Your module guide lists the order.
Token expired 401TokenExpired Refresh it - see Authentication - and resend.
Token missing, malformed or rejected 401TokenInvalid Log in again.
Authenticated, but not allowed to touch that record 403Forbidden Usually the wrong CompanyId for your integration user.
Anything else 500InternalError Send us the request body and the response - treat it as a bug report.
A 500 does not always mean we are broken. A missing field or an unresolved ExternalId currently comes back as 500 rather than 400, so do not treat 500 as automatically retryable - read the message, which names the field or the record in both cases. Retrying a payload that is missing CompanyId will fail every time.
The batch endpoint is different: it answers 200 even when operations fail, and reports each one separately with its own errorCode. See Batch endpoint.

Response envelope

Every successful response is wrapped the same way, across the whole Ketendo API:

{
  "httpStatusCode": 200,
  "code": "200",
  "message": "Request processed successfully.",
  "result": { /* endpoint-specific payload - shown per endpoint below */ },
  "totalRecords": null
}

To keep things short, the Example tab on each endpoint below only shows the result payload, not the full envelope. One casing note: field names in responses are always camelCase (e.g. externalId). Field names in requests are case-insensitive - PascalCase (as documented in the tables below, matching our internal model names) or camelCase both work.