Image Extraction
Extract every image attached anywhere in a Ditio project — feed posts, alerts (incident registrations), and checklist submissions — in a single paginated stream. Intended for BI tools, archives, and integrations that need a complete image inventory with GPS, uploader, timestamps, and project/task context.
Endpoint
Section titled “Endpoint”GET /v1/images- Test:
https://core-api.ditio.dev/reporting/v1/images - Production:
https://core-api.ditio.app/reporting/v1/images - Auth: OAuth 2.0 client credentials with the
reportingapiv1scope — see Authentication
What it returns
Section titled “What it returns”One row per (image × parent record) combination. A parent record can be a
feed post, an alert/incident, or a checklist submission. Each row includes:
- File identity:
id, original filename, type, size - Four download URLs (original + thumbnail/medium/large resizes)
- Source:
feed,alert, orchecklist - Project/task context:
projectId,projectNumber,projectName,taskId,taskName,activityId,activityNumber,companyId,projectCompanyId - Uploader:
createdByUserId,createdByUserName,createdDateTime - GPS:
latitude,longitude,accuracy(falls back to the parent record’s location when the image itself has no embedded coordinates) - Road reference:
roadReference— country-agnostic road-network position resolved asynchronously after upload (e.g. NVDBvegreferansefor Norwegian roads)
Images are returned as URLs, not bytes. Fetch the bytes from the returned URLs using the same bearer token.
Query parameters
Section titled “Query parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
ProjectId | string | No | Restrict to one project. |
CompanyId | string | No | Restrict to data owned by this project-company. |
Sources | string[] | No | Restrict to a subset. Allowed values: feed, alert, checklist. Omit or leave empty to include all three. |
FromDateTime | datetime | No | Full-sync window start. Applies to the parent record’s creation date. |
ToDateTime | datetime | No | Full-sync window end. |
ModifiedSince | datetime | No | Incremental-sync window start. When set, switches the response into incremental mode — results are ordered by the parent’s last modification timestamp. |
ModifiedBefore | datetime | No | Incremental-sync window end. Requires ModifiedSince. Must be strictly greater than ModifiedSince. |
ChunkLimit | int | No | Target page size, measured in parent records. Default 2500, range 1–10000. A single parent may emit multiple image rows, so data.length can exceed ChunkLimit. |
ContinuationToken | string | No | Opaque token from a previous response — echo it back verbatim to get the next page. |
RequireGeoCoordinate | bool | No | When true, drop images that have neither file-level nor parent-level GPS. Default false. |
OwnCompanyDataOnly | bool | No | When true, only return records owned by the calling company (not those merely shared with it). |
ExcludeDataFromSubsidiaries | bool | No | For parent-company callers, exclude data from subsidiary companies. |
Sync modes
Section titled “Sync modes”| Mode | Trigger | When to use |
|---|---|---|
| Full | ModifiedSince not set | Initial backfill. Optionally bound the window with FromDateTime / ToDateTime. |
| Incremental | ModifiedSince set | Ongoing sync — pass your last successful sync timestamp. Tighter is better (see performance notes). |
The response field syncTypeName echoes back "full" or "incremental".
Response shape
Section titled “Response shape”{ "data": [], "meta": {}, "continuationToken": "cG9zdHwyMDI2LTA0LTIzVDEwOjAwOjAwWg==", "syncTypeName": "incremental", "recordCount": 1842, "deletedRecordCount": 0, "links": { "next": "/v1/images?ProjectId=...&ContinuationToken=..." }}data[i] fields (example row)
Section titled “data[i] fields (example row)”{ "id": "6610f1a3c5000000000000aa", "fileName": "uploaded-6610f1a3c5.jpg", "fileNameOriginal": "IMG_20260412_093015.jpg", "fileType": "image/jpeg", "fileSize": 2847391,
"downloadUrl": "https://core-api.ditio.dev/core/api/file/6610f1a3c5000000000000aa", "thumbnailUrl": "https://core-api.ditio.dev/core/api/file/6610f1a3c5000000000000aa?MaxDimension=128", "mediumUrl": "https://core-api.ditio.dev/core/api/file/6610f1a3c5000000000000aa?MaxDimension=640", "largeUrl": "https://core-api.ditio.dev/core/api/file/6610f1a3c5000000000000aa?MaxDimension=1980",
"source": "feed", "originDescription": "Feed", "collectionRef": "Post", "idRef": "6610e02a00000000000000bb",
"companyId": "620000000000000000000001", "projectCompanyId": "620000000000000000000001", "projectId": "65a000000000000000000002", "projectNumber": "P-2026-042", "projectName": "Prosjekt Fjellhall", "taskId": "65b000000000000000000003", "taskName": "Brudekke", "activityId": null, "activityNumber": null,
"createdByUserId": "7a3f0000000000000000000c", "createdByUserName": "Kari Nordmann", "createdDateTime": "2026-04-12T09:30:15Z", "parentModifiedDateTime": "2026-04-12T10:05:02Z",
"latitude": 59.9139, "longitude": 10.7522, "accuracy": 8.5,
"roadReference": { "provider": "nvdb", "countryCode": "NO", "referenceText": "Ev6 S2D1 m1234", "roadCategory": "E", "roadNumber": "6", "section": "S2D1", "meterFromStart": 1234, "distanceFromCoordinateMeters": 4.5 },
"userSecurityLevel": 0}roadReference is resolved asynchronously from the image’s GPS after upload.
It is null when there is no GPS, no road match within tolerance, the
provider was unreachable, the uploader’s company doesn’t have the
nvdb-road-reference pilot flag enabled, or the background enrichment
job has not yet processed the upload (typically populated within seconds —
re-read the image to pick up the value). The first provider is NVDB (Statens
vegvesen, Norway) — referenceText is the same kortform you’d see in
Vegkart.
Source values
Section titled “Source values”feed— user posted the image directly to the project feedalert— image attached to an alert / incident registrationchecklist— image on a checklist submission (section banner or question answer)
Use source to categorize and originDescription for a human-readable label
(e.g. a checklist template name).
Fetching image bytes
Section titled “Fetching image bytes”The four URL fields point to Ditio’s authenticated file endpoint. Call them with the same bearer token:
curl -L -o image.jpg \ -H "Authorization: Bearer $TOKEN" \ "https://core-api.ditio.dev/core/api/file/6610f1a3c5000000000000aa?MaxDimension=640"Responses include Cache-Control headers — bytes are cacheable for 14 days
by default, so CDN/client caching is safe.
Pagination
Section titled “Pagination”Paging is driven by continuationToken:
- Call the endpoint with your initial filter (no
ContinuationToken). - If the response has a non-empty
continuationToken, pass it verbatim as theContinuationTokenquery parameter on the next call (keep everything else the same). - Repeat until the response’s
continuationTokenis absent or empty.
The token is opaque — don’t parse or modify it. It encodes which source
(feed / alert / checklist) is being drained and where within that
source; when one source exhausts, the token automatically advances to the
next enabled source. See Pagination.
Example — full backfill
Section titled “Example — full backfill”export DITIO_REPORTING_BASE="https://core-api.ditio.dev/reporting" # test
# First pagecurl -s -G "$DITIO_REPORTING_BASE/v1/images" \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "ProjectId=65a000000000000000000002" \ --data-urlencode "ChunkLimit=1000" \ > page1.json
# Follow the continuation token until emptyCT=$(jq -r '.continuationToken // empty' page1.json)while [ -n "$CT" ]; do curl -s -G "$DITIO_REPORTING_BASE/v1/images" \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "ProjectId=65a000000000000000000002" \ --data-urlencode "ChunkLimit=1000" \ --data-urlencode "ContinuationToken=$CT" \ > page_next.json CT=$(jq -r '.continuationToken // empty' page_next.json)doneExample — hourly incremental sync (Python)
Section titled “Example — hourly incremental sync (Python)”import osimport requests
REPORTING_BASE = os.environ.get( "DITIO_REPORTING_BASE", "https://core-api.ditio.dev/reporting")headers = {"Authorization": f"Bearer {token}"}
def sync_images(last_sync_timestamp): params = {"ModifiedSince": last_sync_timestamp, "ChunkLimit": 2500} while True: page = requests.get( f"{REPORTING_BASE}/v1/images", headers=headers, params=params ).json() for image_row in page["data"]: process(image_row) # your logic continuation = page.get("continuationToken") if not continuation: break params["ContinuationToken"] = continuation
# Persist the timestamp of the last row you processed,# and pass it as ModifiedSince on the next run.sync_images("2026-04-12T00:00:00Z")Performance notes
Section titled “Performance notes”- Narrow your window. For incremental sync, pass
ModifiedSinceclose to your last successful sync. For full backfills, prefer a pagedFromDateTime/ToDateTimewindow (e.g. month-by-month) over a single unbounded call. - Scope by project or company when you can.
ProjectIdandCompanyIddramatically narrow the underlying query. - Use
Sourcesto skip what you don’t need. If you only care about checklist images, passSources=checklist. - Respect the
ChunkLimit=2500default. Larger values (up to 10000) increase per-call payload and latency; smaller values increase round-trips. - Cache image bytes. URL patterns are stable for the lifetime of the file; use standard HTTP caching.
Known limitations (v1)
Section titled “Known limitations (v1)”deletedRecordCountis always0. Deletion tracking isn’t supported in v1; if a file is removed it simply stops appearing in future responses.ChunkLimitis counted in parent records, not image rows. A parent with multiple attached images contributes multiple rows.- EXIF data is not returned. Ditio does not persist camera make/model, orientation, or capture-time metadata beyond the upload timestamp and GPS. Width/height are likewise not stored — read the image bytes if you need them.
- No dedup across parents. The same underlying file referenced from two
parents (rare) yields two rows. Use
(id, collectionRef, idRef)as the row identity to dedup client-side. - Initial backfill for large tenants is the heaviest pattern this API
supports. Chunk your backfill with
ModifiedSince+ModifiedBeforewindows if you hit timeouts.
Error responses
Section titled “Error responses”| Status | When |
|---|---|
400 Bad Request | ModifiedBefore without ModifiedSince, ModifiedBefore <= ModifiedSince, or malformed ContinuationToken |
401 Unauthorized | Missing/expired bearer token |
403 Forbidden | Token valid but caller doesn’t have access to the requested ProjectId / CompanyId |
500 Internal Server Error | Unexpected failure — retry with backoff |
Related
Section titled “Related”- PDF Extraction — the PDFs Ditio generates for checklists, alerts, and absences
- Safety Reporting guide — checklists and incidents end-to-end
- Pagination — the shared continuation-token pattern
A ready-to-use Postman collection with the token request and example image-extraction calls is available on request — contact support@ditio.no.