Getting Started

Base URL, authentication, pagination, rate limits and error handling

The Latner External API gives approved integration partners programmatic access to a Latner
tenant: equipment and its current rental status, CRM lead capture, and aggregated revenue
reporting.

Base URL

Every tenant is served from its own host, so the subdomain is part of the base URL:

https://{tenant}.latner.app/api/external/v1

{tenant} is your Latner subdomain - the same one you use to sign in to the web app. A UAT
environment is available at https://{tenant}.latner-uat.app/api/external/v1 and is the
right place to build against first.

Authentication

Every request needs a bearer token issued by Kinde, our OAuth provider,
in the Authorization header:

Authorization: Bearer <access_token>

Latner issues your integration a client_id and client_secret. Which flow you use depends
on what your integration is:

Permissions

Access is permission-gated per endpoint. The token resolves to a Latner account - your
service account for machine-to-machine, or the signed-in user for the authorization code
flow - and that account is granted only the permissions your integration needs, so a token
that can read equipment will not necessarily be able to create CRM leads.

Both a rejected token and a missing permission return 403; the API never sends 401.
Read the detail string to tell them apart: Invalid or expired token. means the token
is the problem, while You do not have permission to perform this action. means the token
is fine and the grant is missing. Ask your Latner contact to widen the grant - retrying
will not help. The full list of authentication detail strings is on the
Machine-to-Machine page.

Pagination

List endpoints are paginated and return a consistent envelope:

{
  "count": 123,
  "next": "https://{tenant}.latner.app/api/external/v1/equipment/?page=3",
  "previous": "https://{tenant}.latner.app/api/external/v1/equipment/?page=1",
  "data": []
}

count is the total across all pages, and next/previous are null at the ends. Use
?page= to walk the pages and ?page_size= to set the page size - the default is 50 and
the maximum is 100.

Follow the next URL rather than constructing it yourself, and keep following it until it
is null. This matters most on the reporting endpoints: a wide group_by returns many
pages, and totalling only the first one silently under-reports.

Rate limits

Requests are limited to 60 per minute per account. The limit is shared by every token
issued to the same service account or user, so minting a second token does not buy more
capacity. Over that, the API returns 429 with a Retry-After header giving the number of
seconds to wait. Back off for that long rather than retrying immediately.

Errors

Errors use standard HTTP status codes and always return a JSON body.

Validation failures (400) are keyed by the offending input, with a list of messages per
key: request-body failures by field name, query-string failures by parameter name. Errors
that belong to no single field appear under non_field_errors:

{ "name": ["This field is required."] }
{ "from_date": ["from_date must be an ISO date (YYYY-MM-DD)."] }

Every other error carries a single detail string:

{ "detail": "Invalid or expired token." }
StatusMeaningWhat to do
400The request body or query parameters failed validationFix the request - the body names the fields or parameters
403The token was rejected (missing, malformed, expired, wrong issuer), or it is valid but the account lacks the permission for this endpointCheck detail: get a new access token for Invalid or expired token.; otherwise ask Latner to grant the permission and do not retry
404No such record, or it is outside the branches this account can seeThe two are deliberately indistinguishable
429Rate limit exceededWait for Retry-After seconds

Conventions

Dates and times. Datetimes are ISO 8601 in UTC (2025-02-16T22:54:06.955000Z). Plain
dates are YYYY-MM-DD.

Money. Amounts are JSON numbers with two decimal places and are tax-exclusive (the
invoice sub-total), in the tenant's own currency.

Identifiers. Equipment is addressed by uuid, which is stable across renames and is
what to store on your side - the equipment number can be changed or reused. Reporting
dimensions expose a *_id alongside their human-readable labels for the same reason: join
on the id, display the label.