Global
GET /api/health — Service health checkPOST /api/v1/servers/register — Register new server (master token auth)
GET /api/health — Service health checkPOST /api/v1/servers/register — Register new server (master token auth)GET /api/v1/public/{token} — Get share metadataPOST /api/v1/public/{token}/verify — Verify share password: { password }GET /api/v1/public/{token}/download — Download shared fileGET /v1/modules — List all modulesGET /v1/modules/grouped — Modules grouped by menuGET /v1/modules/storage-paths — List storage path namesPOST /v1/modules — Create module: { key, display_name, storage_path, ... }GET /v1/modules/{key} — Get modulePUT /v1/modules/{key} — Update moduleDELETE /v1/modules/{key} — Delete modulePOST /v1/modules/{key}/sync — Queue sync to all serversPOST /v1/modules/{key}/sync-now — Sync immediatelyPOST /v1/modules/sync-all — Queue sync all modulesPOST /v1/modules/sync-all-now — Sync all immediatelyPOST /v1/modules/clear-cache — Clear module cacheGET /v1/{server_code}/ — Server info (includes legacy_mode, is_migrating)PUT /v1/{server_code}/ — Update server: { server_name?, status?, storage_quota_mb?, api_rate_limit? }GET /v1/{server_code}/stats — Storage stats (files_count, directories_count)PUT /v1/{server_code}/storage-config — Set storage config (legacy): { storage_provider, storage_config }GET /v1/{server_code}/storage/configs — List all configured providersPOST /v1/{server_code}/storage/configs/test — Test draft config: { provider, config: { host, port, ... } }POST /v1/{server_code}/storage/configs — Save config: { provider, config, name?, description?, activate? }PUT /v1/{server_code}/storage/configs/{provider} — Update config: { config, name?, activate? }DELETE /v1/{server_code}/storage/configs/{provider} — Delete config (cannot delete active)POST /v1/{server_code}/storage/configs/{provider}/test — Retest saved configPOST /v1/{server_code}/storage/configs/{provider}/activate — Activate provider (requires connection_test_passed)POST /v1/{sc}/storage/upload — Multipart: file (required), path (required), metadata? (JSON string), overwrite? (bool, default true)GET /v1/{sc}/storage/download?path=... — Stream file downloadGET /v1/{sc}/storage/lookup?path=... — File metadata (id, name, mime_type, size, module, record_id)GET /v1/{sc}/storage/list?directory=...&recursive=...&per_page=... — List files/dirs (note: param is "directory" not "path")GET /v1/{sc}/storage/exists?path=...&type=file|directory — Check if file/dir existsDELETE /v1/{sc}/storage?path=... — Delete filePOST /v1/{sc}/storage/copy — { source_path, dest_path } — files AND directories (recursive)POST /v1/{sc}/storage/move — { source_path, dest_path } — files onlyPOST /v1/{sc}/storage/rename — { path, new_name } (new_name is filename only, not full path)POST /v1/{sc}/storage/mkdir — { path }DELETE /v1/{sc}/storage/rmdir — { path } (deletes dir + all files inside)POST /v1/{sc}/storage/compress — { paths: [...], archive_name?, dest_path? }POST /v1/{sc}/storage/extract — { path (zip file), dest_path? }POST /v1/{sc}/storage/share — { path, expires_at?, password? (min 4), download_limit? }POST /v1/{sc}/storage/bulk-copy — { paths: [files and/or folders], target_path (directory) } — folder entries copy recursivelyPOST /v1/{sc}/storage/bulk-move — { paths: [...], target_path (directory) } — files onlyPOST /v1/{sc}/storage/bulk-delete — { paths: [...] }GET /v1/{sc}/storage/migration/status — Check if migration in progressGET /v1/{sc}/storage/migration/jobs — List last 50 migrationsPOST /v1/{sc}/storage/migration/start — { source_provider, destination_provider, delete_source_files?, overwrite_existing?, preserve_directory_structure?, file_pattern?, activate_destination? }POST /v1/{sc}/storage/migration/{id}/cancel — Cancel running migrationPOST /v1/{sc}/storage/migration/{id}/retry — Retry failed files onlyPOST /v1/{sc}/storage/import — Start import: { provider: "ftp"|"sftp" }GET /v1/{sc}/storage/import/status — Latest import/sync status and progressGET /v1/{sc}/storage/import/history — List past 20 imports/syncsPOST /v1/{sc}/storage/import/sync — Re-sync: { provider: "ftp"|"sftp" } (requires prior completed import)POST /v1/{sc}/storage/import/retry — Retry timed-out directories from last import (no body needed)POST /v1/{sc}/storage/scan-directory — Sync single directory: { path } (legacy_mode only, synchronous)GET /v1/{sc}/modules — List server module configsGET /v1/{sc}/modules/{key} — Get module configPOST /v1/{sc}/modules — Create module configDELETE /v1/{sc}/modules/{key} — Delete module configPOST /v1/{sc}/modules/import — Import module configsPOST /v1/{sc}/modules/backup — Backup configsPOST /v1/{sc}/modules/restore — Restore from backupPOST /v1/{sc}/modules/reset — Reset to defaultsGET /v1/{sc}/modules/export — Export configsGET /v1/{sc}/modules/health — Module health checkGET /v1/{sc}/modules/backups — List backupsGET /v1/{sc}/shares — List all shares for serverGET /v1/{sc}/shares/{shareId} — Get share detailsPUT /v1/{sc}/shares/{shareId} — Update shareDELETE /v1/{sc}/shares/{shareId} — Delete sharePOST /v1/{sc}/shares/{shareId}/activate — Activate sharePOST /v1/{sc}/shares/{shareId}/deactivate — Deactivate shareGET /v1/{sc}/files/{fileId}/shares — List shares for a filePOST /v1/{sc}/files/{fileId}/shares — Create share for a fileGET /v1/{sc}/files/{fileId}/permissions — List permissionsPOST /v1/{sc}/files/{fileId}/permissions — Grant permissionDELETE /v1/{sc}/files/{fileId}/permissions — Revoke permissionGET /v1/{sc}/files/{fileId}/permissions/check — Check permissionGET|POST /v1/{sc}/files — List/create filesGET|PUT|DELETE /v1/{sc}/files/{fileId} — CRUD file by IDGET /v1/{sc}/files/{fileId}/download — Download by IDPOST /v1/{sc}/files/{fileId}/copy|move|restore — File operations by IDPOST /v1/{sc}/files/bulk/delete|copy|move — Bulk operations by IDGET|POST /v1/{sc}/directories — List/create directoriesGET|PUT|DELETE /v1/{sc}/directories/{directoryId} — CRUD directory by IDPOST /v1/{sc}/templates/document — UNIFIED (recommended). Dynamic engine: { file_no, report_code, version_id?, language?, format?, staff_code (required), document_name? (also default download name), filename? (override), engine?: auto|legacy|native } → document bytes. auto = native when template mapped else legacy gdwmm; format doc|docx|pdf|… (pdf/doc LibreOffice-converted). X-Generator header shows engine used.POST /v1/{sc}/templates/generate-legacy — force LEGACY engine (gdwmm), 100% legacy-identical: { file_no, report_code, version_id?, doc_type?, language?, format?, staff_code (required — empty => WCF 406), document_name?, lod_serial? }POST /v1/{sc}/templates/generate — FileVault-native engine (OpenXML in PHP): { file_no, template_path | report_code(+version_id), procedure?, doc_type?, language?, format? }POST /v1/{sc}/templates/render — Native engine, caller-supplied data (no tenant DB): { template_path, merge_data: {token: value}, format? }GET /v1/{sc}/templates/catalog — List templates (report_code/version + metadata + mapped?): ?category=&language=&kind=system|customized&mapped=yes|no&search=&limit= — each row has kind (system=built-in / customized=tenant own)POST /v1/{sc}/templates/catalog — Register/update a catalog entry (e.g. set template_path): { report_code, version_id?, template_path?, description?, category?, language?, doc_type? }POST /v1/{sc}/templates/catalog/sync — Re-seed catalog metadata from the tenant cboTemplatePractical integration guide for services that consume FileVault.
https://filevault.denning.online/api/v1/{server_code}Authorization: Bearer fv_<token>X-API-Token: fv_<token>X-User-Email: <identity>staffCode in this header.Do not send server_code inside path values.
Valid path formats:
{module}/{recordId}/{sub_path}/{filename}documents/{sub_path}/{filename} (no recordId)Examples:
matters/12345/evidence/contract.pdfstaff/101/avatar/photo.jpgdocuments/policies/leave-policy.pdfInvalid:
TS0004/matters/12345/contract.pdf/matters/12345/contract.pdfdocuments/12345/file.pdfThe metadata field on upload controls file visibility and ownership tracking. It is optional — if omitted, the file defaults to access_level: "server" (visible to all firm users) and uploaded_by: "system".
Metadata fields:
uploaded_by — staff code or email of uploader (default: "system")access_level — file visibility (default: "server"):
"server" — everyone in the same firm/tenant can see it"private" — only the uploader can see it"public" — anyone with the linkUpload fields:
file (required) — the file to uploadpath (required) — destination path, e.g. matters/12345/document.pdfmetadata (optional JSON string) — see Access Levels and Metadata aboveoverwrite (optional boolean, default true) — replace existing file at same pathMinimal example (most common — all firm users can see):
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/upload" \
-H "Authorization: Bearer fv_your_token" \
-H "X-User-Email: ST0001" \
-F "file=@document.pdf" \
-F "path=matters/12345/document.pdf"
With metadata (restrict to uploader only):
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/upload" \
-H "Authorization: Bearer fv_your_token" \
-H "X-User-Email: ST0001" \
-F "file=@document.pdf" \
-F "path=matters/12345/document.pdf" \
-F 'metadata={"uploaded_by":"ST0001","access_level":"private"}'
Note: If you skip metadata, the file defaults to access_level: "server" (visible to all firm users) and uploaded_by: "system".
curl "https://filevault.denning.online/api/v1/TS0004/storage/download?path=matters/12345/document.pdf" \
-H "Authorization: Bearer fv_your_token" \
-H "X-User-Email: ST0001" \
-o document.pdf
curl "https://filevault.denning.online/api/v1/TS0004/storage/lookup?path=matters/12345/document.pdf" \
-H "Authorization: Bearer fv_your_token" \
-H "X-User-Email: ST0001"
curl "https://filevault.denning.online/api/v1/TS0004/storage/list?directory=matters/12345&recursive=false&per_page=100" \
-H "Authorization: Bearer fv_your_token" \
-H "X-User-Email: ST0001"
Check if a file exists:
curl "https://filevault.denning.online/api/v1/TS0004/storage/exists?path=matters/12345/document.pdf" \
-H "Authorization: Bearer fv_your_token"
Check if a directory/folder exists (pass type=directory):
curl "https://filevault.denning.online/api/v1/TS0004/storage/exists?path=matters/12345&type=directory" \
-H "Authorization: Bearer fv_your_token"
Response:
{ "status": "success", "exists": true, "path": "matters/12345", "type": "directory" }
curl -X DELETE "https://filevault.denning.online/api/v1/TS0004/storage" \
-H "Authorization: Bearer fv_your_token" \
-H "Content-Type: application/json" \
-d '{"path":"matters/12345/document.pdf"}'
Use these for cross-file operations. Responses may be:
status: success (all passed)status: partial (some failed; inspect data.errors)paths[] accepts both files AND folders. Folders are copied recursively to {target_path}/{basename}. Per-file collisions at the destination are skipped (reported under data.errors) — existing files are never overwritten.
# Mixed files + folder, single request
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/bulk-copy" \
-H "Authorization: Bearer fv_your_token" \
-H "Content-Type: application/json" \
-d '{
"paths": ["matters/12345/a.pdf", "matters/12345/evidence"],
"target_path": "matters/67890"
}'
Guards that return 422:
target_path/{basename} (same source and destination).curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/bulk-move" \
-H "Authorization: Bearer fv_your_token" \
-H "Content-Type: application/json" \
-d '{
"paths": ["matters/12345/draft1.pdf", "matters/12345/draft2.pdf"],
"target_path": "matters/12345/final"
}'
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/bulk-delete" \
-H "Authorization: Bearer fv_your_token" \
-H "Content-Type: application/json" \
-d '{
"paths": ["matters/12345/old1.pdf", "matters/12345/old2.pdf"]
}'
Each tenant (server_code) can store multiple provider configs and activate one.
Supported providers:
system, s3, azure, gcp, alibaba, ftp, sftp, localcurl "https://filevault.denning.online/api/v1/TS0004/storage/configs" \
-H "Authorization: Bearer fv_your_token"
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/configs" \
-H "Authorization: Bearer fv_your_token" \
-H "Content-Type: application/json" \
-d '{
"provider": "s3",
"name": "Tenant S3",
"config": {
"bucket": "tenant-bucket",
"region": "ap-southeast-1",
"key": "AKIA...",
"secret": "..."
},
"activate": false
}'
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/configs/s3/activate" \
-H "Authorization: Bearer fv_your_token"
Migration is async and requires queue workers.
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/migration/start" \
-H "Authorization: Bearer fv_your_token" \
-H "Content-Type: application/json" \
-d '{
"source_provider": "system",
"destination_provider": "s3",
"activate_destination": true,
"overwrite_existing": false
}'
curl "https://filevault.denning.online/api/v1/TS0004/storage/migration/status" \
-H "Authorization: Bearer fv_your_token"
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/migration/{migrationId}/retry" \
-H "Authorization: Bearer fv_your_token"
Recommended client strategy:
4xx validation/permission errors without fixing payload/identity.data.errors and retry only failed paths.GET /storage/migration/statusGET /storage/migration/jobsPOST /storage/migration/{migrationId}/retryIdempotency notes:
upload is replace-by-default (overwrite=true default).move and bulk-move are not safe to blind-retry unless you verify source/destination state.delete and bulk-delete should re-check with exists or list.FileVault generates merged documents from a matter's data. Use the unified
/templates/document endpoint — one call, with a dynamic engine switch that
lets the firm migrate from the legacy engine to the native one with no caller
change.
# Unified endpoint — engine chosen dynamically. Returns the document bytes.
# Only `format` changes between doc / docx / pdf.
curl -X POST "$BASE/api/v1/$SC/templates/document" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"file_no":"6000-6293","report_code":"11367","version_id":"1",
"language":"B.M.","format":"docx","staff_code":"164"}' \
-o document.docx
engine (optional): auto (default) uses the native engine when the template is mapped, else legacy gdwmm; legacy forces gdwmm (100% legacy-identical); native forces the PHP engine.report_code = cboTemplate.ReportCode (numeric). doc_type optional — auto-derived. language = English|B.M. format = doc|docx|pdf|xls|xlsx|png|txt (pdf/doc are LibreOffice-converted from the native docx).staff_code is required — it becomes webuser-id; an empty value is rejected upstream (HTTP 406 → returned as a 422).X-Generator = legacy:gdwmm or native. Add ?meta=1 for JSON {format, bytes, source}.?encoding=base64 (or base64=1): returns the document base64-encoded inside JSON instead of streaming raw bytes — { status, format, mime, filename, bytes, base64, source }. Use this if your HTTP/WCF stack mangles a raw binary body (e.g. reads it as a string) — base64 is text-safe; decode it to get the exact .docx/.pdf. (Mirrors the legacy DenningAPI generate response shape.) Works on /document, /generate, /generate-legacy, /render.POST .../templates/generate-legacy (force gdwmm), POST .../templates/generate (force native), POST .../templates/render (native, caller-supplied merge_data, no tenant DB). See API Reference §12.dp-api and dcc-api should call path-based endpoints (/storage/*) only.X-User-Email using staffCode.server_code in service config (example: DCC01) and keep path values server-code-free.| Group | Method | Endpoint |
|---|---|---|
| Health | GET | /api/health |
| Global modules | GET/POST/PUT/DELETE | /api/v1/modules/* |
| Server | GET/PUT | /api/v1/{server_code} |
| Server stats | GET | /api/v1/{server_code}/stats |
| Storage (path-based) | Various | /api/v1/{server_code}/storage/* |
| Storage providers | Various | /api/v1/{server_code}/storage/configs* |
| Storage migration | Various | /api/v1/{server_code}/storage/migration/* |
| Module config (server) | Various | /api/v1/{server_code}/modules/* |
| Template generation | POST | /api/v1/{server_code}/templates/document (unified, engine switch), /templates/generate-legacy, /templates/generate, /templates/render, /templates/catalog* |
| Legacy files/directories | Various | /api/v1/{server_code}/files/*, /api/v1/{server_code}/directories/* |
Versioned API reference aligned with current routes.
/api/health/api/v1/api/v1/{server_code}{server_code} format: uppercase alphanumeric, 4-20 chars (example: TS0004, DCC01).
Required for all protected endpoints.
Authorization: Bearer fv_<64_hex>X-API-Token: fv_<64_hex>Optional caller identity (used in access checks):
X-User-Email: <caller-id> (Denning convention: staffCode)Do not include server_code inside path params/body.
Accepted path patterns:
{module}/{recordId}/{sub_path}/{filename}documents/{sub_path}/{filename}Rejected examples:
TS0004/matters/123/file.pdf/matters/123/file.pdf../../../etc/passwdSuccess:
{
"status": "success",
"message": "Operation completed",
"data": {}
}
Partial (bulk operations):
{
"status": "partial",
"message": "Copied 2 of 3 files",
"data": {
"errors": []
}
}
Error:
{
"status": "error",
"message": "Validation failed",
"errors": {}
}
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Service health |
| POST | /api/v1/servers/register |
Register server (requires X-Master-Token) |
| GET | /api/v1/public/{token} |
Public share info |
| POST | /api/v1/public/{token}/verify |
Verify share password |
| GET | /api/v1/public/{token}/download |
Download shared file |
| Method | Endpoint |
|---|---|
| GET | /api/v1/modules |
| POST | /api/v1/modules |
| GET | /api/v1/modules/grouped |
| GET | /api/v1/modules/storage-paths |
| POST | /api/v1/modules/sync-all |
| POST | /api/v1/modules/sync-all-now |
| POST | /api/v1/modules/clear-cache |
| GET | /api/v1/modules/{key} |
| PUT | /api/v1/modules/{key} |
| DELETE | /api/v1/modules/{key} |
| POST | /api/v1/modules/{key}/sync |
| POST | /api/v1/modules/{key}/sync-now |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/{server_code} |
Server details |
| PUT | /api/v1/{server_code} |
Update server profile/quota/rate limit |
| GET | /api/v1/{server_code}/stats |
Storage and file stats |
| PUT | /api/v1/{server_code}/storage-config |
Update active provider and config map |
| Method | Endpoint |
|---|---|
| POST | /api/v1/{server_code}/storage/upload |
| GET | /api/v1/{server_code}/storage/download |
| GET | /api/v1/{server_code}/storage/lookup |
| GET | /api/v1/{server_code}/storage/list |
| GET | /api/v1/{server_code}/storage/exists |
| DELETE | /api/v1/{server_code}/storage |
| POST | /api/v1/{server_code}/storage/copy |
| POST | /api/v1/{server_code}/storage/move |
| POST | /api/v1/{server_code}/storage/rename |
| POST | /api/v1/{server_code}/storage/mkdir |
| DELETE | /api/v1/{server_code}/storage/rmdir |
| POST | /api/v1/{server_code}/storage/compress |
| POST | /api/v1/{server_code}/storage/extract |
| POST | /api/v1/{server_code}/storage/share |
| POST | /api/v1/{server_code}/storage/bulk-copy |
| POST | /api/v1/{server_code}/storage/bulk-move |
| POST | /api/v1/{server_code}/storage/bulk-delete |
Multipart form fields:
file (required) — the file to uploadpath (required) — destination path, e.g. matters/6000-6280/contract.pdfmetadata (optional JSON string) — controls file visibility and tracking:
uploaded_by — staff code or email of uploader (default: "system")access_level — file visibility (default: "server"):
"server" — everyone in the same firm/tenant can see it"private" — only the uploader can see it"public" — anyone with the linkoverwrite (optional boolean, default true) — replace existing file at same pathMinimal example (most common — all firm users can see):
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/upload" \
-H "Authorization: Bearer fv_token" \
-F "file=@contract.pdf" \
-F "path=matters/12345/evidence/contract.pdf"
With metadata (restrict to uploader only):
curl -X POST "https://filevault.denning.online/api/v1/TS0004/storage/upload" \
-H "Authorization: Bearer fv_token" \
-F "file=@contract.pdf" \
-F "path=matters/12345/evidence/contract.pdf" \
-F 'metadata={"uploaded_by":"ST0001","access_level":"private"}'
Note: If you skip metadata, the file defaults to access_level: "server" (visible to all firm users) and uploaded_by: "system".
download query: pathlookup query: pathexists query: path, type (optional: file or directory, default: file)list query:
directory (optional)recursive (optional boolean)per_page (optional, max 100)copy body — works for both files and directories:
{ "source_path": "...", "dest_path": "..." }
Behavior:
dest_path (must be a full file path). dest_path must NOT already exist (returns 409 Destination already exists); rename upstream if you need to overwrite.source_path to dest_path. Both storage objects and DB rows are duplicated with fresh IDs. Files that already exist at the destination are skipped (not overwritten) and reported in data.errors[].422 if source_path === dest_path, or if dest_path is source_path itself or any path inside it (self-recursion).404.Response (file):
{
"status": "success",
"message": "File copied successfully",
"data": { "type": "file", "id": 123, "source_path": "...", "dest_path": "...", "storage_path": "TS0004/..." }
}
Response (directory):
{
"status": "success",
"message": "Copied 12 of 12 files (0 skipped) from directory",
"data": {
"type": "directory",
"source_path": "matters/123",
"dest_path": "matters/456",
"copied": 12, "skipped": 0, "failed": 0, "total": 12,
"errors": []
}
}
If some children fail or are skipped, status becomes "partial" and failures are listed in errors[].
move body (files only today):
{ "source_path": "...", "dest_path": "..." }
rename body:
{ "path": "...", "new_name": "new-file.pdf" }
mkdir body:
{ "path": "...", "access_level": "private", "created_by": "ST0001" }
rmdir body:
{ "path": "..." }
delete body:
{ "path": "..." }
compress body:
{
"paths": ["matters/123/a.pdf", "matters/123/b.pdf"],
"archive_name": "archive.zip",
"dest_path": "matters/123/archives"
}
extract body:
{
"path": "matters/123/archives/archive.zip",
"dest_path": "matters/123/extracted"
}
share body:
{
"path": "matters/123/contract.pdf",
"expires_at": "2026-03-01T00:00:00Z",
"password": "1234",
"download_limit": 10
}
bulk-copy:
{
"paths": ["matters/123/a.pdf", "matters/123/folder-to-clone"],
"target_path": "matters/456"
}
Each entry in paths may be a file OR a directory. Files go to {target_path}/{basename}; directories are cloned recursively to {target_path}/{basename}/.... Per-path guards, collision skips, and success/partial status use the same rules as /storage/copy. The response aggregates across all paths:
{
"status": "success",
"message": "Copied 14 item(s), skipped 0, failed 0",
"data": {
"copied": 14, "skipped": 0, "failed": 0, "total": 2,
"files": [ { "id": 123, "path": "matters/456/a.pdf", "original_path": "matters/123/a.pdf" } ],
"directories": [
{ "source_path": "matters/123/folder-to-clone", "dest_path": "matters/456/folder-to-clone",
"copied": 13, "skipped": 0, "failed": 0 }
],
"errors": []
}
}
bulk-move (files only today):
{
"paths": ["matters/123/a.pdf", "matters/123/b.pdf"],
"target_path": "matters/123/archive"
}
bulk-delete:
{
"paths": ["matters/123/a.pdf", "matters/123/b.pdf"]
}
Supported providers:
system, s3, azure, gcp, alibaba, ftp, sftp, local| Method | Endpoint |
|---|---|
| GET | /api/v1/{server_code}/storage/configs |
| POST | /api/v1/{server_code}/storage/configs |
| PUT | /api/v1/{server_code}/storage/configs/{provider} |
| DELETE | /api/v1/{server_code}/storage/configs/{provider} |
| POST | /api/v1/{server_code}/storage/configs/{provider}/test |
| POST | /api/v1/{server_code}/storage/configs/{provider}/activate |
Create/update payload:
{
"provider": "s3",
"config": {},
"name": "Tenant S3",
"description": "Primary tenant storage",
"visibility": "private",
"driver": "aws_s3",
"activate": false
}
| Method | Endpoint |
|---|---|
| GET | /api/v1/{server_code}/storage/migration/status |
| GET | /api/v1/{server_code}/storage/migration/jobs |
| POST | /api/v1/{server_code}/storage/migration/start |
| POST | /api/v1/{server_code}/storage/migration/{migrationId}/cancel |
| POST | /api/v1/{server_code}/storage/migration/{migrationId}/retry |
Start payload:
{
"source_provider": "system",
"destination_provider": "s3",
"delete_source_files": false,
"overwrite_existing": false,
"preserve_directory_structure": true,
"file_pattern": "*.pdf",
"activate_destination": true
}
Notes:
| Method | Endpoint |
|---|---|
| GET | /api/v1/{server_code}/modules |
| POST | /api/v1/{server_code}/modules |
| GET | /api/v1/{server_code}/modules/export |
| GET | /api/v1/{server_code}/modules/health |
| GET | /api/v1/{server_code}/modules/backups |
| GET | /api/v1/{server_code}/modules/{key} |
| DELETE | /api/v1/{server_code}/modules/{key} |
| POST | /api/v1/{server_code}/modules/import |
| POST | /api/v1/{server_code}/modules/backup |
| POST | /api/v1/{server_code}/modules/restore |
| POST | /api/v1/{server_code}/modules/reset |
Still available for older consumers:
| Group | Endpoint Pattern |
|---|---|
| Legacy files | /api/v1/{server_code}/files/* |
| Legacy directories | /api/v1/{server_code}/directories/* |
| File permissions | /api/v1/{server_code}/files/{fileId}/permissions* |
| File-specific shares | /api/v1/{server_code}/files/{fileId}/shares* |
| Share management | /api/v1/{server_code}/shares* |
For new integrations, use /storage/*.
Generate merged documents (conveyancing forms, letters, bills, …) from a matter's
data. Use the unified /templates/document endpoint (it picks the engine
dynamically); the per-engine endpoints below are for forcing a specific one:
| Endpoint | Engine | Output | When to use |
|---|---|---|---|
POST /v1/{sc}/templates/document |
Dynamic — engine: auto|legacy|native |
picks per engine |
preferred entry — one call, engine chosen dynamically |
POST /v1/{sc}/templates/generate-legacy |
Legacy — delegates to gdwmm on the tenant server via DenningAPI WCF |
byte-identical to the legacy system | force legacy / full fidelity |
POST /v1/{sc}/templates/generate |
FileVault-native — OpenXML token-substitution in PHP | re-implementation of the legacy merge | force native (no .NET dependency) |
POST /v1/{sc}/templates/render |
Native, caller-supplied data (no tenant DB) | — | preview / ad-hoc merge data |
POST /v1/{server_code}/templates/document
Same body as generate-legacy plus an engine field:
engine: "auto" (default) — use the native engine when the template is mapped (catalog has a template_path, or one is passed); otherwise fall back to legacy (gdwmm). Templates migrate to native as they gain a mapping — no caller change. The default is set server-side by TEMPLATE_DEFAULT_ENGINE.engine: "legacy" — always gdwmm (100% legacy-identical; resolves report_code itself).engine: "native" — always the PHP engine (requires a mapped template_path; 422 if unmapped).This is the single endpoint dp-api/DO should call so the firm can migrate to its own engine gradually.
Response modes (apply to /document, /generate, /generate-legacy, /render):
Content-Disposition: attachment).?meta=1 — JSON { status, format, bytes, stats, source } (no document body).?encoding=base64 (or base64=1) — JSON { status, format, mime, filename, bytes, base64, source } with the document base64-encoded. Use when the client/WCF stack would corrupt a raw binary body (reading it as a string); base64 is text-safe — decode it to get the exact file. Mirrors the legacy DenningAPI generate response shape.POST /v1/{server_code}/templates/generate-legacy
{
"file_no": "6000-6291",
"report_code": "11199",
"version_id": "1",
"doc_type": "Conveyancing",
"language": "B.M.",
"format": "doc",
"document_name": "16A LPPSA",
"staff_code": "354"
}
file_no (required) — matter file number.report_code (required) — the template's cboTemplate.ReportCode. The legacy engine resolves the template internally; no template_path or catalog mapping is needed.version_id — template version (default 1).doc_type — Conveyancing (default), ConveyancingLetter, Litigation, LitigationLetter, LetterDemand, CKHT, Bill, Voucher, Receipt, Payslip, … (a raw DataMapping enum int is also accepted).language — English (default) or B.M..format — doc (default), docx, pdf, xls, xlsx, png, txt.staff_code — required. The logged-in staff code (e.g. 164). Sent as webuser-id / strGCode; an empty value makes the tenant WCF reject the request with HTTP 406.filename — optional download filename (no extension); defaults to document_name, else document_<report_code>. Sets the Content-Disposition name.lod_serial — only for LOD documents.Returns the document bytes (Content-Disposition: attachment), header X-Generator: legacy:gdwmm. Add ?meta=1 to get { status, format, bytes, source } JSON instead. Errors surface as 502 with the upstream WCF status.
Auth to the tenant WCF uses
webuser-sessionid+webuser-id, configured server-side inconfig/template.php(legacy_wcf). The dev bypass sessiontesttestdenningis testing-only.
POST /v1/{server_code}/templates/generate
{ "file_no": "6000-6291", "report_code": "11199", "version_id": "1", "format": "docx" }
Provide either report_code (+version_id, resolved via the catalog) or an explicit template_path. Optional procedure, doc_type, language. Returns the document bytes (X-Tokens-Replaced / X-Tokens-Remaining headers).
POST /v1/{server_code}/templates/render
{ "template_path": "template/.../{guid}.bin", "merge_data": { "C1Name": "Tan Chee Jian" }, "format": "docx" }
GET /v1/{sc}/templates/catalog?category=&language=&kind=&mapped=yes|no&search=&limit= — list templates. Each row includes a kind: system (built-in, always present) or customized (the tenant's own). Derived from cboTemplate.location (populated ⇒ system; empty ⇒ customized) — the same split gdwmm uses. Filter with ?kind=system / ?kind=customized. Also returns report_code/version + metadata + whether a template_path is mapped.POST /v1/{sc}/templates/catalog — register/update an entry: { report_code, version_id?, template_path?, description?, category?, language?, doc_type?, kind? }.POST /v1/{sc}/templates/catalog/sync — re-seed metadata (incl. kind) from the tenant's cboTemplate.GET /v1/{sc}/templates/download/{report_code}?version_id=&filename= — download the source template itself (unwrapped .docx), resolved via the catalog; or GET /v1/{sc}/templates/download?template_path=.... Supports ?encoding=base64 / ?meta=1.private and server files.partial; always process data.errors.404 typically means missing file/path/config/job.409 typically means conflict (already exists, migration in progress, active config deletion).422 indicates validation errors or unsupported provider input.Setup and integration checklist for engineers working with FileVault.
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan serve
Recommended for async features (migration/jobs):
php artisan queue:work
Every consuming service must provide:
FILEVAULT_URLFILEVAULT_TOKENFILEVAULT_SERVER_CODERequest headers:
Authorization: Bearer <token>X-User-Email: <caller-identity> (use staffCode in Denning services)Use path-based API for all new work:
/api/v1/{server_code}/storage/*Legacy endpoints are still present for compatibility:
/api/v1/{server_code}/files/*/api/v1/{server_code}/directories/*Paths must be server-code-free and follow:
{module}/{recordId}/{sub_path}/{filename}documents/{sub_path}/{filename}Do not send:
path/../)The metadata field on upload is optional and controls file visibility and ownership. If omitted, files default to access_level: "server" (visible to all firm users) and uploaded_by: "system".
Metadata fields:
uploaded_by — staff code or email of uploader (default: "system")access_level — file visibility (default: "server"):
"server" — everyone in the same firm/tenant can see it"private" — only the uploader can see it"public" — anyone with the linkExample (restrict to uploader only):
{
"uploaded_by": "ST0001",
"access_level": "private"
}
Most uploads do not need metadata — just send file and path, and the file will be visible to all users in the firm.
If caller identity is missing (X-User-Email), access checks for private/server files may deny reads.
Per tenant (server_code) you can:
storage/configs)storage/configs/{provider}/test)storage/configs/{provider}/activate)Supported providers:
system, s3, azure, gcp, alibaba, ftp, sftp, localMigration endpoints:
GET /storage/migration/statusGET /storage/migration/jobsPOST /storage/migration/startPOST /storage/migration/{migrationId}/cancelPOST /storage/migration/{migrationId}/retryImportant:
Use these rules in dp-api/dcc-api/other services:
5xx errors with exponential backoff.4xx blindly; fix payload or permissions first.status=partial and retry only failed paths.move, delete), verify state with exists or list.healthy.server_code.X-User-Email is passed from caller context.server_code.partial responses.docs/API_REFERENCE.mddocs/INTEGRATION_GUIDE.mddocs/bruno-collection//integration-guide