Skip to content

Documents

Push documents (PDF, Word, images, …) to Ditio projects and work orders. Documents you push surface in the Ditio Info Center, so field workers assigned to the project see them in the mobile app — grouped into a folder per project, and (optionally) into named sections within it. Typical use: a document management system that keeps drawings, contracts, and method statements in sync with the crews on site.

Scope: ditioapiv3 · Your API client must have Administrator access, and can only see and act on projects and work orders that belong to your company structure.

TermWhat it is
ProjectA top-level job in Ditio. Has a Ditio id (a string like 65f1a2b3c4d5e6f7a8b9c0d1) and a human project number (e.g. P-2026-001).
Work order (a task in the API)A unit of work inside a project. Also has a Ditio id.
DocumentAny file you push to a project or work order. It becomes a document on an Info Center page, visible to that project’s members on mobile.
SectionOptional name that groups a project’s documents into a page (e.g. Drawings, Contracts). Re-using the same (project, section) adds to the same page. Omit it and documents land on a default “Documents” page. A section can be a path — Dokumenter/Tillegg og endringer/Endringsmeldinger — to build folders inside the project’s folder (Step 7).
fileReference.idThe id Ditio returns when you push a document. Save it — you need it to download or delete that document later — or delete by section and file name instead, or use your own ids (Step 8).
pageExternalIdReturned on upload and list; the stable handle of the Info Center page the documents landed on.
pageIdDitio’s id of the Info Center page (or folder). Returned on upload, list, describe and section responses — use it, for example, to link to the page.
Your own ids (sectionId, fileId)Optional: address folders, pages and documents by the ids your own system already has (for example SharePoint item ids), so renames and moves follow (Step 8).

You push a document using the Ditio id of a project or work order — steps 2 and 3 below show how to find those ids — or, for a project, by its project number. Upload, list, describe, delete by name and the your-own-id endpoints all also exist under /api/v4/integration/projects/by-project-number/{projectNumber}/documents…, so you never need to look up or store Ditio’s project id. Download and delete by Ditio’s fileReferenceId take the project id.

Set the environment variables from Environments and get a token with scope=ditioapiv3 into $TOKEN (DITIO_SCOPE=ditioapiv3, the default), as shown in Getting a token in your scripts. Reuse the token across calls; see Token Expiry.

Terminal window
curl "$DITIO_API_BASE/api/v4/integration/projects" \
-H "Authorization: Bearer $TOKEN"

Or, if you already know the project number:

Terminal window
curl "$DITIO_API_BASE/api/v4/integration/projects/by-project-number/P-2026-001" \
-H "Authorization: Bearer $TOKEN"

The id in the response is what you attach documents to. More lookup endpoints: Projects.

Terminal window
curl "$DITIO_API_BASE/api/v4/integration/tasks/project/65f1a2b3c4d5e6f7a8b9c0d1" \
-H "Authorization: Bearer $TOKEN"

More work-order endpoints: Work Orders.

POST /api/v4/integration/projects/{id}/documents
POST /api/v4/integration/projects/by-project-number/{projectNumber}/documents

Send one or more files as multipart/form-data. The whole request body, all files together, can be at most about 30 MB (30 000 000 bytes); split larger batches over several requests. Query parameters (all optional):

ParamEffect
sectionGroups the documents onto a named page under the project’s document folder (e.g. Drawings). Re-use the same value to add more documents to that page. Omit it → a default “Documents” page. Separate levels with / to put the page inside folders — see Step 7.
replaceExistingFilesWithSameNameWhen true, documents already on the page with the same filename are replaced (use on re-sync to avoid duplicates).
sectionIdInstead of section: your own id of a page created in Step 8. Sending both section and sectionId is rejected with 400.
fileIdYour own id for the document. Upload exactly one file with it (400 otherwise). Uploading the same fileId again replaces the document, wherever on the project it was.
Terminal window
curl -X POST "$DITIO_API_BASE/api/v4/integration/projects/65f1a2b3c4d5e6f7a8b9c0d1/documents?section=Drawings&replaceExistingFilesWithSameName=true" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/Drawing-A1.pdf" \
-F "file=@/path/to/Site-plan.docx"

Response:

{
"successful": true,
"message": "Documents uploaded and attached ok",
"section": "Drawings",
"pageExternalId": "int-projdocs:65f1a2b3c4d5e6f7a8b9c0d1:drawings",
"pageId": "66c1a2b3c4d5e6f7a8b9c0d3",
"files": [
{
"name": "Drawing-A1.pdf",
"size": 482113,
"url": "/api/file/66b1a2c3d4e5f6a7b8c9d0e2",
"fileReference": {
"id": "66b1a2c3d4e5f6a7b8c9d0e2",
"collectionRef": "InfoMessage",
"fileNameOrig": "Drawing-A1.pdf",
"fileType": "pdf",
"fileSize": 482113,
"createdDateTime": "2026-06-05T08:10:25Z"
}
}
],
"infoPageFiles": [
{ "fileReferenceId": "66b1a2c3d4e5f6a7b8c9d0e2", "name": "Drawing-A1.pdf" }
]
}

files are the documents created by this request; infoPageFiles is everything on the page after the upload. Store files[].fileReference.id — it’s the handle for downloading or deleting a document later. pageId is Ditio’s id of the page the documents landed on, for example to link to it.

files[].url is relative to the Core API base; fetch it with your bearer token. Without an accepted credential it returns 404 and no bytes, as it does for a file that no longer exists. Responses are private, no-store.

Change (October 2026): files[].url used to answer without credentials and allowed public caching for 14 days. It now needs an accepted bearer token, returns private, no-store, and returns 404 for a missing file. Migration: send the Authorization: Bearer header you already use for the upload on every download, and store downloaded bytes yourself instead of relying on HTTP or CDN caches.

Identical, using the work order id, and it takes the same section and replaceExistingFilesWithSameName query parameters. The work order’s documents nest under its parent project’s folder; when you omit section it defaults to the work order’s number/short description. On a work order, section is always one level: a / in it is part of the page name. Section paths, sectionId and fileId are available on project uploads only. The response has the same shape, including pageExternalId and pageId.

Terminal window
curl -X POST "$DITIO_API_BASE/api/v4/integration/tasks/66a1b2c3d4e5f6a7b8c9d0e2/documents" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/Method-statement.pdf"

Behind the scenes, Ditio routes each document into the project’s Info Center — creating a folder per project and a page per section. The durable handle is (project, section); re-syncing to the same handle updates the same page rather than duplicating it:

📁 P-2026-001 – Prosjekt Fjellhall (folder, one per project)
├── 📑 Drawings (section page)
│ ├── 📎 Drawing-A1.pdf
│ └── 📎 Site-plan.docx
├── 📑 Contracts (section page)
│ └── 📎 Main-contract.pdf
└── 📑 Documents (default page — no section given)
└── 📎 Misc.pdf

The project folder is named “{project number} – {project name}” and follows the project when its number or name changes. A folder an admin has renamed by hand in Ditio Web keeps that name.

Each project folder/page is scoped to that project, so it’s shown to active employees assigned to the project in your company. To put pages inside folders of their own, send section as a path (Step 7) or build the tree with your own ids (Step 8).

Step 5 — List, download, replace, delete

Section titled “Step 5 — List, download, replace, delete”

List the document pages on a project or work order:

GET /api/v4/integration/projects/{id}/documents
GET /api/v4/integration/projects/by-project-number/{projectNumber}/documents
GET /api/v4/integration/tasks/{id}/documents

Returns the section pages at every depth — each with section (its full path, e.g. Dokumenter/Tillegg og endringer/Endringsmeldinger), sectionId (your id, when the page was created with one; otherwise null), pageExternalId, pageId (Ditio’s id of the Info Center page, for example to link to it), and a files array of { fileReferenceId, name, fileId } (fileId is your id, when the document was uploaded with one). For a work order, it returns the work order’s own section page; its pageId is null while that page does not exist yet.

The path is built from the titles of the levels, so a title that itself contains / (renamed in Ditio Web, or a work order’s name) reads as an extra level — use sectionId or pageExternalId when you need an exact handle.

[
{
"section": "Dokumenter/Tegninger",
"sectionId": null,
"pageExternalId": "int-projdocs:65f1a2b3c4d5e6f7a8b9c0d1:dokumenter/tegninger",
"pageId": "66c1a2b3c4d5e6f7a8b9c0d5",
"files": [
{ "fileReferenceId": "66b1a2c3d4e5f6a7b8c9d0e3", "name": "Plan-A1.pdf", "fileId": null }
]
},
{
"section": "Dokumenter/Tillegg og endringer/Endringsmeldinger",
"sectionId": "sp-meldinger",
"pageExternalId": "int-projdocs:65f1a2b3c4d5e6f7a8b9c0d1:id:sp-meldinger",
"pageId": "66c1a2b3c4d5e6f7a8b9c0d6",
"files": [
{ "fileReferenceId": "66b1a2c3d4e5f6a7b8c9d0e4", "name": "Endringsmelding - 01.docx", "fileId": "sp-file-0042" }
]
}
]

Above, the first page was created by a section path (Step 7) and the second with your own ids (Step 8).

Download a document (use the fileReferenceId from upload/list):

GET /api/v4/integration/projects/{id}/documents/{fileReferenceId}
GET /api/v4/integration/tasks/{id}/documents/{fileReferenceId}

The endpoint streams the document bytes directly as the response body (200 OK, Content-Type: application/octet-stream, Content-Disposition carrying the original filename) — there is no JSON body:

Terminal window
curl "$DITIO_API_BASE/api/v4/integration/projects/{id}/documents/{fileReferenceId}" \
-H "Authorization: Bearer $TOKEN" -o Drawing-A1.pdf

A 404 means the document is not on that project/work order.

Replace — re-upload to the same (project, section) with ?replaceExistingFilesWithSameName=true (matches by filename).

Delete:

DELETE /api/v4/integration/projects/{id}/documents/{fileReferenceId}
DELETE /api/v4/integration/tasks/{id}/documents/{fileReferenceId}

Returns 204 No Content.

Delete by section and file name — no Ditio id needed:

DELETE /api/v4/integration/projects/{id}/documents?section=Dokumenter/Tegninger&fileName=Plan-A1.pdf
DELETE /api/v4/integration/projects/by-project-number/{projectNumber}/documents?section=Dokumenter/Tegninger&fileName=Plan-A1.pdf
Terminal window
curl -X DELETE "$DITIO_API_BASE/api/v4/integration/projects/by-project-number/P-2026-001/documents?section=Dokumenter/Tegninger&fileName=Plan-A1.pdf" \
-H "Authorization: Bearer $TOKEN"

Removes every document with that name on that page (the name matches case-insensitively, like replaceExistingFilesWithSameName). Returns 204, or 404 when the section or the document is not there. Omit section for the default “Documents” page.

Deleting through these endpoints removes both the file and its entry on the page.

Set the guidance text a field worker sees on the project’s document folder, or on one of its section pages. It is shown above the contents, and in the level above as a one-line summary under the name — so someone can tell what a folder covers before opening it.

PATCH /api/v4/integration/projects/{id}/documents
PATCH /api/v4/integration/projects/{id}/documents?section=Drawings
PATCH /api/v4/integration/projects/by-project-number/{projectNumber}/documents?section=Drawings

Omit section to describe the project’s document folder; name one to describe that section page. Work-order documents live as sections under the same project folder, so pass the work order’s section name here rather than looking for a separate work-order endpoint.

Terminal window
curl -X PATCH "$DITIO_API_BASE/api/v4/integration/projects/65f1a2b3c4d5e6f7a8b9c0d1/documents?section=Drawings" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"description": "Approved drawings only.\nSuperseded revisions are moved to Archive."}'

Response:

{
"externalId": "int-projdocs:65f1a2b3c4d5e6f7a8b9c0d1:drawings",
"pageId": "66c1a2b3c4d5e6f7a8b9c0d3",
"title": "Drawings",
"description": "Approved drawings only.\nSuperseded revisions are moved to Archive."
}

externalId is the stable handle of the folder or page you described, and pageId is Ditio’s id of it, for example to link to it.

  • Send plain text. Each line becomes a paragraph. Markup is shown as literal characters, not interpreted, so you cannot inject formatting or HTML.
  • The folder or section must already exist — push a document to it first (Step 4). A 404 means it does not.
  • Send null or "" to clear it.
  • Limits: up to 10 000 characters, and a 64 KB request body. Longer text is rejected with 400.
  • Any level of a path can be described: ?section=Dokumenter/Tillegg og endringer describes that folder, ?section=Dokumenter/Tillegg og endringer/Endringsmeldinger that page.

Step 7 — Folders inside the project (section paths)

Section titled “Step 7 — Folders inside the project (section paths)”

Mirror a folder structure by sending the folders as a path in section, levels separated by /:

Terminal window
curl -X POST "$DITIO_API_BASE/api/v4/integration/projects/by-project-number/P-2026-001/documents?section=Dokumenter/Tillegg%20og%20endringer/Endringsmeldinger" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/Endringsmelding - 01.docx"

Ditio creates whatever is missing and reuses what exists:

📁 P-2026-001 – Prosjekt Fjellhall (project folder)
└── 📁 Dokumenter (folder)
└── 📁 Tillegg og endringer (folder)
└── 📑 Endringsmeldinger (page — the last level)
└── 📎 Endringsmelding - 01.docx
  • Every level but the last is a folder; the last is the page that holds the documents. A / in section always separates levels, so section=A/B is folder A with page B, and a level’s name cannot itself contain /.
  • A level holds either documents or sub-sections. A folder can contain both folders and pages, but documents sit on pages. So a path through a level that already holds documents, or ending at a level that holds sub-sections, is rejected with 409 — put those documents on a page one level down, for example Dokumenter/Tillegg og endringer/Dokumenter.
  • Levels are trimmed and blank ones ignored — Dokumenter / Tegninger and /Dokumenter//Tegninger/ are the same path. Matching is by name, ignoring case and punctuation (Tegninger og modeller = tegninger-og-modeller).
  • At most 10 levels. Deeper paths are rejected with 400.
  • A one-level section is a page directly in the project’s folder — section=Drawings is the Drawings page.
  • Renaming a folder in your system means a new path, which Ditio sees as a new folder. If you need renames and moves to follow, use your own ids (Step 8).

Step 8 — Mirror your folder tree with your own ids

Section titled “Step 8 — Mirror your folder tree with your own ids”

If your system gives every folder and file a stable id — SharePoint does, through Microsoft Graph — address them by those ids. Then a rename or a move is just the same id with a new name or parent, and nothing has to be re-uploaded. You never store a Ditio id.

Create or update a section (a folder, or a page that holds documents):

PUT /api/v4/integration/projects/{id}/documents/sections/{sectionId}
PUT /api/v4/integration/projects/by-project-number/{projectNumber}/documents/sections/{sectionId}
Terminal window
# Folders: Dokumenter → Tillegg og endringer
curl -X PUT "$DITIO_API_BASE/api/v4/integration/projects/by-project-number/P-2026-001/documents/sections/sp-dokumenter" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name": "Dokumenter", "isFolder": true}'
curl -X PUT "$DITIO_API_BASE/api/v4/integration/projects/by-project-number/P-2026-001/documents/sections/sp-tillegg" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name": "Tillegg og endringer", "parentSectionId": "sp-dokumenter", "isFolder": true, "description": "Change orders and change notices"}'
# Page that holds documents
curl -X PUT "$DITIO_API_BASE/api/v4/integration/projects/by-project-number/P-2026-001/documents/sections/sp-meldinger" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name": "Endringsmeldinger", "parentSectionId": "sp-tillegg"}'
Field
nameRequired (400 when missing). The name shown in the app.
parentSectionIdYour id of the parent folder. Omit it to place the section directly in the project’s folder. The parent must already exist (404) and be a folder (409).
isFoldertrue for a folder (holds sections), false/omitted for a page (holds documents). A section cannot change kind (409) — delete and recreate it.
descriptionOptional guidance text, as in Step 6. Omit it to leave the current text; "" clears it.

Returns { sectionId, pageId, name, parentSectionId, isFolder, description }, where pageId is Ditio’s id of the folder or page, for example to link to it. Sending the same sectionId again with a new name or parentSectionId renames or moves the section, with everything in it. A move into the section’s own subtree is rejected with 400.

Upload to it, with your file id:

Terminal window
curl -X POST "$DITIO_API_BASE/api/v4/integration/projects/by-project-number/P-2026-001/documents?sectionId=sp-meldinger&fileId=sp-file-0042" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@/path/to/Endringsmelding - 01.docx"

Upload exactly one file when you pass fileId. Uploading the same fileId again — a new version, or the file moved to another folder — replaces the document wherever it was on the project. Uploading to a sectionId that does not exist returns 404, and to a folder’s sectionId 409. The response’s section is then the page’s name.

Delete:

DELETE /api/v4/integration/projects/{id}/documents/files/{fileId}
DELETE /api/v4/integration/projects/{id}/documents/sections/{sectionId}

The first deletes the document; the second deletes the section, everything in it, and their files. Both return 204, or 404 when there is nothing with that id, and both also exist under by-project-number/{projectNumber}:

Terminal window
curl -X DELETE "$DITIO_API_BASE/api/v4/integration/projects/by-project-number/P-2026-001/documents/files/sp-file-0042" \
-H "Authorization: Bearer $TOKEN"
curl -X DELETE "$DITIO_API_BASE/api/v4/integration/projects/by-project-number/P-2026-001/documents/sections/sp-dokumenter" \
-H "Authorization: Bearer $TOKEN"

Section ids and section paths are separate: a page you create by id is never reached by a path, and the other way round.

using System.Net.Http.Headers;
var apiBase = Environment.GetEnvironmentVariable("DITIO_API_BASE")
?? "https://core-api.ditio.dev/core";
using var http = new HttpClient();
// DitioAuth.GetTokenAsync: see /getting-started/authentication/#token-expiry
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", await DitioAuth.GetTokenAsync(http));
using var form = new MultipartFormDataContent();
var pdf = new ByteArrayContent(await File.ReadAllBytesAsync("Drawing-A1.pdf"));
pdf.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(pdf, "file", "Drawing-A1.pdf");
var projectId = "65f1a2b3c4d5e6f7a8b9c0d1";
var response = await http.PostAsync(
$"{apiBase}/api/v4/integration/projects/{projectId}/documents" +
"?section=Drawings&replaceExistingFilesWithSameName=true",
form);
response.EnsureSuccessStatusCode();
  • Auth: Administrator-level API client. For 401s see Error Handling.
  • Company-scoped: you only see and act on projects/work orders that belong to your company structure. An unknown id returns 404; a project or work order outside your company structure returns 400.
  • Upload size: about 30 MB (30 000 000 bytes) per request, all files together. A larger body is refused with 413 Payload Too Large.
  • Uploads must be multipart/form-data. Any other content type returns 415 Unsupported Media Type, and a request with no Content-Type header returns 500. Don’t retry either; fix the request.
  • Visibility: documents are shown to active employees assigned to the project, in the mobile app’s Info Center.
  • Grouping is by section. There is no separate version concept; replaceExistingFilesWithSameName matches by filename, and fileId matches by your id.
  • Section paths: at most 10 levels (400 when deeper); a folder/page kind conflict is 409.
  • Any file type is accepted (PDF, Word, images, …).
  • Dates are ISO 8601; IDs are strings. Your own sectionId/fileId can be any string up to 256 characters.
  • Project numbers resolve within your own company. A number shared by several projects resolves to the one active project; if several match and none or more than one is active, the call is rejected with 400. An unknown number is 404.

A ready-to-run Postman collection covering the integration endpoints — including these document endpoints, the token request, and project/work-order lookups — lives in the public samples repo: ditioas/Ditio.Core.Samples.Integrations (the postman/ folder: collection + Production/Test environment files).

  • Projects · Work Orders — finding the ids you upload to
  • Safety Reporting — archiving the PDFs Ditio generates for checklists, incidents, and absences (each record’s pdfUrl, the reverse direction)
  • Authentication — token flow and caching