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
- Get a Bearer token for the environment you're calling (UAT / DEV / Production have separate tokens). Tokens expire after about 15 minutes - see Authentication.
- Pick a fixed source system name for the
X-Source-Systemheader (e.g.FABRIC). Use the same value on every call - it's how Ketendo tracks which records came from you. - Every request needs:
Authorization: Bearer <token>,X-Source-System: <your system>,Content-Type: application/json.
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.
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.
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:
Id/ internal GUIDs and numeric ids - resolved automatically from yourExternalId(created the first time, reused after that).ExternalIdentifier(on the saved record) - set from theExternalIdyou sent.
Two ways to send data
| Way | Endpoint pattern | Best for | Records per call | Handles dependencies for you? |
|---|---|---|---|---|
| Single record | /v1/integration/transportation/Save… | Adding or updating one thing at a time | 1 | No - you must save things in the right order yourself |
| Batch | /v1/integration/sync/batch | Sending many related records together (10-500 typical) | Many | Yes - 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:
| Situation | HTTP | code | What 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 | 500 | InternalError |
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 | 500 | InternalError |
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 | 401 | TokenExpired |
Refresh it - see Authentication - and resend. |
| Token missing, malformed or rejected | 401 | TokenInvalid |
Log in again. |
| Authenticated, but not allowed to touch that record | 403 | Forbidden |
Usually the wrong CompanyId for your integration user. |
| Anything else | 500 | InternalError |
Send us the request body and the response - treat it as a bug report. |
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.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.