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:
- Machine-to-Machine (Client Credentials) - for a backend
service acting as itself, with no end user involved. This is the usual choice. - Get Access Token via Kinde (Authorization Code) - for an app where
a Latner user signs in and the integration acts on their behalf. Latner whitelists your
redirect URI. - Refresh Token - renewing an access token once it expires, without
sending the user back through the login screen.
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." }| Status | Meaning | What to do |
|---|---|---|
400 | The request body or query parameters failed validation | Fix the request - the body names the fields or parameters |
403 | The token was rejected (missing, malformed, expired, wrong issuer), or it is valid but the account lacks the permission for this endpoint | Check detail: get a new access token for Invalid or expired token.; otherwise ask Latner to grant the permission and do not retry |
404 | No such record, or it is outside the branches this account can see | The two are deliberately indistinguishable |
429 | Rate limit exceeded | Wait 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.
