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
GETonly. 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.
| Parameter | Default | Rules |
|---|---|---|
pageSize | 500 | Must be 1 or greater. Maximum 1000; larger values are reduced to 1000. |
after | none | Leave 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.
pageSizein 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
pageSizetasks (or assets), so one task's or asset's rows never split across two pages. These pages can contain more thanpageSizerows. - Stop when
hasMoreisfalse, not when a page is empty. On those three endpoints, a page is only empty whilehasMoreistrueif 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
Zsuffix, for example2026-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 arenullwhen the position is unknown (missing, unreadable, or 0,0). - Date filters such as
completedSincetake an ISO 8601 date-time and must be URL-encoded. Use UTC with aZsuffix, for examplecompletedSince=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 with400.
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
| Column | Type | Meaning |
|---|---|---|
campaignId | uuid | Campaign id. |
name | string | Campaign name. |
status | string | InProgress or Closed. |
dueDate | date or null | Campaign due date. |
GET /v1/tasks
One row per task: campaign tasks, scheduled and ad-hoc tasks, and workflow tasks.
Filters
| Filter | Type | Meaning |
|---|---|---|
campaignId | uuid | Only tasks in this campaign. |
status | string | Only 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. |
completedSince | date-time | Only tasks completed at or after this time. |
Columns
| Column | Type | Meaning |
|---|---|---|
taskId | uuid | Task id. |
name | string | Task name. |
templateName | string or null | Name of the task template the task was created from. |
campaignId | uuid or null | Campaign the task belongs to. Null for tasks outside a campaign. |
status | string | One of Outstanding, InProgress, Completed, CriticalExceptions, Exceptions, Rejected, Closed, CanNotComplete. An unrecognised status is returned as its number in a string. |
targetType | string or null | What the task is about: Item (an asset), Location, or AssetRegistration (a new asset created in the field). |
assetId | uuid or null | Asset the task is for. |
assetIdentifier | string or null | That asset's identifier. |
locationId | uuid or null | The task's location, or else its asset's location. |
locationIdentifier | string or null | That location's identifier. |
division | string or null | The asset's division. |
area | string or null | The asset's area. |
assignedTo | string or null | Display name of the person the task is assigned to. |
completedBy | string or null | Display name of the person who completed the task. |
dueDate | date-time or null | When the task is due. |
completedOn | date-time or null | When the task was completed. |
mergedOn | date-time or null | When the task's results were merged into the asset or location. |
rejectedOn | date-time or null | When the task was rejected. |
closedOn | date-time or null | When the task was closed. |
latitude | number or null | Position captured during the task if there is one, else the asset's position. |
longitude | number or null | As latitude. |
hasExceptions | boolean | True 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
| Filter | Type | Meaning |
|---|---|---|
taskId | uuid | Only this task's values. |
campaignId | uuid | Only values from tasks in this campaign. |
completedSince | date-time | Only values from tasks completed at or after this time. |
Columns
| Column | Type | Meaning |
|---|---|---|
taskId | uuid | Task the answer belongs to. |
section | string or null | Name of the task section the step is in. |
actionType | string | Verify or Check. |
question | string or null | Verify: the asset field's name, or the step name. Check: the question text. |
anchorId | uuid or null | Verify: the asset field being verified, if any. Null for Check. |
previousValue | string or null | Verify: the value before the task. Null for Check. |
value | string or null | Verify: the updated value, or the previous value if unchanged. Check: the yes or no answer text. |
isVerified | boolean or null | Verify: whether the value was confirmed. Null for Check. |
isUnknown | boolean or null | Verify: answered as unknown. Null for Check. |
isNotApplicable | boolean or null | Verify: 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
| Filter | Type | Meaning |
|---|---|---|
taskId | uuid | Only this task's exceptions. |
campaignId | uuid | Only exceptions on tasks in this campaign. |
completedSince | date-time | Only exceptions on tasks completed at or after this time. |
includeCleared | boolean | Also return exceptions that have been cleared. Default false. |
Columns
| Column | Type | Meaning |
|---|---|---|
taskId | uuid | Task the exception was raised on. |
exceptionId | uuid | Exception id. |
name | string or null | Exception name. |
description | string or null | Exception description. |
severity | string or null | Low or Critical. |
section | string or null | Name of the task section of the step that raised it. Null for exceptions raised on the task as a whole. |
step | string or null | Name of the step that raised it. |
actionType | string or null | Type of that step, for example Verify or Check. |
isCleared | boolean | True if the exception has been cleared. Always false unless you pass includeCleared=true. |
resolution | string or null | How the exception was resolved, when cleared. |
raisedOn | date-time or null | When the exception was raised. Null for cleared exceptions. |
GET /v1/assets
One row per asset, active and inactive. No filters.
Columns
| Column | Type | Meaning |
|---|---|---|
assetId | uuid | Asset id. |
identifier | string | Asset identifier. |
assetType | string or null | Asset type name. |
tag | string or null | Tag code attached to the asset. |
division | string or null | Division. |
area | string or null | Area. |
locationId | uuid or null | Location the asset is at. |
latitude | number or null | Asset position. |
longitude | number or null | Asset position. |
positionIsApproximate | boolean | True if the position is approximate. |
isActive | boolean | True 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
| Filter | Type | Meaning |
|---|---|---|
assetId | uuid | Only this asset's values. |
Columns
| Column | Type | Meaning |
|---|---|---|
assetId | uuid | Asset the value belongs to. |
anchorId | uuid | Id of the field. |
field | string or null | Field name. |
value | string or null | Current value. |
dateChanged | date or null | Day the value last changed. |
GET /v1/locations
One row per location. No filters.
Columns
| Column | Type | Meaning |
|---|---|---|
locationId | uuid | Location id. |
identifier | string | Location identifier. |
locationType | string | Point, LSD or Custom. |
division | string or null | Division. |
area | string or null | Area. |
latitude | number or null | Location marker position. |
longitude | number or null | Location marker position. |
How the tables join
| Key | From | To |
|---|---|---|
campaignId | tasks | campaigns |
taskId | task-values, task-exceptions | tasks |
assetId | tasks, asset-values | assets |
locationId | tasks, assets | locations |
anchorId | task-values | asset-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 get429with aRetry-Afterheader 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
completedSinceis a convenience filter, not a key for incremental refresh. Pulling only recently completed tasks will miss those later changes.
| Status | Meaning |
|---|---|
200 | Success. |
400 | Bad 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. |
401 | Missing, invalid or expired token. Request a new token. |
403 | The token is valid but this credential is not allowed to use the Reporting API. |
404 | No such endpoint, or a method other than GET (POST for /connect/token). Check the path. |
429 | Rate limited. See Retry-After. |
500 | Unexpected error. The body is { "message": "…", "reference": "…" }; quote the reference when you contact CarbonAi. |
503 | Temporarily unavailable. Wait for the Retry-After header (seconds), then retry. |
504 | The 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 exampleottotasks-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.