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.
Concepts
Section titled “Concepts”| Term | What it is |
|---|---|
| Project | A 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. |
| Document | Any 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. |
| Section | Optional 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.id | The 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). |
pageExternalId | Returned on upload and list; the stable handle of the Info Center page the documents landed on. |
pageId | Ditio’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.
Step 1 — Get a token
Section titled “Step 1 — Get a token”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.
Step 2 — Find your project’s id
Section titled “Step 2 — Find your project’s id”curl "$DITIO_API_BASE/api/v4/integration/projects" \ -H "Authorization: Bearer $TOKEN"Or, if you already know the project number:
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.
Step 3 — Find work orders in a project
Section titled “Step 3 — Find work orders in a project”curl "$DITIO_API_BASE/api/v4/integration/tasks/project/65f1a2b3c4d5e6f7a8b9c0d1" \ -H "Authorization: Bearer $TOKEN"More work-order endpoints: Work Orders.
Step 4 — Upload documents
Section titled “Step 4 — Upload documents”To a project
Section titled “To a project”POST /api/v4/integration/projects/{id}/documentsPOST /api/v4/integration/projects/by-project-number/{projectNumber}/documentsSend 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):
| Param | Effect |
|---|---|
section | Groups 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. |
replaceExistingFilesWithSameName | When true, documents already on the page with the same filename are replaced (use on re-sync to avoid duplicates). |
sectionId | Instead of section: your own id of a page created in Step 8. Sending both section and sectionId is rejected with 400. |
fileId | Your 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. |
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[].urlused to answer without credentials and allowed public caching for 14 days. It now needs an accepted bearer token, returnsprivate, no-store, and returns404for a missing file. Migration: send theAuthorization: Bearerheader you already use for the upload on every download, and store downloaded bytes yourself instead of relying on HTTP or CDN caches.
To a work order
Section titled “To a work order”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.
curl -X POST "$DITIO_API_BASE/api/v4/integration/tasks/66a1b2c3d4e5f6a7b8c9d0e2/documents" \ -H "Authorization: Bearer $TOKEN" \ -F "file=@/path/to/Method-statement.pdf"Where documents land
Section titled “Where documents land”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.pdfThe 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}/documentsGET /api/v4/integration/projects/by-project-number/{projectNumber}/documentsGET /api/v4/integration/tasks/{id}/documentsReturns 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:
curl "$DITIO_API_BASE/api/v4/integration/projects/{id}/documents/{fileReferenceId}" \ -H "Authorization: Bearer $TOKEN" -o Drawing-A1.pdfA 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.pdfDELETE /api/v4/integration/projects/by-project-number/{projectNumber}/documents?section=Dokumenter/Tegninger&fileName=Plan-A1.pdfcurl -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.
Step 6 — Describe a folder or section
Section titled “Step 6 — Describe a folder or section”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}/documentsPATCH /api/v4/integration/projects/{id}/documents?section=DrawingsPATCH /api/v4/integration/projects/by-project-number/{projectNumber}/documents?section=DrawingsOmit 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.
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
404means it does not. - Send
nullor""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 endringerdescribes that folder,?section=Dokumenter/Tillegg og endringer/Endringsmeldingerthat 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 /:
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
/insectionalways separates levels, sosection=A/Bis folderAwith pageB, 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 exampleDokumenter/Tillegg og endringer/Dokumenter. - Levels are trimmed and blank ones ignored —
Dokumenter / Tegningerand/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=Drawingsis theDrawingspage. - 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}# Folders: Dokumenter → Tillegg og endringercurl -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 documentscurl -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 | |
|---|---|
name | Required (400 when missing). The name shown in the app. |
parentSectionId | Your 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). |
isFolder | true for a folder (holds sections), false/omitted for a page (holds documents). A section cannot change kind (409) — delete and recreate it. |
description | Optional 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:
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}:
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.
C# example — upload with a cached token
Section titled “C# example — upload with a cached token”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-expiryhttp.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();Limitations & notes
Section titled “Limitations & notes”- 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 returns400. - 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 returns415 Unsupported Media Type, and a request with noContent-Typeheader returns500. 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;replaceExistingFilesWithSameNamematches by filename, andfileIdmatches by your id. - Section paths: at most 10 levels (
400when deeper); a folder/page kind conflict is409. - Any file type is accepted (PDF, Word, images, …).
- Dates are ISO 8601; IDs are strings. Your own
sectionId/fileIdcan 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 is404.
Postman
Section titled “Postman”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).
Related
Section titled “Related”- 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