Skip to content

IntraVox API Reference

This document describes the IntraVox REST API for developers who want to integrate with IntraVox programmatically.

Table of Contents


Quick Reference: OCS API Endpoints (External Access)

Method Endpoint Description
GET /api/v1/pages List all pages
POST /api/v1/pages Create a new page
GET /api/v1/pages/{id} Get single page
PUT /api/v1/pages/{id} Update page
DELETE /api/v1/pages/{id} Delete page
GET /api/v1/pages/{pageId}/media List media files
POST /api/v1/pages/{pageId}/media Upload media
GET /api/v1/pages/{pageId}/media/{filename} Get media file

All OCS endpoints are prefixed with /ocs/v2.php/apps/intravox


Base URLs

IntraVox provides two API interfaces:

Use the OCS API when accessing IntraVox from external tools, scripts, or applications using Basic Auth or app passwords:

https://your-nextcloud.com/ocs/v2.php/apps/intravox/api/v1/

The OCS API properly handles HTTP methods (GET, POST, PUT, DELETE) and is the recommended interface for programmatic access.

Two things to know before building on it — both measured against a running server, not inferred from the /ocs/ path:

  • There is no OCS envelope. You get the same bare JSON as on the app mount, not {ocs: {meta, data}}. No IntraVox controller extends OCSController. A client written against the envelope will fail to parse every response. ?format=xml does not work here either.
  • Only the eight /api/v1/* routes are mounted here. Every other path returns 404 with an OCS error envelope — including /api/health. For a liveness probe use https://your-nextcloud.com/apps/intravox/api/health on the app mount.

The OCS-APIRequest: true header is required on writes: without it Nextcloud refuses a POST/PUT/DELETE with 412 (CSRF check failed). It is harmless on reads, so send it on every request.

Standard API (Internal use)

The standard API is used internally by the IntraVox frontend with session authentication:

https://your-nextcloud.com/apps/intravox/api/

Authentication

The API uses Nextcloud's standard authentication:

  1. App Password + Basic Auth (recommended for external tools)
  2. Create an app password in Nextcloud: Settings → Security → Devices & sessions → Create new app password
  3. Include OCS-APIREQUEST: true header for OCS endpoints

  4. Session cookies (for browser-based applications)

  5. Bearer token with Nextcloud OAuth tokens

Example with curl (OCS API):

curl -u "username:app-password" \
  -H "OCS-APIREQUEST: true" \
  https://your-nextcloud.com/ocs/v2.php/apps/intravox/api/v1/pages

Example: Create a page

curl -X POST \
  -u "username:app-password" \
  -H "Content-Type: application/json" \
  -H "OCS-APIREQUEST: true" \
  -d '{"id": "my-page", "title": "My Page", "uniqueId": "page-12345"}' \
  https://your-nextcloud.com/ocs/v2.php/apps/intravox/api/v1/pages

Response Format

All endpoints return JSON responses with a consistent structure.

Success Response

{
  "success": true,
  "data": { ... }
}

Error Response

Errors return appropriate HTTP status codes (400, 403, 404, 500) with a consistent format:

{
  "success": false,
  "error": "Human-readable error message",
  "errorId": "err_abc123"
}

The errorId field is included for server errors (5xx) and can be used by support to correlate with server logs. Client errors (4xx) may not include this field.

Important: Error messages are intentionally generic for security reasons. Sensitive details (stack traces, file paths, SQL errors) are logged server-side but never exposed to API consumers.


Pages API

List All Pages

GET /api/pages

Returns a flat list of all pages the user has read access to.

Response:

[
  {
    "id": "page-abc123",
    "uniqueId": "page-abc123",
    "title": "Welcome Page",
    "path": "/IntraVox/en/Welcome Page",
    "parentId": null,
    "permissions": {
      "canRead": true,
      "canWrite": true,
      "canDelete": true
    },
    "createdAt": "2025-01-15T10:30:00Z",
    "updatedAt": "2025-01-16T14:22:00Z"
  }
]

Get Page Tree

GET /api/pages/tree

Returns pages organized in a hierarchical tree structure.

Get Single Page

GET /api/pages/{id}

Returns full page details including content.

Parameters: - id (string): The unique page ID

Response:

{
  "id": "page-abc123",
  "uniqueId": "page-abc123",
  "title": "Welcome Page",
  "content": "<p>Page HTML content...</p>",
  "metadata": {
    "author": "admin",
    "category": "general"
  },
  "permissions": {
    "canRead": true,
    "canWrite": true,
    "canDelete": true
  },
  "breadcrumb": [
    {"id": "page-root", "title": "Home"},
    {"id": "page-abc123", "title": "Welcome Page"}
  ]
}

Create Page

POST /api/pages

Creates a new page.

Request Body:

{
  "title": "New Page Title",
  "content": "<p>Initial content</p>",
  "parentId": "page-parent123",
  "metadata": {
    "category": "news"
  }
}

Response: Returns the created page object with HTTP 201.

Update Page

PUT /api/pages/{id}

Updates an existing page.

Request Body:

{
  "title": "Updated Title",
  "content": "<p>Updated content</p>",
  "metadata": {
    "category": "updated"
  }
}

Delete Page

DELETE /api/pages/{id}

Deletes a page. Returns {"success": true} on success.


Page Layout & Widgets

IntraVox pages use a structured JSON layout instead of raw HTML. The layout consists of rows containing widgets.

Layout Structure

{
  "title": "My Page",
  "layout": {
    "columns": 1,
    "rows": [
      {
        "columns": 1,
        "backgroundColor": "",
        "widgets": [...]
      }
    ],
    "sideColumns": {
      "left": { "enabled": false, "widgets": [] },
      "right": { "enabled": false, "widgets": [] }
    }
  }
}

Row Properties

Property Type Description
columns int Number of columns (1-4)
backgroundColor string CSS color or variable (e.g., var(--color-primary-element))
collapsible bool Make row collapsible (SharePoint-style)
sectionTitle string Title shown in collapsible header
defaultCollapsed bool Start collapsed (default: false)
widgets array Array of widget objects

Widget Types

All widgets have these common properties: - type (string): Widget type identifier - column (int): Which column (1-based) - order (int): Sort order within column

Text Widget

{
  "type": "text",
  "column": 1,
  "order": 1,
  "content": "**Markdown** supported text content"
}

Heading Widget

{
  "type": "heading",
  "column": 1,
  "order": 1,
  "content": "Section Title",
  "level": 2
}
- level: 1-6 (h1-h6)

Image Widget

{
  "type": "image",
  "column": 1,
  "order": 1,
  "src": "photo.jpg",
  "alt": "Description",
  "caption": "Optional caption"
}
- src: Filename (relative to page media folder) or full URL

Video Widget

{
  "type": "video",
  "column": 1,
  "order": 1,
  "url": "https://youtube.com/watch?v=...",
  "caption": ""
}

Divider Widget

{
  "type": "divider",
  "column": 1,
  "order": 1,
  "style": "solid",
  "color": "",
  "height": "2px"
}
- style: "solid", "dashed", "dotted"

{
  "type": "links",
  "column": 1,
  "order": 1,
  "columns": 3,
  "items": [
    {
      "title": "Link Title",
      "text": "Description",
      "url": "https://example.com",
      "icon": "home",
      "target": "_blank"
    }
  ]
}
- icon: Material Design Icon name (without mdi- prefix)

News Widget

{
  "type": "news",
  "column": 1,
  "order": 1,
  "title": "Latest News",
  "sourcePath": "news",
  "layout": "carousel",
  "columns": 3,
  "limit": 5,
  "sortBy": "modified",
  "sortOrder": "desc",
  "showImage": true,
  "showDate": true,
  "showExcerpt": true,
  "excerptLength": 100,
  "autoplayInterval": 5
}
- layout: "grid", "carousel", "list" - sortBy: "modified", "created", "title"

Calendar Widget

{
  "type": "calendar",
  "column": 1,
  "order": 1,
  "title": "Upcoming Events",
  "calendarIds": [1, 106],
  "dateRange": "upcoming",
  "limit": 5,
  "showTime": true,
  "showLocation": false,
  "backgroundColor": null
}
- calendarIds: Array of Nextcloud calendar IDs - dateRange: "upcoming", "this_week", "next_two_weeks", "this_month", "next_three_months", "next_six_months", "next_year", "past_week", "past_month", "past_three_months"

Complete Page Example

curl -X POST \
  -u "username:app-password" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Department Page",
    "parentId": "page-parent-123",
    "layout": {
      "columns": 1,
      "rows": [
        {
          "columns": 1,
          "backgroundColor": "var(--color-primary-element)",
          "widgets": [
            {
              "type": "heading",
              "column": 1,
              "order": 1,
              "content": "Welcome to IT Department",
              "level": 1
            }
          ]
        },
        {
          "columns": 2,
          "backgroundColor": "",
          "widgets": [
            {
              "type": "text",
              "column": 1,
              "order": 1,
              "content": "Welcome to the IT department page. Here you will find all relevant information."
            },
            {
              "type": "links",
              "column": 2,
              "order": 1,
              "columns": 1,
              "items": [
                {"title": "Helpdesk", "url": "/p/helpdesk", "icon": "help-circle"},
                {"title": "Systems", "url": "/p/systems", "icon": "server"}
              ]
            }
          ]
        }
      ]
    }
  }' \
  https://your-nextcloud.com/apps/intravox/api/pages

Media API

Each IntraVox page has a _media folder for storing images and videos. Media can be uploaded via the internal API (session auth) or via WebDAV for external tools.

Upload Media (Internal API)

POST /api/pages/{pageId}/media

Uploads a file to a page's media folder. Used by the IntraVox frontend.

Content-Type: multipart/form-data

Form Fields: - media: The file to upload (or image/video for backward compatibility)

Response:

{
  "filename": "img_60f1e5b2d3a4c6.jpg"
}

Upload Media with Custom Name

POST /api/pages/{pageId}/media/upload

Uploads a file with a specified filename (instead of auto-generated).

Content-Type: multipart/form-data

Form Fields: - file: The file to upload - filename: Desired filename

Response:

{
  "filename": "custom-name.jpg",
  "url": "/apps/intravox/api/pages/{pageId}/media/custom-name.jpg"
}

Check for Duplicate Media

POST /api/pages/{pageId}/media/check

Checks if a file with the same name already exists before uploading.

Request Body:

{
  "filename": "image.jpg"
}

Response:

{
  "exists": true,
  "existingFile": {
    "name": "image.jpg",
    "size": 125432,
    "modified": 1705412520
  }
}

List Media (Internal API)

GET /api/pages/{pageId}/media/list

Returns all media files for a page (internal route).

Upload Media via WebDAV (External Tools)

For external tools using Basic Auth, use Nextcloud's WebDAV API to upload media files:

# First, get the page path
curl -u "user:app-password" \
  -H "OCS-APIREQUEST: true" \
  "https://your-nextcloud.com/ocs/v2.php/apps/intravox/api/v1/pages/{uniqueId}?format=json"
# Response includes: "path": "en/page-folder"

# Upload the file via WebDAV
curl -X PUT \
  -u "user:app-password" \
  --data-binary @/path/to/image.jpg \
  -H "Content-Type: image/jpeg" \
  "https://your-nextcloud.com/remote.php/dav/files/user/IntraVox/{path}/_media/image.jpg"

Example:

# Upload banner.svg to page "page-media-test-001"
curl -X PUT \
  -u "admin:app-password" \
  --data-binary @banner.svg \
  -H "Content-Type: image/svg+xml" \
  "https://your-nextcloud.com/remote.php/dav/files/admin/IntraVox/en/api-media-test/_media/banner.svg"

List Media (OCS API)

GET /ocs/v2.php/apps/intravox/api/v1/pages/{pageId}/media

Returns all media files attached to a page.

Headers: - OCS-APIREQUEST: true

Response:

{
  "media": [
    {
      "name": "img_60f1e5b2.jpg",
      "size": 125432,
      "mimeType": "image/jpeg",
      "modified": 1705412520
    }
  ]
}

Get Media File (OCS API)

GET /ocs/v2.php/apps/intravox/api/v1/pages/{pageId}/media/{filename}

Returns the media file content.


Translations API

Since 2.0. Pages in different languages can be linked as translations of each other via a shared, symmetric translation group — there is no "source" and no "copies". Every page response carries a translations array with its other language versions (language, uniqueId, title, status), filtered against the caller's own Team Folder access.

Create a Translation

POST /api/pages/{pageId}/translations/create

Copies the page — content, layout and its _media files — into the target language as a draft, mirrored to the same position in that language's tree and under the same folder name as the source (each language is a separate tree, so the name cannot clash), and links both pages in one group. The target language folder must already exist; missing ancestor levels are created as bare folders and render in the tree as non-clickable placeholders.

Body:

{
  "language": "de",
  "title": "Optional title for the new page"
}

Response: 201 with { "success": true, "page": {…}, "translations": […] }

Errors: 400 (invalid or missing language), 403 (no create permission in the target language), 404 (page not found).

POST /api/pages/{pageId}/translations

Body: { "targetUniqueId": "page-…" }

Joins two pages (in different languages) into one group, adopting an existing group when either side has one. Requires edit permission on both pages — checked before anything is written.

DELETE /api/pages/{pageId}/translations

Gives this page a fresh group of its own; other members keep theirs. Nothing is deleted.

Translation Candidates

GET /api/pages/{pageId}/translation-candidates?language=de

Pages in other languages that could be linked — excludes pages already in a different group, so linking can never silently pull a page out of an existing set.

Translatable Languages

GET /api/pages/{pageId}/translatable-languages

Languages this page can still be created in. Each entry carries code, name and missingAncestors — how many parent pages do not exist in that language yet.


Versioning API

Get Page Versions

GET /api/pages/{pageId}/versions

Returns version history for a page.

Response:

[
  {
    "timestamp": 1705412520,
    "date": "2025-01-16T14:22:00Z",
    "author": "admin",
    "label": "Published version"
  },
  {
    "timestamp": 1705326120,
    "date": "2025-01-15T14:22:00Z",
    "author": "editor",
    "label": null
  }
]

Restore Version

POST /api/pages/{pageId}/versions/{timestamp}

Restores a page to a previous version.

Get Version Content

GET /api/pages/{pageId}/versions/{timestamp}/content

Returns the content of a specific version.

Update Version Label

PUT /api/pages/{pageId}/versions/{timestamp}/label

Updates the label/name of a version (for named snapshots).

Request Body:

{
  "label": "Before major redesign"
}

Response:

{
  "success": true,
  "timestamp": 1705412520,
  "label": "Before major redesign"
}


Comments API

Get Comments

GET /api/pages/{pageId}/comments

Query Parameters: - limit (int, optional): Maximum comments to return (default: 50) - offset (int, optional): Pagination offset

Response:

{
  "comments": [
    {
      "id": 123,
      "message": "Great page!",
      "authorId": "user1",
      "authorName": "John Doe",
      "createdAt": "2025-01-16T10:00:00Z",
      "parentId": null,
      "replies": []
    }
  ],
  "total": 15
}

Create Comment

POST /api/pages/{pageId}/comments

Request Body:

{
  "message": "This is my comment",
  "parentId": null
}

Set parentId to another comment ID to create a reply.

Update Comment

PUT /api/comments/{commentId}

Request Body:

{
  "message": "Updated comment text"
}

Only the comment author can update their comments.

Delete Comment

DELETE /api/comments/{commentId}

Comment authors and admins can delete comments.


Reactions API

Page Reactions

GET /api/pages/{pageId}/reactions
POST /api/pages/{pageId}/reactions/{emoji}
DELETE /api/pages/{pageId}/reactions/{emoji}

GET Response:

{
  "reactions": {
    "👍": ["user1", "user2"],
    "❤️": ["user3"]
  },
  "userReactions": ["👍"]
}

Comment Reactions

GET /api/comments/{commentId}/reactions
POST /api/comments/{commentId}/reactions/{emoji}
DELETE /api/comments/{commentId}/reactions/{emoji}

Analytics API

Track Page View

POST /api/analytics/track/{pageId}

Tracks a page view. Called automatically by the frontend. Requires read access to the page.

Response:

{
  "success": true
}

Get Page Statistics

GET /api/analytics/page/{pageId}

Query Parameters: - days (int, optional): Number of days to include (default: 30, max: 365)

Response:

{
  "success": true,
  "pageId": "page-abc123",
  "period": 30,
  "totalViews": 150,
  "totalUniqueUsers": 42,
  "dailyStats": [
    {
      "date": "2025-01-15",
      "views": 25,
      "uniqueUsers": 12
    }
  ]
}

Get Top Pages

GET /api/analytics/top

Query Parameters: - limit (int, optional): Maximum pages to return (default: 10, max: 50) - days (int, optional): Period to consider (default: 30, max: 365)

Response:

{
  "success": true,
  "period": 30,
  "pages": [
    {
      "pageId": "page-abc123",
      "title": "Popular Page",
      "path": "/news/popular",
      "totalViews": 500,
      "totalUniqueUsers": 150
    }
  ]
}

Get Dashboard Statistics

GET /api/analytics/dashboard

Returns aggregated statistics for the analytics dashboard. Admin only.

Query Parameters: - days (int, optional): Period to include (default: 30, max: 365)

Response:

{
  "success": true,
  "period": 30,
  "totalViews": 5000,
  "totalPagesViewed": 45,
  "dailyStats": [...],
  "topPages": [...]
}

Analytics Settings (Admin Only)

GET /api/analytics/settings
POST /api/analytics/settings

GET Response:

{
  "success": true,
  "enabled": true,
  "retentionDays": 90
}

POST Request Body:

{
  "enabled": true,
  "retentionDays": 90
}

POST Response:

{
  "success": true,
  "enabled": true,
  "retentionDays": 90
}


Bulk Operations API

All bulk operations require admin privileges. Non-admin users receive HTTP 403.

Operation Limits: Maximum 100 pages per bulk operation to prevent server overload.

Validate Operation

POST /api/bulk/validate

Validates a bulk operation without executing it (dry-run).

Request Body:

{
  "pageIds": ["page-1", "page-2", "page-3"],
  "operation": "delete"
}

Response:

{
  "success": true,
  "operation": "delete",
  "total": 3,
  "canProceed": 2,
  "willFail": 1,
  "valid": [
    {"pageId": "page-1", "title": "Page 1"},
    {"pageId": "page-2", "title": "Page 2"}
  ],
  "invalid": [
    {"pageId": "page-3", "reason": "Permission denied"}
  ]
}

Bulk Delete

POST /api/bulk/delete

Request Body:

{
  "pageIds": ["page-1", "page-2"],
  "deleteChildren": true,
  "dryRun": false
}

Response:

{
  "success": true,
  "operation": "delete",
  "total": 2,
  "deleted": 2,
  "failed": 0,
  "results": [
    {"pageId": "page-1", "title": "Page 1", "status": "deleted"},
    {"pageId": "page-2", "title": "Page 2", "status": "deleted"}
  ],
  "errors": []
}

Bulk Move

POST /api/bulk/move

Request Body:

{
  "pageIds": ["page-1", "page-2"],
  "targetParentId": "page-parent",
  "dryRun": false
}

Bulk Update

POST /api/bulk/update

Request Body:

{
  "pageIds": ["page-1", "page-2"],
  "updates": {
    "metadata": {
      "category": "archived"
    }
  },
  "dryRun": false
}


Get Navigation

GET /api/navigation

Returns the custom navigation configuration.

Save Navigation

POST /api/navigation

Updates the navigation configuration (requires write permission).

GET /api/footer
POST /api/footer

Settings API

Video Domains

GET /api/settings/video-domains
POST /api/settings/video-domains

Get or set allowed video embed domains.

GET Response:

{
  "domains": ["youtube.com", "vimeo.com", "youtu.be"]
}

POST Request Body:

{
  "domains": ["youtube.com", "vimeo.com", "dailymotion.com"]
}

Upload Limit

GET /api/settings/upload-limit

Returns the maximum file upload size.

Response:

{
  "limit": 104857600,
  "limitFormatted": "100 MB"
}

Engagement Settings

GET /api/settings/engagement
POST /api/settings/engagement

Configure page engagement features (comments, reactions).

Response:

{
  "commentsEnabled": true,
  "reactionsEnabled": true
}

Publication Settings

GET /api/settings/publication
POST /api/settings/publication

Configure publication workflow settings.

Response:

{
  "requireApproval": false,
  "defaultStatus": "published"
}


Page Metadata API

Get Page Metadata

GET /api/pages/{pageId}/metadata

Returns metadata for a page without the full content.

Response:

{
  "id": "page-abc123",
  "title": "Page Title",
  "author": "admin",
  "createdAt": "2025-01-15T10:00:00Z",
  "updatedAt": "2025-01-16T14:00:00Z",
  "category": "news",
  "tags": ["important", "announcement"]
}

Update Page Metadata

PUT /api/pages/{pageId}/metadata

Updates only the metadata of a page.

Request Body:

{
  "category": "archived",
  "tags": ["old", "archived"]
}

Get Page Content

GET /api/pages/{pageId}/content

Returns only the content/layout of a page (without metadata).

Get Breadcrumb

GET /api/pages/{id}/breadcrumb

Returns the breadcrumb path for a page.

Response:

[
  {"id": "page-root", "title": "Home", "path": "/"},
  {"id": "page-parent", "title": "Parent Page", "path": "/parent"},
  {"id": "page-current", "title": "Current Page", "path": "/parent/current"}
]

Check Cache Status

GET /api/page/{pageId}/cache-status

Returns cache status information for a page.

Response:

{
  "cached": true,
  "lastModified": "2025-01-16T14:00:00Z",
  "etag": "abc123"
}


News API

Get News Items

GET /api/news

Returns news items from pages marked as news.

Query Parameters: - limit (int, optional): Maximum items to return (default: 10) - sourcePath (string, optional): Filter by source path

Response:

[
  {
    "id": "page-news-1",
    "title": "Latest Announcement",
    "excerpt": "This is an important...",
    "image": "banner.jpg",
    "date": "2025-01-16T10:00:00Z",
    "path": "/news/announcement"
  }
]


Resources API

Get Resource Media

GET /api/resources/media/{filename}
GET /api/resources/media/{folder}/{filename}

Returns shared resource files (backgrounds, icons, etc.).

Examples:

GET /api/resources/media/logo.svg
GET /api/resources/media/backgrounds/header-bg.jpg


Permissions API

Get User Permissions

GET /api/permissions

Returns the current user's permissions in IntraVox.

Response:

{
  "isAdmin": true,
  "canCreatePages": true,
  "canEditNavigation": true,
  "canImport": true,
  "canExport": true,
  "canManageSettings": true
}


MetaVox Integration API

Get MetaVox Status

GET /api/metavox/status

Checks if MetaVox is installed and enabled.

Response:

{
  "installed": true,
  "enabled": true,
  "version": "1.2.0"
}

Get MetaVox Fields

GET /api/metavox/fields

Returns available MetaVox metadata fields for IntraVox pages.

Response:

{
  "fields": [
    {"name": "department", "type": "select", "options": ["IT", "HR", "Finance"]},
    {"name": "owner", "type": "user"},
    {"name": "expiryDate", "type": "date"}
  ]
}


Setup & Demo Data API

Run Setup

POST /api/setup

Initializes IntraVox (creates folder structure, default pages). Admin only.

Response:

{
  "success": true,
  "message": "IntraVox setup completed"
}

Get Demo Data Status

GET /api/demo-data/status

Checks if demo data is installed.

Response:

{
  "installed": false,
  "availableLanguages": ["en", "nl", "de"]
}

Get Available Languages

GET /api/demo-data/languages

Returns languages available for demo data import.

Import Demo Data

POST /api/demo-data/import

Imports demo data for a specific language.

Request Body:

{
  "language": "en"
}

Clean Start

POST /api/demo-data/clean-start

Removes all IntraVox content and starts fresh. Admin only.


Calendar API

Requires authentication (session). Private and confidential events (CLASS:PRIVATE, CLASS:CONFIDENTIAL) are automatically filtered out. Date range is capped at 1 year maximum.

List Calendars

GET /api/calendar/calendars

Returns all calendars available to the current user (personal + shared).

Responses: 200 OK, 401 Authentication required, 500 Server error

Response:

{
  "calendars": [
    {
      "id": 106,
      "uri": "org-events",
      "displayName": "Org events",
      "color": "#D6B461",
      "isReadOnly": false
    }
  ]
}

Get Calendar Events

GET /api/calendar/events?calendarIds={ids}&rangeStart={iso}&rangeEnd={iso}&limit={n}

Returns events from one or more calendars within a time range. Recurring events (RRULE) are expanded into individual occurrences.

Parameters:

Parameter Type Required Description
calendarIds string Yes Comma-separated calendar IDs (e.g., 1,106)
rangeStart string No ISO 8601 start date. Defaults to now
rangeEnd string No ISO 8601 end date. Defaults to 30 days from now
limit integer No Maximum events (1-20, default: 5)

Responses: 200 OK, 400 Invalid date format, 401 Authentication required, 500 Server error

Response:

{
  "events": [
    {
      "uid": "f602fd5a-a355-4c98-87c4-c6ea30d8e405-2026-03-26T07:00:00+01:00",
      "summary": "Team meeting A",
      "location": "Utrecht office",
      "start": "2026-03-26T07:00:00+01:00",
      "end": "2026-03-26T09:30:00+01:00",
      "isAllDay": false,
      "calendarColor": "#D6B461",
      "calendarName": "Org events"
    }
  ]
}

Get Calendar Events (Public Share)

GET /api/share/{token}/calendar/events?calendarIds={ids}&rangeStart={iso}&rangeEnd={iso}&limit={n}

Same parameters and response as above, but uses the share owner's calendar context. No authentication required — only a valid share token.

Responses: 200 OK, 400 Invalid date format, 403 Invalid or expired share token, 500 Server error


Search API

Search Pages

GET /api/search?q={query}

Query Parameters: - q (string): Search query

Response:

[
  {
    "id": "page-abc123",
    "title": "Matching Page",
    "excerpt": "...text containing the <mark>search</mark> term..."
  }
]


Export/Import API

Get Exportable Languages

GET /api/export/languages

Returns a list of languages that can be exported.

Response:

{
  "languages": ["en", "nl", "de"]
}

Export Language

GET /api/export/language/{language}
GET /api/export/language/{language}/zip

Exports all pages for a language as JSON or ZIP.

Export Single Page

GET /api/export/page/{uniqueId}

Import ZIP

POST /api/import/zip

Imports pages from a ZIP file (requires admin).

Import from Confluence

POST /api/import/confluence/html

Imports pages from a Confluence HTML export ZIP file.

Content-Type: multipart/form-data

Form Fields: - file: The Confluence HTML export ZIP file - language (string, optional): Target language (default: "nl") - parentPageId (string, optional): Parent page ID for imported pages


Error Codes

Code Meaning Example
200 Success Request completed
201 Created Page/comment created
207 Multi-Status Partial success in bulk operations
400 Bad Request Invalid parameters, validation error
403 Forbidden No permission, admin required
404 Not Found Page/resource doesn't exist
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server-side error (check errorId in response)

Security

Admin-Only Endpoints

The following endpoints require system administrator privileges:

Endpoint Purpose
POST /api/bulk/* Bulk operations (delete, move, update)
POST /api/import/* ZIP and Confluence imports
POST /api/setup Initial setup
POST /api/settings/* Video domains, engagement, publication settings
GET /api/analytics/dashboard Analytics dashboard
POST /api/analytics/settings Analytics configuration
POST /api/demo-data/* Demo data management

File Upload Security

  • Extension whitelist: Only allowed extensions: jpg, jpeg, png, gif, webp, svg, mp4, webm, ogg
  • MIME type validation: Files are validated using both finfo and getimagesize() for images
  • SVG sanitization: SVG files are sanitized to remove scripts, iframes, and other dangerous content
  • Size limits: Respects PHP and Nextcloud upload limits

Path Traversal Protection

All path parameters are validated against: - Null byte injection - URL-encoded traversal patterns (../) - Double-encoding attacks - Unicode normalization attacks


Rate Limiting

Some endpoints carry a per-user limit. Exceeding one returns 429 Too Many Requests; the limit is counted per user, per endpoint, over a rolling window.

This table is guarded: RateLimitDocsTest walks every controller and fails when a #[UserRateLimit] in code is missing here or carries a different number.

Endpoint Handler Limit
POST /api/pages createPage 10 / minute
DELETE /api/pages/{id} deletePage 10 / minute
POST /api/pages/reorder reorderPages 20 / minute
POST /api/pages/move movePage 20 / minute
GET /api/search searchPages 30 / minute
POST /api/bulk/delete deletePages 5 / minute
POST /api/bulk/move movePages 5 / minute
POST /api/bulk/update updatePages 5 / minute
POST /api/bulk/validate validateOperation 20 / minute
GET /api/feed/external getFeed 30 / minute
GET /api/feed/preview getPreview 30 / minute
POST /api/analytics/track/{pageId} trackView 60 / minute
GET /api/preview fetch 600 / minute
POST /api/preview/warmup warmup 60 / minute
POST /api/pages/{pageId}/comments createComment 20 / minute
POST /api/pages/{id}/reactions/{emoji} addPageReaction 30 / minute
POST /api/comments/{id}/reactions/{emoji} addCommentReaction 30 / minute
GET /api/health health 60 / minute (anonymous)
GET /api/share/{token}/* PublicShareController 30–60 / minute (anonymous)

Withdrawing a reaction is deliberately not limited; adding one is.

429 without a declared limit

Everything else relies on Nextcloud's brute-force protection, and that is worth spelling out because it surprises automated clients:

Repeated failed authentication from one address returns 429 on every authenticated endpoint, declared limit or not. Once tripped it keeps answering 429 even for correct credentials until the window expires. A client that retries a bad token in a loop therefore locks itself out rather than eventually getting through.

Treat 401 as terminal: fix the credential, do not retry. This was measured against a running instance, not inferred from the code.

One more measured detail that trips strict clients: that 429 answers text/xml, not JSON. It is produced by Nextcloud before the app is reached, so it carries an OCS error envelope rather than this API's error shape. A client that assumes every response is JSON will fail to parse it — and will fail on the throttle rather than on the request that caused it.

Bulk provisioning and migrations

createPage at 10 per minute is the binding constraint on any migration: two thousand pages is roughly three and a half hours of waiting, and none of it is work. Plan for it rather than discovering it.

Nextcloud already provides the way out, and it needs no change to IntraVox. When apply_allowlist_to_ratelimit is enabled, an IP range on the brute-force allow-list bypasses rate limiting entirely:

# The host the migration runs from — a range, in CIDR notation.
occ config:app:set bruteForce whitelist_0 --value='10.0.0.0/24'
occ config:app:set bruteforcesettings apply_allowlist_to_ratelimit --value='yes'

Two things worth being blunt about. This exempts every rate limit for that range, not just page creation, so scope it to the migration host and nothing wider. And turn it off afterwards — an allow-list entry that outlives the migration is a permanent hole that nothing will remind you about:

occ config:app:delete bruteforcesettings apply_allowlist_to_ratelimit
occ config:app:delete bruteForce whitelist_0

If that is not acceptable in a given environment, the alternative is to pace the import at ten pages per minute and let it run. There is deliberately no IntraVox-specific bypass: a second mechanism next to the one Nextcloud already has would be one more thing to get wrong, and it would live in application code where an administrator cannot see it.

Migration Tool Integration

For migration tools (SharePoint, Confluence, custom CMS) that need to bulk-import pages with media, we recommend the following Nextcloud-supported approach:

┌─────────────────────────────────────────────────────────────────┐
│  Migration Tool                                                  │
├─────────────────────────────────────────────────────────────────┤
│  1. Create page via OCS API                                     │
│     POST /ocs/v2.php/apps/intravox/api/v1/pages                │
│     → Returns pageId and path                                   │
│                                                                  │
│  2. Upload media via WebDAV (Nextcloud native)                  │
│     PUT /remote.php/dav/files/{user}/{path}/_media/{filename}  │
│     → Standard Nextcloud file upload                            │
│                                                                  │
│  3. Update page content with media references                   │
│     - Edit page.json via WebDAV, or                            │
│     - Use OCS PUT endpoint (when supported)                     │
└─────────────────────────────────────────────────────────────────┘

Why WebDAV for Media Uploads?

Aspect WebDAV
Stability Core Nextcloud feature since version 1
Documentation Officially documented at docs.nextcloud.com
Large files Chunked upload support built-in
Authentication App passwords (secure, revocable)
Reliability Used by all official Nextcloud clients

Complete Migration Example

#!/bin/bash
# Example: Migrate a SharePoint page with images to IntraVox

SERVER="https://your-nextcloud.com"
USER="admin"
APP_PASSWORD="your-app-password"
AUTH="$USER:$APP_PASSWORD"

# Step 1: Create the page via OCS API
PAGE_RESPONSE=$(curl -s -X POST \
  -u "$AUTH" \
  -H "Content-Type: application/json" \
  -H "OCS-APIREQUEST: true" \
  -d '{
    "id": "migrated-sharepoint-page",
    "title": "Migrated from SharePoint",
    "uniqueId": "page-sp-migration-001",
    "layout": {
      "columns": 1,
      "rows": [
        {
          "columns": 1,
          "widgets": [
            {
              "type": "heading",
              "column": 1,
              "order": 1,
              "content": "Migrated Page",
              "level": 1
            },
            {
              "type": "image",
              "column": 1,
              "order": 2,
              "src": "header-banner.jpg",
              "alt": "Header image"
            },
            {
              "type": "text",
              "column": 1,
              "order": 3,
              "content": "Content migrated from SharePoint."
            }
          ]
        }
      ]
    }
  }' \
  "$SERVER/ocs/v2.php/apps/intravox/api/v1/pages")

echo "Page created: $PAGE_RESPONSE"

# Step 2: Upload media via WebDAV
# The _media folder is auto-created when the page is created
curl -X PUT \
  -u "$AUTH" \
  --data-binary @/path/to/header-banner.jpg \
  -H "Content-Type: image/jpeg" \
  "$SERVER/remote.php/dav/files/$USER/IntraVox/en/migrated-sharepoint-page/_media/header-banner.jpg"

echo "Media uploaded successfully"

WebDAV Chunked Upload (Large Files)

For files larger than 10MB, use Nextcloud's chunked upload:

# Initialize chunked upload
UPLOAD_DIR=".upload-$(uuidgen)"
curl -X MKCOL \
  -u "$AUTH" \
  "$SERVER/remote.php/dav/uploads/$USER/$UPLOAD_DIR"

# Upload chunks (5MB each)
split -b 5242880 large-video.mp4 chunk_
for chunk in chunk_*; do
  curl -X PUT \
    -u "$AUTH" \
    --data-binary @$chunk \
    "$SERVER/remote.php/dav/uploads/$USER/$UPLOAD_DIR/$chunk"
done

# Assemble the file
curl -X MOVE \
  -u "$AUTH" \
  -H "Destination: $SERVER/remote.php/dav/files/$USER/IntraVox/en/page/_media/large-video.mp4" \
  "$SERVER/remote.php/dav/uploads/$USER/$UPLOAD_DIR/.file"

Media Path Structure

IntraVox stores media in a predictable structure:

/IntraVox/
├── en/                          # Language folder
│   └── page-folder/             # Page folder (same as page ID)
│       ├── page-folder.json     # Page content
│       └── _media/              # Media folder
│           ├── .nomedia         # Marker file (auto-created)
│           ├── header.jpg       # Uploaded images
│           └── video.mp4        # Uploaded videos

Authentication Recommendations

  1. Create dedicated app password for migration tools:
  2. Settings → Security → Devices & sessions
  3. Create app password with descriptive name ("SharePoint Migration Tool")

  4. Use service account for automated migrations:

  5. Create dedicated Nextcloud user for migrations
  6. Assign appropriate GroupFolder permissions

  7. Revoke after completion:

  8. App passwords can be revoked without affecting user account

Privacy

Analytics data is privacy-conscious: - User IDs are hashed before storage - Only daily aggregates are stored, not individual page views - Data is automatically cleaned up based on retention settings (default: 90 days)