Media API Specification¶
Overview¶
The Media API provides endpoints for browsing and uploading media files to S3/CloudFront. It supports two use cases:
- CDN Media Browser — Browse existing images on S3, upload new images with automatic resize/crop/WebP conversion
- Marketing Campaign Uploader — Upload mixed media (image/PDF/audio/video) stored as-is under auto-generated campaign prefixes
Base Path: /media
Content Types:
- Request: application/json (browse, templates) or multipart/form-data (upload)
- Response: application/json
Access: agent, admin
Response Format¶
All endpoints return a TlinqApiResponse object:
CDN Browse¶
POST /media/cdn/browse¶
Browse images in an S3 bucket by purpose and prefix. Returns folders (common prefixes) and image files with CDN URLs. Non-image files are filtered out server-side.
Request Body:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| purpose | string | Yes | Purpose ID (e.g. hotel-photos, offer-images, destination-heroes) |
| subPrefix | string | No | Sub-path relative to the purpose prefix (e.g. dubai/). Empty string for root. |
| continuationToken | string | No | Token from previous response for pagination. Null for first page. |
Request Example:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"purposePrefix": "hotels/photos/",
"currentPrefix": "hotels/photos/dubai/",
"folders": [
"hotels/photos/dubai/spa/",
"hotels/photos/dubai/pool/"
],
"files": [
{
"key": "hotels/photos/dubai/hero.webp",
"cdnUrl": "https://cdn.tripqlub.com/hotels/photos/dubai/hero.webp",
"filename": "hero.webp",
"size": 87040,
"lastModified": "2026-03-10T14:22:00Z"
}
],
"nextContinuationToken": "abc123...",
"truncated": true
}
}
Response Fields: | Field | Type | Description | |-------|------|-------------| | purposePrefix | string | The base prefix for the purpose | | currentPrefix | string | Full S3 prefix being listed | | folders | string[] | S3 common prefixes (sub-folders) | | files | CdnFileInfo[] | Image files with CDN URLs | | nextContinuationToken | string | Token for next page (null if no more pages) | | truncated | boolean | Whether more results are available |
CdnFileInfo Fields: | Field | Type | Description | |-------|------|-------------| | key | string | Full S3 object key | | cdnUrl | string | Public CloudFront URL | | filename | string | File name only | | size | long | File size in bytes | | lastModified | string | ISO 8601 timestamp |
Notes:
- Only image files are returned (jpg, jpeg, png, webp, gif)
- Bucket name is never exposed — resolved server-side from purpose ID
- Maximum 100 items per page
- Folders are returned as common prefixes with trailing /
CDN Upload¶
POST /media/cdn/upload¶
Upload an image, resize/crop to template dimensions, convert to WebP, and store on S3.
Content-Type: multipart/form-data
Form Fields:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| file | binary | Yes | Image file (JPEG, PNG, WebP, GIF, TIFF). Max 20 MB input. |
| purpose | string | Yes | Purpose ID (e.g. hotel-photos) |
| subPrefix | string | No | Sub-path relative to purpose prefix |
| templateId | string | Yes | Image template ID (determines output dimensions) |
| basename | string | Yes | Descriptive base name (e.g. pool-view). No extension. |
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"cdnUrl": "https://cdn.tripqlub.com/hotels/photos/dubai/pool-view-a3f9c21b.webp",
"key": "hotels/photos/dubai/pool-view-a3f9c21b.webp",
"size": 87040,
"filename": "pool-view-a3f9c21b.webp"
}
}
Processing Pipeline:
1. MIME type detected from file bytes (Tika magic-byte detection — extension and Content-Type header are ignored)
2. Template dimensions resolved server-side from templateId
3. Image resized and centre-cropped to template dimensions (Thumbnailator)
4. Converted to WebP, quality tuned to target ≤ 100 KB output
5. Content-Type: image/webp and Cache-Control: public, max-age=31536000 set on S3 object
6. Filename: {sanitizedBasename}-{8-char-hex-suffix}.webp
Error Responses: | Error Code | Condition | |------------|-----------| | INVALID_PARAMETER | Missing required field | | INVALID_PARAMETER | File exceeds 20 MB | | INVALID_PARAMETER | MIME type not an allowed image type | | INVALID_PARAMETER | Unknown purpose ID | | INVALID_PARAMETER | Unknown template ID | | INVALID_PARAMETER | Basename empty after sanitization |
CDN Templates¶
POST /media/cdn/templates¶
List available image templates with their dimensions. Called once by the frontend to populate the template dropdown.
Request Body:
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": [
{ "id": "card-header", "label": "Card Header", "width": 600, "height": 400 },
{ "id": "page-banner", "label": "Page Banner", "width": 1920, "height": 400 },
{ "id": "insta-post", "label": "Instagram Post", "width": 1200, "height": 1200 },
{ "id": "thumb-square", "label": "Thumbnail (Square)", "width": 300, "height": 300 },
{ "id": "wide-hero", "label": "Wide Hero", "width": 1600, "height": 900 }
]
}
Notes:
- Templates are defined in config/nts-client.xml under <MediaConfig><Templates>
- Adding a new template is a config-only change — no code required
- Dimensions are informational for the UI; the server always resolves them from the template ID
Marketing Upload¶
POST /media/marketing/upload¶
Upload a file for a marketing campaign. Files are stored as-is with no image processing. The S3 prefix is auto-derived from the campaign name and ID.
Content-Type: multipart/form-data
Form Fields: | Field | Type | Required | Description | |-------|------|----------|-------------| | file | binary | Yes | File to upload (image, PDF, audio, or video) | | campaignId | integer | Yes | Marketing campaign ID | | basename | string | Yes | Descriptive base name without extension |
Response Structure:
{
"apiStatus": { "errorCode": "OK", "errorMessage": "Success" },
"apiData": {
"cdnUrl": "https://cdn.tripqlub.com/campaigns/summer-dubai-2026-1042/main-flyer-a3f9c21b.pdf",
"key": "campaigns/summer-dubai-2026-1042/main-flyer-a3f9c21b.pdf",
"filename": "main-flyer-a3f9c21b.pdf",
"size": 4521984,
"contentType": "application/pdf",
"fileType": "pdf"
}
}
Accepted File Types and Size Limits:
| Type | Accepted MIME Types | Max Size |
|---|---|---|
| Image | jpeg, png, webp, gif, tiff | 20 MB |
| application/pdf | 10 MB | |
| Audio | mpeg, mp4, wav, ogg | 50 MB |
| Video | mp4, quicktime, avi, webm | 500 MB |
Processing:
1. Campaign loaded by ID — 404 if not found
2. S3 prefix derived: campaigns/{slug}-{campaignId}/ (e.g. campaigns/summer-dubai-2026-1042/)
3. MIME type detected from file bytes (not extension or Content-Type header)
4. Validated against configured file type groups
5. File size checked against per-type maximum
6. Extension derived from MIME-to-extension lookup table
7. File stored as-is — no processing or conversion
8. Content-Type from validated MIME, Cache-Control: public, max-age=31536000
MIME-to-Extension Mapping:
| MIME Type | Extension |
|---|---|
| image/jpeg | .jpg |
| image/png | .png |
| image/webp | .webp |
| image/gif | .gif |
| image/tiff | .tiff |
| application/pdf | |
| audio/mpeg | .mp3 |
| audio/mp4 | .m4a |
| audio/wav | .wav |
| audio/ogg | .ogg |
| video/mp4 | .mp4 |
| video/quicktime | .mov |
| video/x-msvideo | .avi |
| video/webm | .webm |
Error Responses: | Error Code | Condition | |------------|-----------| | INVALID_PARAMETER | Missing required field | | INVALID_PARAMETER | MIME type not in any configured file type group | | INVALID_PARAMETER | File size exceeds per-type maximum | | NOTFOUND | Campaign not found |
Security¶
| Concern | Mitigation |
|---|---|
| Bucket name exposure | Never in request/response — resolved server-side from purpose ID |
| Path traversal (basename) | Sanitized: lowercase, only [a-z0-9-], .. and / stripped, max 64 chars |
| Path traversal (subPrefix) | Validated: no .., no leading /, only [a-zA-Z0-9-_/] |
| MIME type spoofing | Tika magic-byte detection — Content-Type header and file extension ignored |
| Template dimension spoofing | Client sends only templateId — dimensions resolved server-side |
| Campaign prefix injection | Client sends only campaignId — prefix derived from DB record server-side |
| Oversized uploads | Server-side size check before any processing; Jetty 500 MB multipart limit as backstop |
| AWS credential leak | DefaultCredentialsProvider — no credentials in config files |
Configuration¶
Media configuration is in config/nts-client.xml under <MediaConfig>. See config/nts-client.xml for the full XML structure.
Purposes¶
Each purpose maps to a bucket, prefix, and CDN base URL. Adding a purpose is config-only.
Templates¶
Each template defines output dimensions for CDN image uploads. Adding a template is config-only.
Marketing File Types¶
File type groups with accepted MIME types and size limits are configured under the marketing-campaigns purpose's <MarketingUploader> element.
IAM Permissions¶
The application IAM role/user requires the following S3 actions on the media buckets:
| Action | Purpose |
|---|---|
s3:ListBucket |
Browse endpoint (list objects and common prefixes) |
s3:GetObject |
Read object metadata (HeadObject is covered by GetObject) |
s3:PutObject |
Upload objects |
s3:PutObjectAcl |
Set public-read ACL on uploaded objects for CDN access |
s3:ListBucket must be on the bucket ARN (no /*). All other actions must be on the object ARN (with /*).