Gå til indholdet

API

Machine-to-machine API for integrating other municipal systems with OpenCase. Base URL: /index.php/apps/opencase/public/v1/api.

A companion JSON schema gives the exact JSON Schema for every request body and response shown below; $ref it from validators, codegen, or an IDE's JSON-schema-backed autocomplete.

Content negotiation: JSON by default; send Accept: application/xml for XML instead XML schema, namespace https://opencase.dk/case). Request bodies may be JSON or XML (detected from Content-Type); the field names are identical either way — XML wraps them in an element matching the response's root element (e.g. <Case><Title>...</Title></Case>).

⚠️ Every scalar field — including Id, StatusId, and every other numeric-looking field — is serialized as a JSON string, not a JSON number, and every boolean field (e.g. HasAddressProtection) is the string "true"/"false", not a JSON boolean. This falls out of the field builders in CaseExportService being shared verbatim between the XML export and the JSON public API. The exception is the ad-hoc Id/PriorCanWrite fields returned inline by addParticipants/addCaseworkers/addContacts (see below), which are native JSON numbers — the schema flags each field's actual type explicitly, so check it rather than assuming.

Fields that are null/absent are omitted from XML but present as JSON null in the JSON response.

Errors: any endpoint returns { "error": "<message>" } (or an XML <Error> element) with a 4xx/5xx status on failure. Bulk endpoints (addParticipants, addCaseworkers, addContacts) instead return HTTP 200/201 with a per-item result list, where a failed item carries an Error field instead of the created record — a single bad entry never fails the whole request; see each endpoint below.

Authentication

Access to the OpenCase API is secured using token-based authentication based on the Danish Common Municipal Service Platform (Serviceplatformen) and follows the SF1512 Security Token Service (STS) specification.

Clients must obtain a valid security token from SF1512 before invoking any API endpoint.


Prerequisites

Before your application can authenticate against the OpenCase API, the following prerequisites must be fulfilled:

  • A valid Service Agreement (Serviceaftale) for the OpenCase API must be established in Fælleskommunal Administration.
  • The calling system must be configured to authenticate against Serviceplatformen SF1512.
  • The client must possess the required certificates and credentials for requesting security tokens from the Security Token Service (STS).

Obtaining an access token

Request a security token from Serviceplatformen SF1512 for the following Entity ID:

http://opencase.dk/service/api/1

The issued SAML token represents the authenticated client and is used to authorize subsequent API requests.


Calling the API

Include the issued token in the HTTP Authorization header using the Bearer authentication scheme.

Authorization: Bearer <Base64-encoded token>

The token must be Base64 encoded before it is included in the request header.

Example:

GET /index.php/apps/opencase/public/v1/api/cases HTTP/1.1
Host: api.example.dk
Authorization: Bearer PHNhbWwyOkFzc2VydGlvbj4uLi48L3NhbWwyOkFzc2VydGlvbj4=

Token validation

For each request, OpenCase validates:

  • the authenticity of the SAML token
  • the token signature
  • the issuing Security Token Service
  • token validity period
  • the requested audience (http://opencase.dk/service/api/1)

Requests containing an invalid, expired or incorrectly issued token are rejected with an authentication error.

Note

The public key of your certificate must be registered in OpenCase for validation.


Authentication flow

The authentication process consists of the following steps:

  1. The client requests a SAML token from Serviceplatformen SF1512.
  2. SF1512 issues a signed security token for the OpenCase API.
  3. The client Base64-encodes the token.
  4. The token is included in the Authorization header as a Bearer token.
  5. OpenCase validates the token before processing the request.
Client
   │
   │ Request token
   ▼
Serviceplatformen SF1512
   │
   │ SAML Token
   ▼
Client
   │
   │ Authorization: Bearer <Base64 token>
   ▼
OpenCase API

API Functions

JSON schema XML schema

Cases — PublicCaseApiController

Method & path Purpose
GET /cases?uuid=... or ?case_number=... Get a single case (CaseType minus Documents/JournalNotes)
GET /cases/search?... Search cases by metadata
POST /cases Create a case
PUT /cases Update a case's metadata
GET /cases/documents?uuid=... or ?case_number=... List a case's documents
POST /cases/documents Create a document on a case (metadata only, no file)
GET /cases/journal-notes?uuid=... or ?case_number=... List a case's journal notes
POST /cases/journal-notes Add a journal note to a case
POST /cases/participants Add one or more participants to a case
POST /cases/caseworkers Add one or more additional caseworkers to a case

Get a case

GET /cases?uuid=8f14e...
GET /cases?case_number=2024-001234

Exactly one of uuid/case_number is required. Response: { "Case": CaseType } (schema: #/$defs/CaseType), with Participants/Caseworkers nested but not Documents/JournalNotes (use the endpoints below for those).

Search cases

GET /cases/search?organisation=Børn+og+Unge&year=2024&search=vandløb&limit=50&offset=0

Query parameters (all optional): search, organisation, year, status_id, classification_code, sensitivity_key, classification_facet_uuid, insight_level_id, responsible_user_id, casetype_id, limit (default 50, max 1000), offset. Response: { "CaseSearchResult": { "Total", "Limit", "Offset", "Cases": [CaseType, ...] } }.

Create a case

POST /cases
{
  "Title": "Tilsyn med dagtilbud",
  "OrgUuid": "5a1e...",
  "ClassificationCodeUuid": "27.69.00-uuid",
  "SensitivityUuid": "intern-uuid",
  "ClassificationFacetUuid": "facet-uuid",
  "ResponsibleUserId": "jane",
  "InsightLevelId": 1,
  "CasetypeId": 2,
  "ParentCaseId": null,
  "Summary": "<p>Rich-text summary</p>"
}

Title, ClassificationFacetUuid, and one of each {OrgUuid | organisation}, {ClassificationCodeUuid | classification_code}, {SensitivityUuid | sensitivity} are required — the Uuid variants resolve via GET /organisations, /kle-numbers, /sensitivities; the plain variants take the name/code/key directly. ResponsibleUserId, InsightLevelId, CasetypeId, ParentCaseId, Summary are optional. Lowercase/snake_case field names are also accepted throughout. Response (201): { "Case": CaseType }.

Update a case

PUT /cases
{ "Uuid": "8f14e...", "Title": "Updated title", "SensitivityUuid": "fortrolig-uuid" }

Uuid or CaseNumber is required to identify the case; any subset of the Create fields may follow — omitted fields are left unchanged. ClassificationFacetUuid, InsightLevelId, Summary can be explicitly cleared by sending them as an empty string. Response: { "Case": CaseType }.

List / create a case's documents

GET  /cases/documents?case_number=2024-001234

Response: { "Documents": { "Document": [DocumentType, ...] } } in XML, or { "Documents": [DocumentType, ...] } in JSON (all list wrappers follow this shape — see public-api.schema.json for the exact JSON form).

POST /cases/documents
{ "CaseNumber": "2024-001234", "Title": "Afgørelse", "DocumentCategoryId": 3 }

Uuid/CaseNumber (one of) and Title, DocumentCategoryId are required; DocumentType, InsightLevelId, DocumentDate, ReceivedDate, RegisteredDate are optional. Creates metadata only — no attached file. Response (201): { "Document": DocumentType } (with empty Files/Contacts/Notes/WorkflowHistory).

List / add a case's journal notes

GET /cases/journal-notes?uuid=8f14e...

Response: { "JournalNotes": [NoteType, ...] }.

POST /cases/journal-notes
{ "Uuid": "8f14e...", "Title": "Telefonopkald", "Text": "Ringede til borger ..." }

Uuid/CaseNumber (one of) and Title are required; Text optional. Response (201): { "JournalNote": NoteType }.

Add participants to a case

POST /cases/participants
{
  "CaseNumber": "2024-001234",
  "Participants": [
    { "ParticipantRoleId": 1, "CprCvr": "0101011234" }
  ]
}

Uuid/CaseNumber (one of); Participants is a list of {ParticipantRoleId, CprCvr} (role: see GET /participantroles; CprCvr: 10 digits → CPR lookup via Datafordeler, 8 digits → CVR lookup — name and address come back from that lookup, not the request). Requires write access to the case. Each entry is resolved independently; the response is a per-entry result list (status 201 if at least one succeeded, else 400):

{ "Participants": [
  { "Id": 12, "RoleId": 1, "RoleName": "Part", "ContactType": 1,
    "CprCvr": "0101011234", "Name": "Jane Citizen", "...": "...",
    "HasAddressProtection": "false" }
] }

An entry that failed (bad role/CPR-CVR, lookup miss, duplicate) has { "CprCvr": "...", "Error": "..." } instead. Note Id/RoleId/ContactType here are native JSON numbers, unlike the string-typed fields elsewhere in this API — see #/$defs/ParticipantAddResult in the schema.

Add caseworkers to a case

POST /cases/caseworkers
{ "CaseNumber": "2024-001234", "Caseworkers": [ { "UserId": "jane" } ] }

Uuid/CaseNumber (one of); Caseworkers is a list of {UserId} (a Nextcloud user id). Requires write access to the case. Resolved independently per entry, same 201/400 + per-item Error pattern as participants above. Granting write access to a user who previously had less (or no) access records their prior level in PriorCanWrite so it can be restored if the caseworker assignment is later removed.

Documents — PublicDocumentApiController

Method & path Purpose
GET /documents?uuid=... or ?document_number=... Get a single document (DocumentType, with Files/Contacts/Notes/WorkflowHistory nested)
GET /documents/search?... Search documents by metadata
PUT /documents Update a document's metadata
POST /documents/contacts Add one or more contacts to a document
POST /documents/files Upload a file to a document (base64 body)
POST /documents/files/from-template Create a file by merging a template
POST /documents/notes Add a note to a document

Get a document

GET /documents?document_number=2024-001234-3

Response: { "Document": DocumentType }.

Search documents

GET /documents/search?title=afgørelse&status=2&limit=50&offset=0

Query parameters (all optional): title, document_type, status, date_from, date_to, org_name, document_category_id, insight_level_id, created_by, limit (default 50, max 1000), offset. Response: { "DocumentSearchResult": { "Total", "Limit", "Offset", "Documents": [{Id, Uuid, DocumentNumber, Title, DocumentType, Status, DocumentDate, ReceivedDate, CreatedAt, UpdatedAt, CreatedBy, CaseId, CaseNumber, CaseTitle, OrgName}, ...] } } — note this is a flatter, search-result-specific shape, not the full DocumentType.

Update a document

PUT /documents
{ "Uuid": "9c2b...", "Status": 3 }

Uuid/DocumentNumber (one of) required; any subset of Title, DocumentType, Status (1=draft/active, 2=?, 3=final — see GET /documentstatus for the active code list), InsightLevelId, DocumentDate, ReceivedDate, RegisteredDate may follow. Omitted fields are unchanged; InsightLevelId/DocumentDate/ReceivedDate/RegisteredDate can be cleared by sending them empty. Setting a final status locks the document's files read-only (matches the internal UI). Response: { "Document": DocumentType }.

Add a note to a document

POST /documents/notes
{ "Uuid": "9c2b...", "Title": "Kvalitetssikret", "Text": "..." }

Uuid/DocumentNumber (one of) and Title required, Text optional. Requires write access to the document's case. Response (201): { "Note": NoteType }.

Add contacts to a document

POST /documents/contacts
{
  "DocumentNumber": "2024-001234-3",
  "Contacts": [ { "ContactRoleId": 2, "CprCvr": "12345678" } ]
}

Same shape/semantics as POST /cases/participants above, but for a document's sender/receiver contacts (ContactRoleId: see GET /contactroles). Requires write access to the document's case. Response: { "Contacts": [ContactAddResult, ...] }, 201/400 depending on whether any entry succeeded.

Upload a file to a document

POST /documents/files
{ "DocumentNumber": "2024-001234-3", "FileName": "afgørelse.pdf", "Content": "<base64>", "MimeType": "application/pdf" }

Uuid/DocumentNumber (one of), FileName, Content (base64) required; MimeType optional (default application/octet-stream). There is no multipart/form-data upload on this API — content is base64 in the JSON/XML body. Response (201): { "File": FileType }.

Create a file from a template

POST /documents/files/from-template
{ "DocumentNumber": "2024-001234-3", "TemplateId": 7 }

Uuid/DocumentNumber (one of) and TemplateId (see GET /templates) required. Merges the template with case/document metadata (e.g. {{case.number}}, {{sag.titel}}) and attaches the result as a new file. Response (201): { "File": FileType }.

Files — PublicFileApiController

Method & path Purpose
GET /files?uuid=... Get a file's metadata + base64 content
POST /files/version Upload a new version of an existing file
GET /files/versions?uuid=... List a file's historical (superseded) versions
GET /files/versions/content?uuid=... Get a historical version's metadata + base64 content

Get a file

GET /files?uuid=3af0...

Response: { "File": FileType } with an added Content field (base64 of the current/live content).

Upload a new version

POST /files/version
{ "Uuid": "3af0...", "Content": "<base64>" }

Keeps the existing filename/MIME type; increments Version. Response: { "File": FileType } (updated).

List historical versions

GET /files/versions?uuid=3af0...

Response: { "FileVersions": [FileVersionType, ...] }, newest first. Only superseded versions — the current content is not included; use GET /files?uuid=... for that.

Get a historical version's content

GET /files/versions/content?uuid=<fileversion-uuid>

Note this uuid identifies the file version row, not the file. Response: { "FileVersion": FileVersionType } with an added Content field (base64).

Reference data — PublicReferenceApiController

Code lists and other lookup values referenced by the endpoints above (e.g. StatusId, DocumentCategoryId). All accept an optional ?lang= (default da, falling back to en if the requested language has no rows) and return only active/non-expired entries.

Method & path Response
GET /casestatus { "CaseStatuses": [{Id, Name, IsClosed, Expired}, ...] }
GET /casetype { "CaseTypes": [{Id, Name, PrimaryParticipant, Expired}, ...] }
GET /contactroles { "ContactRoles": [{Id, Name, Expired}, ...] }
GET /documentcategory { "DocumentCategories": [{Id, Name, Expired}, ...] }
GET /documentstatus { "DocumentStatuses": [{Id, Name, IsFinal, Expired}, ...] }
GET /insightlevel { "InsightLevels": [{Id, Name, Description}, ...] }
GET /participantroles { "ParticipantRoles": [{Id, Name, Expired}, ...] }
GET /organisations { "Organisations": [{Uuid, Name}, ...] } — scoped to the acting user's access, like GET /cases/search
GET /kle-numbers { "KleNumbers": [{Uuid, Code, Title}, ...] } — KLE classification subjects ("emneord")
GET /classification-facets { "ClassificationFacets": [{Uuid, Code, Title}, ...] } — "handlingsfacetter"
GET /sensitivities { "Sensitivities": [{Uuid, Key, Title}, ...] }Key is what POST /cases expects as sensitivity
GET /users { "Users": [{Id, DisplayName}, ...] } — Nextcloud users with an OpenCase role, sorted by display name
GET /templates { "Templates": [{Id, Name, OriginalFilename, MimeType, Size, UploadedBy, CreatedAt}, ...] }

Note the fields in this section are plain JSON types (numbers/booleans), not the string-encoded scheme used by the Case/Document/File resources above — see public-api.schema.json for the precise per-endpoint types.

Search — PublicSearchApiController

GET /search?q=borgerhenvendelse&organisation=Teknik+og+Miljø&year=2024&limit=20&offset=0

Mirrors the internal /search page: searches case/document metadata in the database plus file content/metadata in Elasticsearch, filtered by the acting user's access profiles. Query parameters: q (required), organisation, year, document_type, mime_type, case_id, date_from, date_to (all optional), limit (default 20, max 1000), offset. Response:

{ "SearchResult": {
  "Total": "42", "Limit": "20", "Offset": "0",
  "Cases": [ { "Id": "1", "CaseNumber": "2024-001234", "Title": "...", "Year": "2024", "CasetypeId": "2", "CasetypeName": "..." } ],
  "Documents": [ { "Id": "5", "Title": "...", "DocumentType": "brev", "CaseId": "1", "CaseNumber": "2024-001234", "CaseTitle": "..." } ],
  "Files": [ { "FileId": "9", "DocumentId": "5", "CaseId": "1", "CaseNumber": "2024-001234", "CaseTitle": "...", "Filename": "brev.pdf", "DocumentType": "brev", "MimeType": "application/pdf", "Organisation": "...", "Year": "2024", "Score": "3.21", "VirtualPath": "/Sager/.../brev.pdf" } ]
} }

Files is omitted (not an error) if Elasticsearch is unavailable — Cases/Documents still come from the database.