Skip to content

Media API Specification

Overview

The Media API provides endpoints for browsing and uploading media files to S3/CloudFront. It supports two use cases:

  1. CDN Media Browser — Browse existing images on S3, upload new images with automatic resize/crop/WebP conversion
  2. 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:

{
  "apiStatus": {
    "errorCode": "OK",
    "errorMessage": "Success"
  },
  "apiData": { ... }
}


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:

{
  "purpose": "hotel-photos",
  "subPrefix": "dubai/",
  "continuationToken": null
}

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
PDF 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 .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 /*).