Migration Guide: ICL Webhook → Connected Workers

This guide is for ICL / SAP integrators who push master data into Treedis. The master‑data sync is moving from the legacy ICL webhook (POST /v1/api/notes/icl/webhook, with a fixed SAP schema) to the generalized Connected Workers tables API (/v2/api/workspaces/{workspaceId}/cw/...), which models any master‑data table as columns → items under one of three entities — Locations, Assets (physical assets), or Lists (generic reference tables).

For notes, the saveThread workflow is the same — but if you call the public saveThread endpoint today, move to the authenticated one. See Notes: migrating saveThread below.

What's changing

Legacy (today) New (Connected Workers)
Endpoint POST /v1/api/notes/icl/webhook POST/PUT/GET /v2/api/workspaces/{workspaceId}/cw/{locations|assets|lists}/...
Shape Fixed SAP arrays (functionalLocations, equipment, workCentres, priorities, categories) Generic tables with your own columns and items, grouped by entity
Extensibility SAP fields only Any field/table you define
Auth X-API-Key OAuth2 Bearer (or X-API-Key) — see Authentication

The key idea: instead of a hard‑coded SAP payload, you describe your data once as tables and columns, then push rows as items. Functional locations go to the Locations entity, physical equipment to Assets, and every other lookup (work centers, priorities, categories) to generic Lists. SAP master data is just one thing they can represent.

The legacy approach (for reference)

You send one webhook call containing one or more arrays. Each object is upserted by its external ID (create or update).

curl -X POST "https://api.treedis.com/v1/api/notes/icl/webhook" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{
    "functionalLocations": [
      { "TPLNR": "FL-1001", "PLTXT": "Pump Station A", "EQART": "PUMP", "SWERK": "1000", "TPLMA": "FL-1000", "PARENTPLTXT": "Plant North" }
    ],
    "equipment": [
      { "EQUNR": "EQ-5001", "EQKTX": "Centrifugal Pump", "EQART": "PUMP", "TPLNR": "FL-1001" }
    ]
  }'

How the legacy SAP fields were interpreted:

Record type (array) SAP fields → internal meaning
functionalLocations TPLNR → external ID, PLTXT → description, EQART → type, SWERK → number, TPLMA → parent ID, PARENTPLTXT → parent description
equipment EQUNR → external ID + number, EQKTX → description, EQART → type, TPLNR → related functional location
workCentres PM_WKCTR → external ID, ARBPL → code, WERKS → site, LVORM → deletion flag, KTEXT → description
priorities PRIOK → external ID + code, PRIOKX → description, DIFF_START_MS → start time, DIFF_END_MS → end time
categories codegrp → external ID + code group, code → code, catalog → catalog, codegrp_txt → code‑group name, code_txt → code name

The new structure

Instead of a hard‑coded SAP payload, your data becomes tables → columns → rows under the entity that fits — Locations, Assets, or Lists (full schemas in the Locations, Assets, and Lists guides):

  • Table — one data set (e.g. Functional Locations), living under one entity.
  • Column — a field on the table (TPLNR, Name, …), with a type: text, number, date, checkbox, hierarchyParent, reference, locationTag, space, tagStatus.
  • Row (item) — one record: { "tplnr": "FL-1001", "name": "Pump Station A" }.
  SAP (one export, all plants)              Treedis — Connected Workers (workspace)
  ----------------------------              ---------------------------------------
  Functional Locations (table)     ----->   Locations  "Functional Locations"
    TPLNR . PLTXT . TPLMA . ...     ----->     Columns:  tplnr . name . parent . ...
    one record                     ----->     one Row:  { tplnr, name, parent, ... }

  Everything is workspace-owned — one set of tables per workspace, no
  per-project copies and no per-project filter. You push every row once.

Each legacy SAP array becomes a table under one entity, each record a row:

Legacy array -> Entity -> Table Primary‑key / title column
functionalLocations Locations (cw/locations) Functional Locations TPLNR
equipment Assets (cw/assets) Assets EQUNR
workCentres Lists (cw/lists) Work Centers PM_WKCTR / ARBPL
priorities Lists (cw/lists) Priorities PRIOK
categories Lists (cw/lists) Categories codegrp

Each SAP field becomes a column — for Functional Locations:

SAP field Column Type
TPLNR TPLNR text (primary key + title)
PLTXT Name text
EQART Object Type text
TPLMA / PARENTPLTXT Parent hierarchyParent

Relationships map onto column types: parent location (TPLMA) -> hierarchyParent; asset -> its functional location -> reference; status -> tagStatus; spatial tagging -> locationTag.

You don't create tables and columns one at a time — send everything in one batch call, shown next.

Integrator workflow (workspace level)

You integrate entirely at the workspace level using your own codes — there are no Treedis project IDs and nothing project‑specific to declare. You push every row once to the workspace; everything is workspace‑owned.

There are two calls.

1. Master data — batch insert tables

Send your master tables once as a workspace batch — every row of every table. Send functional locations to cw/locations, physical assets to cw/assets, and every other lookup to cw/lists. Each surface uses its own batch envelope key — locations, assets, and lists respectively.

columns is sent only the first time you create a table (or to add columns); once the table exists, later batches send just name + items and Treedis matches each value to the existing column by key.

POST /v2/api/workspaces/{workspaceId}/cw/locations/bulk
{
  "locations": [
    {
      "name": "Functional Locations",
      "items": [
        { "tplnr": "R400-20-FL-1001", "name": "Pump Station A" },
        { "tplnr": "R400-30-FL-2001", "name": "Compressor X" }
      ]
    }
  ]
}
POST /v2/api/workspaces/{workspaceId}/cw/assets/bulk
{
  "assets": [
    {
      "name": "Assets",
      "items": [
        { "equnr": "R400-20-EQ-900", "name": "Feed Pump" },
        { "equnr": "R400-20-EQ-901", "name": "Booster Pump" },
        { "equnr": "R400-30-EQ-110", "name": "Air Compressor" }
      ]
    }
  ]
}
POST /v2/api/workspaces/{workspaceId}/cw/lists/bulk
{
  "lists": [
    {
      "name": "Priorities",
      "items": [
        { "priok": "1", "name": "Emergency" },
        { "priok": "2", "name": "High" },
        { "priok": "3", "name": "Medium" },
        { "priok": "4", "name": "Low" }
      ]
    }
  ]
}

Prefer one endpoint for everything? POST /v2/api/workspaces/{workspaceId}/cw/bulk takes a tables array where each table carries its own type (location · asset · list) — so one call can push your whole export across all three surfaces at once. Equivalent to posting each table to its surface's own /bulk:

POST /v2/api/workspaces/{workspaceId}/cw/bulk
{
  "tables": [
    {
      "type": "location",
      "name": "Functional Locations",
      "items": [
        { "tplnr": "R400-20-FL-1001", "name": "Pump Station A" },
        { "tplnr": "R400-30-FL-2001", "name": "Compressor X" }
      ]
    },
    {
      "type": "asset",
      "name": "Assets",
      "items": [
        { "equnr": "R400-20-EQ-900", "name": "Feed Pump" },
        { "equnr": "R400-20-EQ-901", "name": "Booster Pump" }
      ]
    },
    {
      "type": "list",
      "name": "Priorities",
      "items": [
        { "priok": "1", "name": "Emergency" },
        { "priok": "2", "name": "High" },
        { "priok": "3", "name": "Medium" }
      ]
    }
  ]
}

Everything is read back from the workspace — GET /v2/api/workspaces/{workspaceId}/cw/locations/{table}/items and the assets / lists equivalents.

2. Adding notes

Create notes at the workspace, addressed to a location by its business key — no Treedis project id, nothing to configure per note. Send location (the location's primary‑key value — its name / code). Treedis finds that location and files the note against it. Optionally link an asset with assetKey (the asset table) + asset (that asset row's primary‑key value), and carry note content (title, description, status, priority, assignee, scheduledDate, dueDate, reporter, customFields). The note is sent as application/json; attachments go under files as base64 objects.

POST /v2/api/workspaces/7/notes
{
  "location": "R400-20-FL-1001",
  "assetKey": "assets",
  "asset": "R400-20-FL-1001-EQ",
  "title": "Pump leak",
  "description": "There is a leak in the pump, needs immediate attention.",
  "status": "open",
  "priority": "high",
  "assignee": "user123",
  "scheduledDate": "2026-06-23T10:00:00Z",
  "dueDate": "2026-06-25T17:00:00Z",
  "reporter": "user456",
  "customFields": { "severity": "3", "area": "north-yard" },
  "files": [
    { "filename": "photo.jpg", "contentType": "image/jpeg", "data": "/9j/4AAQSkZJRgABAQ..." }
  ]
}

So "note about functional location R400-20-FL-1001" → Treedis finds that location by its primary‑key value and files the note against it; the optional assetKey + asset pin it to a specific asset row too. Attachments may be multipart/form-data (binary files) or base64 application/json (each file under files as a base64 object { filename, contentType, data }, same as POST /v1/api/notes).

Notes: migrating saveThread to the authenticated API

Notes are created and updated with the saveThread and saveThreadMessage operations. Historically these were called on the public (unauthenticated, rate‑limited) endpoints:

  • POST /v1/public/saveThread
  • POST /v1/public/saveThreadMessage

The authenticated API exposes the same operations with the same request body under /v1/api/notes, secured with your OAuth2 token (or API key). Migrating is just two changes — change the path and add an Authorization header. The form fields don't change.

Operation Legacy (public) Authenticated (new)
Create / update a note POST /v1/public/saveThread POST /v1/api/notes
Add a message to a note POST /v1/public/saveThreadMessage POST /v1/api/notes/messages

saveThread → POST /v1/api/notes

Before — public saveThread (no auth):

curl -X POST "https://api.treedis.com/v1/public/saveThread" \
  -F 'title=Pump leak' \
  -F 'tourId=123' \
  -F 'tagId=456' \
  -F 'status=in_progress' \
  -F 'files=@photo.jpg'

After — authenticated (add the Authorization header, new path) and drop tourId:

curl -X POST "https://api.treedis.com/v1/api/notes" \
  -H "Authorization: Bearer your-access-token" \
  -F 'title=Pump leak' \
  -F 'tagId=456' \
  -F 'status=in_progress' \
  -F 'files=@photo.jpg'

Omit id to create a note; include id to update an existing one.

tourId is no longer used on saveThread — remove it from your payload. Attach the note to a project (projectId) and/or a tag (tagId); the tour is resolved from the tag when needed.

saveThreadMessage → POST /v1/api/notes/messages

Same change for messages — move off the public endpoint and add the header:

# Before:  POST https://api.treedis.com/v1/public/saveThreadMessage   (no auth)
# After:
curl -X POST "https://api.treedis.com/v1/api/notes/messages" \
  -H "Authorization: Bearer your-access-token" \
  -F 'threadId=789' \
  -F 'description=Replaced the seal; monitoring.' \
  -F 'files=@after.jpg'

Need a token? See the Authentication guide. The request bodies are identical to the public endpoints, so no field changes are required — only the URL and the added Authorization header.

Sending attachments as JSON + base64

multipart/form-data is still fully supported, but both saveThread and saveThreadMessage now also accept application/json — send each file under files as a base64 object { filename, contentType, data }. Use whichever your integration produces more naturally; the two transports create identical notes and enforce the same rules (image / audio / video / PDF only, up to 10 files, 20 MB per file).

curl -X POST "https://api.treedis.com/v1/api/notes" \
  -H "Authorization: Bearer your-access-token" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Pump leak",
    "tagId": 456,
    "status": "in_progress",
    "files": [
      { "filename": "photo.jpg", "contentType": "image/jpeg", "data": "/9j/4AAQSkZJRgABAQ..." }
    ]
  }'

data may be raw base64 or a full data URI (data:image/jpeg;base64,/9j/4AAQ...) — with a data URI, contentType is inferred when omitted. Omit files for a note with no attachments. Because the whole JSON body must fit the 20 MB request limit, prefer multipart/form-data for large attachments.

Both authenticated endpoints are in the API Reference under the Notes tag.

Migration checklist

Send master data as workspace batches

POST /v2/api/workspaces/{workspaceId}/cw/{locations|assets|lists}/bulk with { <entity>: [ { name, items } ] } (envelope key locations / assets / lists to match the surface) — your whole SAP export in one call per entity (columns only on first creation). Functional locations → cw/locations, physical assets → cw/assets, other lookups → cw/lists. One integration touching several surfaces can use the unified POST .../cw/bulk and tag each table with its own type instead.

Add notes at the workspace

POST /v2/api/workspaces/{workspaceId}/notes with location (a business key) instead of a Treedis project id — Treedis files the note against the matching location row (optionally link an asset with assetKey + asset).

Connected Workers Locations Full tables / columns / items reference — the anchor entity for worker features.