File Endpoints
File endpoints expose a storage directory as a REST API, so your callers can upload files via multipart form POST, retrieve them by file ID, and list what's available per endpoint. File type restrictions, environment scoping, and the base directory all live in the endpoint configuration.
Configuration
You define one with endpoints/File/{EndpointName}/entity.json:
{
"StorageType": "Local",
"BaseDirectory": "documents",
"AllowedExtensions": [".pdf", ".docx", ".xlsx", ".txt"],
"IsPrivate": false,
"AllowedEnvironments": ["prod", "test"]
}Configuration properties
| Property | Required | Type | Description |
|---|---|---|---|
StorageType |
Yes | string | Storage backend. Currently Local |
BaseDirectory |
Yes | string | Path under files/ where uploaded files are stored. Supports placeholders |
AllowedExtensions |
No | array | File extensions accepted by this endpoint. Empty array allows all non-blocked types |
IsPrivate |
No | boolean | Exclude from OpenAPI documentation. Defaults to false |
AllowedEnvironments |
No | array | Environments where this endpoint responds |
Base directory placeholders
BaseDirectory supports dynamic path segments:
| Placeholder | Resolves to |
|---|---|
{env} |
Environment name |
{year} |
Current year (2025) |
{month} |
Current month (01 to 12) |
{date} |
Current date (2025-01-15) |
{ "BaseDirectory": "backups/{env}/{year}/{month}" }Files land at paths like files/prod/2025/01/database-backup.sql.
Namespaces
File endpoints support namespaces (since v1.7.0). Place the endpoint under endpoints/Files/{Namespace}/{Name}/entity.json (or set Namespace in entity.json) and it is served at /api/{env}/files/{Namespace}/{Name}/.... All operations (upload, download, delete, list) resolve the namespace, and returned download URLs include it so they round-trip. Non-namespaced endpoints keep their existing /api/{env}/files/{Name} URLs. Reserved words (api, docs, openapi, health, admin, system, composite, webhook, files) cannot be used as a namespace; the loader skips such endpoints at startup and the Web UI validator rejects them.
API operations
Upload a file
POST /api/{env}/files/{EndpointName}
Authorization: Bearer YOUR_TOKEN
Content-Type: multipart/form-data
file=@report.pdfcurl -X POST "https://your-api/api/500/files/Documents" \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@report.pdf"List files
GET /api/{env}/files/{EndpointName}/list
Authorization: Bearer YOUR_TOKEN{
"success": true,
"count": 1,
"value": [
{
"fileId": "abc123fileId",
"fileName": "report.pdf",
"contentType": "application/pdf",
"size": 125679,
"lastModified": "2025-03-21T10:00:00Z",
"url": "/api/500/files/Documents/abc123fileId",
"isInMemoryOnly": false
}
],
"nextLink": null
}Download a file
Use the fileId from the list response:
GET /api/{env}/files/{EndpointName}/{fileId}
Authorization: Bearer YOUR_TOKENcurl -X GET "https://your-api/api/500/files/Documents/abc123fileId" \
-H "Authorization: Bearer YOUR_TOKEN" \
-o "downloaded-report.pdf"File type restrictions
Specify which extensions callers can upload via AllowedExtensions. Any extension not listed is rejected.
Regardless of AllowedExtensions, the following types are always blocked:
.exe, .dll, .bat, .sh, .cmd, .msi, .vbs
The default maximum file size is 50MB. This is configurable in system settings.
JavaScript integration
// List files
const listResponse = await fetch('/api/500/files/Documents/list', {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await listResponse.json();
// Download a file by ID
const fileId = data.files[0].fileId;
const fileResponse = await fetch(`/api/500/files/Documents/${fileId}`, {
headers: { 'Authorization': 'Bearer ' + token }
});
const blob = await fileResponse.blob();
// Trigger browser download
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = data.files[0].fileName;
a.click();
window.URL.revokeObjectURL(url);INFO
File endpoints require the Authorization header. Direct <img src> or <embed src> tags in HTML will not work unless authentication is handled via JavaScript.
Troubleshooting
File list returns empty: If BaseDirectory uses {env}, verify Portway created the correct path. A literal folder named {env} indicates the placeholder was not resolved. Move files to the correct path under the actual environment name.
"File size exceeds maximum": The file exceeds the 50MB default. Either compress the file or increase the limit in system settings.
"Extension not allowed": Add the extension to AllowedExtensions in the endpoint config, or convert the file to an allowed format.
"File not found" on download: Confirm you are using the fileId from a list response (not the filename), that you are requesting from the correct environment, and that the file has not been deleted.
Files are stored at predictable paths: files/{environment}/{baseDirectory}/{filename}. Application logs at log/portwayapi-[date].log record upload and download events.
Portway