OttoTasks

Reporting API

Read-only access to your company's OttoTasks data as flat, paged tables.

Overview

  • Base URL: https://api.ottotasks.dev.carbonai.ca/v1
  • Every endpoint is GET only. The API cannot change any data.
  • Each credential belongs to one company. You only ever see that company's data; there is no company parameter.
  • Machine-readable reference (OpenAPI): https://api.ottotasks.dev.carbonai.ca/docs/openapi.json

Authentication

CarbonAi gives you a client ID and a client secret. Exchange them for an access token using the OAuth 2.0 client credentials grant:

POST https://api.ottotasks.dev.carbonai.ca/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET
curl -s https://api.ottotasks.dev.carbonai.ca/connect/token \
  -d grant_type=client_credentials \
  --data-urlencode "client_id=$CLIENT_ID" \
  --data-urlencode "client_secret=$CLIENT_SECRET"

The response contains access_token, token_type (Bearer) and expires_in (seconds):

{ "access_token": "…", "expires_in": 3600, "token_type": "Bearer", "scope": "…" }

Send the token on every API request:

curl -s "https://api.ottotasks.dev.carbonai.ca/v1/campaigns" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
  • Tokens last about one hour. Request a new token at the start of each run rather than storing one.
  • Send the client secret only in the form body of the token request. Never put it in a URL or query string.

Paging

Every endpoint returns one page at a time.

ParameterDefaultRules
pageSize500Must be 1 or greater. Maximum 1000; larger values are reduced to 1000.
afternoneLeave out for the first page. For each next page, send the previous response's nextAfter.

Every response has the same envelope:

{
  "items": [ … ],
  "pageSize": 500,
  "hasMore": true,
  "nextAfter": "0b1f6c55-3f0e-4d55-9c1a-3c3f2a9d7e01"
}

Keep requesting with after set to the last nextAfter until hasMore is false. nextAfter is null on the last page. There is no total count.

  • pageSize in the response is the size actually used (after the 1000 limit).
  • Rows are returned in a stable order (by id), and each page starts after the previous one's last id, so rows created or deleted while you page never cause a row to repeat or be skipped.
  • task-values, task-exceptions and asset-values page over tasks and assets, not rows. A page holds every row for up to pageSize tasks (or assets), so one task's or asset's rows never split across two pages. These pages can contain more than pageSize rows.
  • Stop when hasMore is false, not when a page is empty. On those three endpoints, a page is only empty while hasMore is true if a very long run of tasks or assets has no rows.
GET https://api.ottotasks.dev.carbonai.ca/v1/tasks?pageSize=1000                    → "hasMore": true,  "nextAfter": "0b1f…"
GET https://api.ottotasks.dev.carbonai.ca/v1/tasks?pageSize=1000&after=0b1f…      → "hasMore": true,  "nextAfter": "7c42…"
GET https://api.ottotasks.dev.carbonai.ca/v1/tasks?pageSize=1000&after=7c42…      → "hasMore": false, "nextAfter": null  (stop)

Conventions

  • Column names are camelCase.
  • Ids are UUID strings.
  • Enums are strings, for example "Completed", not numbers.
  • Date-times are UTC in ISO 8601 with a Z suffix, for example 2026-11-05T17:44:30Z. Fractional seconds may appear.
  • Calendar dates have no time or zone, for example 2026-11-05. Don't convert them to a time zone.
  • A value that isn't set is null. Coordinates are null when the position is unknown (missing, unreadable, or 0,0).
  • Date filters such as completedSince take an ISO 8601 date-time and must be URL-encoded. Use UTC with a Z suffix, for example completedSince=2026-11-01T00:00:00Z. A value with no offset, or a date alone (2026-11-01), is read as UTC. If you send an offset instead, encode the plus sign as %2B (2026-11-01T00:00:00%2B02:00): a raw + in a query string is read as a space, and the date is rejected with 400.

Endpoints

All endpoints accept pageSize and after. Other filters are listed per endpoint and are all optional.

GET /v1/campaigns

One row per campaign. No filters.

Columns

ColumnTypeMeaning
campaignIduuidCampaign id.
namestringCampaign name.
statusstringInProgress or Closed.
dueDatedate or nullCampaign due date.

GET /v1/tasks

One row per task: campaign tasks, scheduled and ad-hoc tasks, and workflow tasks.

Filters

FilterTypeMeaning
campaignIduuidOnly tasks in this campaign.
statusstringOnly tasks with this status (see status below). Send the status name, in any letter case (Completed, completed), or its numeric value. Any other value returns 400.
completedSincedate-timeOnly tasks completed at or after this time.

Columns

ColumnTypeMeaning
taskIduuidTask id.
namestringTask name.
templateNamestring or nullName of the task template the task was created from.
campaignIduuid or nullCampaign the task belongs to. Null for tasks outside a campaign.
statusstringOne of Outstanding, InProgress, Completed, CriticalExceptions, Exceptions, Rejected, Closed, CanNotComplete. An unrecognised status is returned as its number in a string.
targetTypestring or nullWhat the task is about: Item (an asset), Location, or AssetRegistration (a new asset created in the field).
assetIduuid or nullAsset the task is for.
assetIdentifierstring or nullThat asset's identifier.
locationIduuid or nullThe task's location, or else its asset's location.
locationIdentifierstring or nullThat location's identifier.
divisionstring or nullThe asset's division.
areastring or nullThe asset's area.
assignedTostring or nullDisplay name of the person the task is assigned to.
completedBystring or nullDisplay name of the person who completed the task.
dueDatedate-time or nullWhen the task is due.
completedOndate-time or nullWhen the task was completed.
mergedOndate-time or nullWhen the task's results were merged into the asset or location.
rejectedOndate-time or nullWhen the task was rejected.
closedOndate-time or nullWhen the task was closed.
latitudenumber or nullPosition captured during the task if there is one, else the asset's position.
longitudenumber or nullAs latitude.
hasExceptionsbooleanTrue if the task raised any exceptions.

GET /v1/task-values

One row per answered Verify or Check step on a task. Steps in conditional sections appear only when that section was triggered. Steps that were left unanswered are not included. Paged over tasks (see Paging).

Filters

FilterTypeMeaning
taskIduuidOnly this task's values.
campaignIduuidOnly values from tasks in this campaign.
completedSincedate-timeOnly values from tasks completed at or after this time.

Columns

ColumnTypeMeaning
taskIduuidTask the answer belongs to.
sectionstring or nullName of the task section the step is in.
actionTypestringVerify or Check.
questionstring or nullVerify: the asset field's name, or the step name. Check: the question text.
anchorIduuid or nullVerify: the asset field being verified, if any. Null for Check.
previousValuestring or nullVerify: the value before the task. Null for Check.
valuestring or nullVerify: the updated value, or the previous value if unchanged. Check: the yes or no answer text.
isVerifiedboolean or nullVerify: whether the value was confirmed. Null for Check.
isUnknownboolean or nullVerify: answered as unknown. Null for Check.
isNotApplicableboolean or nullVerify: answered as not applicable. Null for Check.

GET /v1/task-exceptions

One row per exception raised on a task. Only open exceptions are included unless includeCleared=true. Paged over tasks (see Paging).

Filters

FilterTypeMeaning
taskIduuidOnly this task's exceptions.
campaignIduuidOnly exceptions on tasks in this campaign.
completedSincedate-timeOnly exceptions on tasks completed at or after this time.
includeClearedbooleanAlso return exceptions that have been cleared. Default false.

Columns

ColumnTypeMeaning
taskIduuidTask the exception was raised on.
exceptionIduuidException id.
namestring or nullException name.
descriptionstring or nullException description.
severitystring or nullLow or Critical.
sectionstring or nullName of the task section of the step that raised it. Null for exceptions raised on the task as a whole.
stepstring or nullName of the step that raised it.
actionTypestring or nullType of that step, for example Verify or Check.
isClearedbooleanTrue if the exception has been cleared. Always false unless you pass includeCleared=true.
resolutionstring or nullHow the exception was resolved, when cleared.
raisedOndate-time or nullWhen the exception was raised. Null for cleared exceptions.

GET /v1/assets

One row per asset, active and inactive. No filters.

Columns

ColumnTypeMeaning
assetIduuidAsset id.
identifierstringAsset identifier.
assetTypestring or nullAsset type name.
tagstring or nullTag code attached to the asset.
divisionstring or nullDivision.
areastring or nullArea.
locationIduuid or nullLocation the asset is at.
latitudenumber or nullAsset position.
longitudenumber or nullAsset position.
positionIsApproximatebooleanTrue if the position is approximate.
isActivebooleanTrue if the asset is active.

GET /v1/asset-values

One row per asset per field value. Only active fields are included. Paged over assets (see Paging).

Filters

FilterTypeMeaning
assetIduuidOnly this asset's values.

Columns

ColumnTypeMeaning
assetIduuidAsset the value belongs to.
anchorIduuidId of the field.
fieldstring or nullField name.
valuestring or nullCurrent value.
dateChangeddate or nullDay the value last changed.

GET /v1/locations

One row per location. No filters.

Columns

ColumnTypeMeaning
locationIduuidLocation id.
identifierstringLocation identifier.
locationTypestringPoint, LSD or Custom.
divisionstring or nullDivision.
areastring or nullArea.
latitudenumber or nullLocation marker position.
longitudenumber or nullLocation marker position.

How the tables join

KeyFromTo
campaignIdtaskscampaigns
taskIdtask-values, task-exceptionstasks
assetIdtasks, asset-valuesassets
locationIdtasks, assetslocations
anchorIdtask-valuesasset-values (same field)

A task's campaignId, assetId and locationId can be null, so join tasks to the other tables as optional (left) joins.

Limits and errors

  • Rate limit: API calls (/v1/…): 60 requests per minute per client, in total across all endpoints. Token requests (/connect/token): 30 per minute per IP address. Each IP address also has limits that normal use won't reach: 300 /v1/… requests per minute before the token is checked (shared by every client calling from that address), 60 per minute for /v1/… calls without a valid token, and 60 per minute for /docs/openapi.json. Over the limit you get 429 with a Retry-After header in whole seconds. Wait that long, then retry.
  • Page size: at most 1000 per request.
  • Refresh: pull the data at most once a day. The API returns the full data set each time; there is no "changed since" sync.
  • completedSince: tasks can change after they are completed (for example, an office review raises exceptions later), so completedSince is a convenience filter, not a key for incremental refresh. Pulling only recently completed tasks will miss those later changes.
StatusMeaning
200Success.
400Bad paging or filter value, for example pageSize=0, a malformed id or after or date (including a date whose + offset was not URL-encoded), or an unknown task status.
401Missing, invalid or expired token. Request a new token.
403The token is valid but this credential is not allowed to use the Reporting API.
404No such endpoint, or a method other than GET (POST for /connect/token). Check the path.
429Rate limited. See Retry-After.
500Unexpected error. The body is { "message": "…", "reference": "…" }; quote the reference when you contact CarbonAi.
503Temporarily unavailable. Wait for the Retry-After header (seconds), then retry.
504The page took too long to build. Retry with a smaller pageSize.

Versioning

The /v1 contract is fixed: new columns may be added to a table, so ignore columns you don't recognise. Columns will not be renamed or removed in /v1; changes like that only happen in a new version (/v2).

Credentials and rotation

  • Client IDs and secrets are issued by CarbonAi. Each secret is valid for 365 days.
  • Client IDs start with ottotasks-, for example ottotasks-acme.
  • Ask CarbonAi for a new secret about a month before the current one expires. The new secret works with the same client ID, so you only replace the secret in your configuration.
  • Keep the secret private: store it in a secrets store, not in code, shared files, emails or URLs. If you think it has been exposed, contact CarbonAi so it can be revoked and replaced.