# Product Updates API Reference

**Last Updated**: 2026-04-08  
**Status**: Production Ready ✅

## Overview

This document provides a complete reference for all API endpoints used by the Product Updates CMS system. All endpoints return JSON responses unless otherwise specified. Public feed endpoints return RSS/JSON Feed format.

## Authentication

### Admin Endpoints

Admin endpoints require session-based authentication. The user must complete sign-in at `/produkt-updates-admin` before accessing these endpoints.

**Primary sign-in (default when Google OAuth credentials are set):** Google OAuth — authorization code flow, callback `/produkt-updates-admin/oauth/callback`, **verified @ordio.com** only. See [PRODUCT_UPDATES_ADMIN_OAUTH.md](PRODUCT_UPDATES_ADMIN_OAUTH.md).

**Legacy sign-in:** If `PRODUCT_UPDATES_ADMIN_OAUTH_ENABLED` is disabled (`0` / `false`) or Google Client ID/Secret are missing, password login uses `PRODUCT_UPDATES_ADMIN_LEGACY_PASSWORD_HASH` (environment; no committed hash in `admin_config.php`).

**Session Requirements**:

- Valid session cookie (secure defaults via `produkt_updates_admin_bootstrap_session()`)
- Session authenticated flag: `$_SESSION['produkt_updates_admin_authenticated'] === true`
- Session timeout: 30 minutes of inactivity on full admin page loads
- **Remember me:** Upload, autosave, and SEO generate APIs call `produkt_updates_admin_try_remember_cookie_login()` so a valid HttpOnly remember cookie can re-establish the session (e.g. new PHP session id while the long-lived remember token is still valid). See [PRODUCT_UPDATES_ADMIN_OAUTH.md](PRODUCT_UPDATES_ADMIN_OAUTH.md) § Remember me.

**Error Response** (401 Unauthorized):

```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Diagnostic Endpoints

Some diagnostic endpoints support direct password authentication via GET/POST parameter for debugging purposes:

```
?password=vXbTz6PqdY6UdexSxlp5
```

## Rate Limiting

### Limits

- **Image Proxy**: 100 requests/minute per IP
- **Diagnostic Endpoints**: 10 requests/minute per IP
- **Migration Scripts**: 1 request/5 minutes per session

### Rate Limit Response (429 Too Many Requests)

```json
{
  "error": "Too Many Requests",
  "message": "Rate limit exceeded for image requests.",
  "retry_after": 60
}
```

Headers:

- `Retry-After: 60` (seconds until retry allowed)

## Endpoints

### 0. JSON Feed (Public)

**Endpoint**: `/produkt-updates/feed.json`

**Method**: `GET`

**Authentication**: Not required (public endpoint)

**Purpose**: JSON Feed 1.1 of product updates for Ordio and feed readers

**Query Parameters**:

| Parameter | Values | Description |
|-----------|--------|-------------|
| `in_app_banner_only` | `1` | Return only items with `show_in_app_banner: true` |
| `type` | `big_feature` \| `small_improvement` | Filter by item type |
| `platform` | `desktop` \| `mobile` | Return only items available on the specified platform |
| `month` | `YYYY-MM` | Return only items from that month (e.g. `2025-02`) |
| `year` | `YYYY` | Return only items from that year |
| `from` | `YYYY-MM-DD` | Return only items on or after this date |
| `to` | `YYYY-MM-DD` | Return only items on or before this date |

**Request Examples**:

```
GET /produkt-updates/feed.json
GET /produkt-updates/feed.json?in_app_banner_only=1
GET /produkt-updates/feed.json?type=big_feature
GET /produkt-updates/feed.json?type=small_improvement
GET /produkt-updates/feed.json?platform=mobile
GET /produkt-updates/feed.json?month=2025-02
GET /produkt-updates/feed.json?year=2025
GET /produkt-updates/feed.json?from=2025-02-01&to=2025-02-28
GET /produkt-updates/feed.json?in_app_banner_only=1&type=big_feature
```

**Success Response** (200 OK):

- Content-Type: `application/feed+json; charset=utf-8`
- Headers: `Access-Control-Allow-Origin: *`, `Cache-Control: public, max-age=300`, `ETag`, `Last-Modified`
- Body: JSON Feed 1.1 with `items` array; each item has `_ordio` extension: `{ type, show_in_app_banner, platforms }`

**304 Not Modified**: Returned when `If-None-Match` or `If-Modified-Since` matches

**CORS**: Cross-origin requests allowed (`Access-Control-Allow-Origin: *`)

**See**: [PRODUCT_UPDATES_FEED.md](./PRODUCT_UPDATES_FEED.md) for full specification

---

### 0b. RSS Feed (Public)

**Endpoint**: `/produkt-updates/feed.xml`

**Method**: `GET`

**Authentication**: Not required (public endpoint)

**Purpose**: RSS 2.0 feed of product updates for feed readers and Ordio

**Query Parameters**: Same as JSON Feed – `in_app_banner_only`, `type`, `platform`, `month`, `year`, `from`, `to`

**Request Examples**:

```
GET /produkt-updates/feed.xml
GET /produkt-updates/feed.xml?month=2025-02
GET /produkt-updates/feed.xml?in_app_banner_only=1
```

**Success Response** (200 OK):

- Content-Type: `application/rss+xml; charset=utf-8`
- Headers: `Cache-Control: public, max-age=300`, `ETag`, `Last-Modified`
- Body: RSS 2.0 with ordio namespace; each item has `<ordio:type>`, `<ordio:show_in_app_banner>`, `<ordio:platforms>` (comma-separated)

**304 Not Modified**: Returned when `If-None-Match` or `If-Modified-Since` matches

**See**: [PRODUCT_UPDATES_FEED.md](./PRODUCT_UPDATES_FEED.md) for full specification

---

### 1. Image Upload

**Endpoint**: `/v2/api/produkt-updates-upload.php`

**Method**: `POST`

**Authentication**: Required (admin session)

**Purpose**: Upload and optimize images for product updates

**Request**:

- Content-Type: `multipart/form-data`
- Field name: `image` (supports single or multiple files)
- File types: JPEG, PNG, WebP, GIF
- Max file size: 5MB per file
- Max dimensions: 4000x4000px

**Request Example**:

```javascript
const formData = new FormData();
formData.append("image", fileInput.files[0]);
// Or multiple files:
// formData.append('image[]', file1);
// formData.append('image[]', file2);

fetch("/v2/api/produkt-updates-upload.php", {
  method: "POST",
  body: formData,
  credentials: "include", // Include session cookie
});
```

**Success Response** (200 OK):

```json
{
  "success": true,
  "files": [
    {
      "path": "/wp-content/uploads/produkt-updates/img_123.webp",
      "filename": "img_123.webp",
      "original_size": 1024000,
      "optimized_size": 512000,
      "savings_percent": 50,
      "width": 1920,
      "height": 1080,
      "format": "webp"
    }
  ]
}
```

**Error Responses**:

Invalid file type (400 Bad Request):

```json
{
  "success": false,
  "message": "Invalid file type. Allowed types: JPEG, PNG, WebP, GIF"
}
```

File too large (400 Bad Request):

```json
{
  "success": false,
  "message": "File size exceeds maximum allowed size (5MB)"
}
```

Upload directory not writable (500 Internal Server Error):

```json
{
  "success": false,
  "message": "Upload directory is not writable"
}
```

**Image Optimization**:

- Automatic resizing to max 1920x1920px (maintains aspect ratio)
- JPEG compression: Quality 85
- PNG compression: Level 6
- WebP conversion when possible
- File size reduction: Typically 30-70%

**Storage Location**:
Images are stored in the primary persistent location determined by `findWritableImageDir()`:

- Primary: `/wp-content/uploads/produkt-updates/`
- Fallbacks: Root-level `/writable/images/`, `/data/images/`, then `v2/data/images/`, etc.

---

### 1b. Admin autosave (JSON merge)

**Endpoint**: `/v2/api/produkt-updates-autosave.php`

**Method**: `POST`

**Authentication**: Required (same admin session as upload)

**Content-Type**: `application/json`

**Purpose**: Debounced background save from the Product Updates admin modals. Merges sanitized fields into an existing feature/improvement or **creates a draft stub** when `id` is empty. Always runs through **`saveUpdatesData()`** (regeneration + atomic write).

**Request body**:

| Field | Type | Description |
|-------|------|-------------|
| `entity` | string | `feature` or `improvement` |
| `id` | string | Existing row id, or empty string to create a new draft |
| `client_revision` | int | Monotonic counter from the client; echoed in the response for stale-response detection |
| `fields` | object | Subset of row fields to merge (sanitized server-side) |

**Feature `fields` keys** (all optional; only present keys are updated): `title`, `tag`, `description`, `page_content`, `read_more_link`, `published_date`, `featured_image`, `platforms` (array), `show_in_app_banner`, `seo_title`, `meta_description`, `featured_image_alt`.

**Improvement `fields` keys**: `title`, `tag`, `description`, `published_date`, `platforms`, `show_in_app_banner`.

**Success** (200):

```json
{
  "success": true,
  "id": "generated-or-existing-id",
  "client_revision": 3,
  "message": "Saved"
}
```

**Errors**: `401` unauthenticated, `400` invalid JSON/entity, `404` unknown id when `id` was non-empty, `500` load/save failure.

**500 save failure** (optional fields for triage, German `hint` for operators):

```json
{
  "success": false,
  "message": "Save failed",
  "error_code": "data_dir_not_writable",
  "hint": "SSOT-Verzeichnis oder produkt_updates.json ist für den Webserver nicht beschreibbar …"
}
```

Codes include: `data_dir_not_writable`, `data_dir_mkdir_failed`, `atomic_write_failed`, `json_encode_failed`, `featured_image_integrity`, `post_save_verify_failed`, `exception`, `save_failed` (fallback). Set via `produktUpdatesGetLastSaveFailure()` in [`v2/includes/produkt-updates-save-updates-data.php`](../../v2/includes/produkt-updates-save-updates-data.php).

**Note**: Autosave does **not** change `publication_status`; use **Entwurf speichern** / **Veröffentlichen / Planen** on the main form for lifecycle changes.

---

### 2. Image Proxy

**Endpoint**: `/v2/api/serve-produkt-updates-image.php`

**Method**: `GET`

**Authentication**: Not required (public endpoint)

**Purpose**: Serve images from filesystem locations (including non-web-accessible directories)

**Rate Limiting**: 100 requests/minute per IP

**Request Parameters**:

- `file` (required): Filename only (no paths, no directory traversal)

**Request Example**:

```
GET /v2/api/serve-produkt-updates-image.php?file=img_123.webp
```

**Security Checks**:

- Blocks directory traversal (`..`, `/`, `\`)
- Validates file extension (jpg, jpeg, png, webp, gif only)
- Only serves from known image directories

**Success Response** (200 OK):

- Content-Type: `image/jpeg`, `image/png`, `image/webp`, or `image/gif`
- Content-Length: File size in bytes
- Cache-Control: `public, max-age=31536000, immutable` (1 year cache)
- ETag: MD5 hash of file path, size, and modification time
- Last-Modified: File modification timestamp
- Vary: `Accept` (for WebP negotiation)

**WebP Auto-Serving**:
If browser supports WebP (via `Accept: image/webp` header) and a WebP version exists, the WebP version is served instead of JPEG/PNG.

**304 Not Modified**:
If client sends `If-None-Match` (ETag) or `If-Modified-Since` headers matching the file, returns 304 Not Modified with no body.

**Error Responses**:

Missing file parameter (400 Bad Request):

```
Missing file parameter
```

Invalid file parameter (400 Bad Request):

```
Invalid file parameter - directory traversal not allowed
```

Invalid file type (400 Bad Request):

```
Invalid file type - only image files allowed
```

Image not found (404 Not Found):

```
Image not found
```

**Image Resolution**:
The endpoint uses `findReadableImage($filename)` to search for images in:

1. WordPress uploads directory (`/wp-content/uploads/produkt-updates/`)
2. Root-level directories (`/writable/images/`, `/data/images/`)
3. V2-level directories (`v2/data/images/produkt-updates/`, `v2/temp/produkt-updates/`, etc.)
4. System temp directory (`/tmp/produkt-updates-images/`) as last resort

---

### 3. Diagnostics

**Endpoint**: `/v2/api/produkt-updates-diagnostics.php`

**Method**: `GET`

**Authentication**: Optional (supports direct password authentication)

**Purpose**: Comprehensive system diagnostics

**Request Parameters**:

- `password` (optional): Direct password authentication for debugging

**Request Example**:

```
GET /v2/api/produkt-updates-diagnostics.php
GET /v2/api/produkt-updates-diagnostics.php?password=vXbTz6PqdY6UdexSxlp5
```

**Success Response** (200 OK):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "status": "ok",
  "php": {
    "version": "7.4.33",
    "sapi": "fpm-fcgi",
    "memory_limit": "256M",
    "max_execution_time": "30",
    "extensions": {
      "json": true,
      "mbstring": true,
      "gd": true,
      "curl": true
    }
  },
  "resolved_paths": {
    "data_file": {
      "path": "/var/www/lexikon/wp-content/uploads/produkt-updates/produkt_updates.json",
      "exists": true,
      "readable": true,
      "writable": true,
      "is_fallback": false
    },
    "image_dir": {
      "path": "/var/www/lexikon/wp-content/uploads/produkt-updates/",
      "url_prefix": "/v2/api/serve-produkt-updates-image.php?file=",
      "exists": true,
      "writable": true,
      "is_fallback": false
    }
  },
  "existing_images": {
    "wp-content/uploads/produkt-updates": {
      "path": "/var/www/lexikon/wp-content/uploads/produkt-updates/",
      "exists": true,
      "count": 5,
      "images": [
        {
          "filename": "img_123.webp",
          "size": 512000,
          "modified": "2025-11-20 10:00:00",
          "url": "/v2/api/serve-produkt-updates-image.php?file=img_123.webp"
        }
      ],
      "note": "Primary location (direct access)"
    },
    "v2/data/images/produkt-updates": {
      "path": "/var/www/lexikon/v2/data/images/produkt-updates/",
      "exists": true,
      "count": 2,
      "images": [
        {
          "filename": "img_456.png",
          "size": 256000,
          "modified": "2025-11-19 15:30:00",
          "url": "/v2/api/serve-produkt-updates-image.php?file=img_456.png"
        }
      ],
      "note": "Primary location (direct access)"
    }
  },
  "image_summary": {
    "total_images": 7,
    "deletable_from_dashboard": 7,
    "not_deletable_from_dashboard": 0,
    "note": "All images can be deleted via dashboard"
  },
  "external_resources": {
    "chartjs_primary": {
      "url": "https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js",
      "available": true,
      "http_code": 200,
      "response_time": "45ms"
    },
    "quilljs": {
      "url": "https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js",
      "available": true,
      "http_code": 200,
      "response_time": "52ms"
    }
  },
  "data": {
    "file_size": 10240,
    "json_valid": true,
    "json_error": null,
    "months_count": 2,
    "current_month": "november-2025"
  },
  "issues": []
}
```

**Key Fields**:

- **`existing_images`** (object): Maps location keys to image information. Each location contains:
  - `path` (string): Absolute filesystem path to directory
  - `exists` (boolean): Whether directory exists
  - `count` (integer): Number of images found in this location
  - `images` (array): Array of image objects with `filename`, `size`, `modified`, and `url`
  - `note` (string): Human-readable note about location type

- **`image_summary`** (object): Summary statistics about all images:
  - `total_images` (integer): Total count across all locations
  - `deletable_from_dashboard` (integer): Images that can be deleted via admin panel
  - `not_deletable_from_dashboard` (integer): Images that cannot be deleted (permission issues)

**Location Keys**:

Location keys are relative paths from the project root (e.g., `wp-content/uploads/produkt-updates`) or special identifiers like `/tmp/produkt-updates-images` for system temp directories.

**Usage in Admin Panel**:

The admin panel's `loadImageLibrary()` function uses this endpoint to:
1. Fetch all available images from all locations
2. Display them in a grid for selection
3. Show usage information (which features use which images)

**Error Response** (401 Unauthorized):

```json
{
  "success": false,
  "message": "Unauthorized. Please provide the correct password or log in to the admin panel."
}
```

---

### 4. Storage Health Check

**Endpoint**: `/v2/api/produkt-updates-storage-health.php`

**Method**: `GET`

**Authentication**: Optional (supports direct password authentication)

**Purpose**: Quick health check for storage locations

**Request Parameters**:

- `password` (optional): Direct password authentication for debugging

**Request Example**:

```
GET /v2/api/produkt-updates-storage-health.php
GET /v2/api/produkt-updates-storage-health.php?password=vXbTz6PqdY6UdexSxlp5
```

**Success Response** (200 OK):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "status": "healthy",
  "data_file": {
    "location": "/wp-content/uploads/produkt-updates/produkt_updates.json",
    "exists": true,
    "writable": true,
    "persistent": true
  },
  "images_directory": {
    "location": "/wp-content/uploads/produkt-updates/",
    "exists": true,
    "writable": true,
    "persistent": true
  },
  "warnings": []
}
```

**Warning Response** (200 OK with warnings):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "status": "warning",
  "data_file": {
    "location": "/tmp/produkt_updates.json",
    "exists": true,
    "writable": true,
    "persistent": false
  },
  "images_directory": {
    "location": "/tmp/produkt-updates-images/",
    "exists": true,
    "writable": true,
    "persistent": false
  },
  "warnings": [
    "Data file is stored in volatile /tmp/ directory. Data will be lost on server restart.",
    "Images are stored in volatile /tmp/ directory. Images will be lost on server restart."
  ]
}
```

**Error Response** (401 Unauthorized):

```json
{
  "success": false,
  "message": "Unauthorized"
}
```

---

### 5. Server Investigation

**Endpoint**: `/v2/api/produkt-updates-server-investigation.php`

**Method**: `GET`

**Authentication**: Optional (supports direct password authentication)

**Purpose**: Comprehensive server environment investigation

**Request Parameters**:

- `password` (optional): Direct password authentication for debugging

**Success Response** (200 OK):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "php": {
    "version": "7.4.33",
    "sapi": "fpm-fcgi",
    "extensions": {...},
    "ini_settings": {
      "upload_max_filesize": "10M",
      "post_max_size": "10M",
      "memory_limit": "256M"
    }
  },
  "filesystem": {
    "temp_dir": "/tmp",
    "document_root": "/var/www/lexikon",
    "writable_directories": [...],
    "permissions": {...}
  },
  "storage": {
    "data_file_locations": [...],
    "image_directory_locations": [...]
  }
}
```

---

### 6. Data Location Check

**Endpoint**: `/v2/api/produkt-updates-data-location-check.php`

**Method**: `GET`

**Authentication**: Optional (supports direct password authentication)

**Purpose**: Compare data file locations between admin and public views

**Success Response** (200 OK):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "writable_location": "/wp-content/uploads/produkt-updates/produkt_updates.json",
  "readable_location": "/wp-content/uploads/produkt-updates/produkt_updates.json",
  "locations_match": true,
  "data_exists": true,
  "data_identical": true
}
```

---

### 7. Image Investigation

**Endpoint**: `/v2/api/produkt-updates-image-investigation.php`

**Method**: `GET`

**Authentication**: Optional (supports direct password authentication)

**Purpose**: Investigate image storage locations and find images

**Success Response** (200 OK):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "writable_image_directory": {
    "path": "/wp-content/uploads/produkt-updates/",
    "url_prefix": "/v2/api/serve-produkt-updates-image.php?file=",
    "exists": true,
    "writable": true
  },
  "all_image_directories": [...],
  "images_found": [
    {
      "filename": "img_123.webp",
      "path": "/wp-content/uploads/produkt-updates/img_123.webp",
      "size": 512000,
      "last_modified": "2025-11-20 10:00:00"
    }
  ],
  "total_images": 5
}
```

---

## Admin Diagnostic Endpoints

These endpoints are located in `/v2/admin/produkt-updates/` and require admin authentication.

### 8. Discover Writable Directories

**Endpoint**: `/v2/admin/produkt-updates/discover-writable-directories.php`

**Method**: `GET`

**Authentication**: Required (admin session or direct password)

**Purpose**: Test all possible directories for writability

**Request Parameters**:

- `password` (optional): Direct password authentication

**Success Response** (200 OK):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "base_directory": "/var/www/lexikon",
  "tested_directories": 29,
  "writable_directories": [
    {
      "path": "/var/www/lexikon/wp-content/uploads/",
      "type": "images",
      "exists": true,
      "writable": true,
      "persistent": true,
      "priority": 1,
      "description": "WordPress uploads directory"
    }
  ],
  "summary": {
    "total_tested": 29,
    "total_writable": 4,
    "persistent_writable": 4,
    "volatile_writable": 0
  }
}
```

---

### 9. Analyze Production Setup

**Endpoint**: `/v2/admin/produkt-updates/analyze-production-setup.php`

**Method**: `GET`

**Authentication**: Required (admin session or direct password)

**Purpose**: Analyze production environment and provide recommendations

**Success Response** (200 OK):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "current_setup": {
    "data_file": {
      "location": "/wp-content/uploads/produkt-updates/produkt_updates.json",
      "persistent": true,
      "writable": true
    },
    "images_directory": {
      "location": "/wp-content/uploads/produkt-updates/",
      "persistent": true,
      "writable": true
    }
  },
  "recommendations": [],
  "status": "optimal"
}
```

---

### 10. List Images

**Endpoint**: `/v2/admin/produkt-updates/list-images.php`

**Method**: `GET`

**Authentication**: Required (admin session or direct password)

**Purpose**: List all images and compare with JSON data

**Success Response** (200 OK):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "status": "success",
  "writable_image_directory": "/wp-content/uploads/produkt-updates/",
  "images_in_writable_dir": {
    "img_123.webp": {
      "full_path": "/var/www/lexikon/wp-content/uploads/produkt-updates/img_123.webp",
      "url": "/v2/api/serve-produkt-updates-image.php?file=img_123.webp",
      "size": 512000,
      "last_modified": "2025-11-20 10:00:00"
    }
  },
  "images_in_json": {
    "img_123.webp": "/v2/api/serve-produkt-updates-image.php?file=img_123.webp"
  },
  "missing_images": {},
  "unreferenced_images": {},
  "statistics": {
    "total_referenced_images": 5,
    "total_found_images": 5,
    "total_missing_images": 0,
    "images_in_writable_dir": 5,
    "total_unreferenced_images": 0
  }
}
```

---

### 11. Migrate Missing Images

**Endpoint**: `/v2/admin/produkt-updates/migrate-missing-images.php`

**Method**: `GET`

**Authentication**: Required (admin session or direct password)

**Purpose**: Find and migrate images from old locations to primary storage

**Request Parameters**:

- `action` (required): `migrate` to start migration
- `password` (optional): Direct password authentication

**Request Example**:

```
GET /v2/admin/produkt-updates/migrate-missing-images.php?action=migrate&password=vXbTz6PqdY6UdexSxlp5
```

**Success Response** (200 OK):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "status": "completed",
  "message": "Image migration process finished. 3 images migrated, 3 JSON paths updated.",
  "summary": {
    "total_referenced_images": 5,
    "total_missing_images_before_migration": 3,
    "total_migrated_images": 3,
    "total_failed_migrations": 0,
    "total_json_path_updates": 3,
    "total_missing_after_migration": 0
  },
  "details": [
    {
      "item_id": "feature_123",
      "title": "Feature Title",
      "status": "migrated",
      "old_path": "/tmp/produkt-updates-images/img_123.webp",
      "new_path": "/wp-content/uploads/produkt-updates/img_123.webp",
      "new_json_path": "/v2/api/serve-produkt-updates-image.php?file=img_123.webp",
      "message": "Image successfully copied and JSON path updated."
    }
  ]
}
```

**Idle Response** (200 OK, no action):

```json
{
  "timestamp": "2025-11-20 10:30:00",
  "status": "idle",
  "message": "Migration script ready. Use ?action=migrate to start."
}
```

---

## Error Codes

### HTTP Status Codes

- **200 OK**: Request successful
- **304 Not Modified**: Resource not modified (caching)
- **400 Bad Request**: Invalid request parameters
- **401 Unauthorized**: Authentication required
- **404 Not Found**: Resource not found
- **429 Too Many Requests**: Rate limit exceeded
- **500 Internal Server Error**: Server error

### Error Response Format

All error responses follow this format:

```json
{
  "success": false,
  "message": "Human-readable error message",
  "error": "Error code (optional)",
  "details": {} // Additional error details (optional)
}
```

## Best Practices

### 1. Always Check Response Status

```javascript
const response = await fetch('/v2/api/produkt-updates-upload.php', {...});
const data = await response.json();

if (!data.success) {
  console.error('Upload failed:', data.message);
  return;
}

// Process successful response
data.files.forEach(file => {
  console.log('Uploaded:', file.path);
});
```

### 2. Handle Rate Limiting

```javascript
const response = await fetch(
  "/v2/api/serve-produkt-updates-image.php?file=img.webp"
);

if (response.status === 429) {
  const retryAfter = response.headers.get("Retry-After");
  console.log(`Rate limited. Retry after ${retryAfter} seconds`);
  // Implement retry logic
}
```

### 3. Include Credentials for Admin Endpoints

```javascript
fetch("/v2/api/produkt-updates-upload.php", {
  method: "POST",
  body: formData,
  credentials: "include", // Required for session cookies
});
```

### 4. Validate File Before Upload

```javascript
const file = fileInput.files[0];

// Check file size (5MB max)
if (file.size > 5 * 1024 * 1024) {
  alert("File size exceeds 5MB");
  return;
}

// Check file type
const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/gif"];
if (!allowedTypes.includes(file.type)) {
  alert("Invalid file type");
  return;
}
```

### 5. CLI verification (includes / visibility / SSOT)

From repo root:

```bash
php v2/scripts/dev-helpers/verify-produkt-updates-admin-includes.php
php v2/scripts/dev-helpers/verify-produkt-updates-publication.php
php v2/scripts/dev-helpers/verify-produkt-updates-ssot.php
```

The admin-includes script catches missing shared requires (e.g. SSOT load before `saveUpdatesData` / `regenerateMonthsFromContent`).

## Related Documentation

- [System Overview](./PRODUCT_UPDATES_SYSTEM_OVERVIEW.md) - High-level architecture
- [Storage System](./PRODUCT_UPDATES_STORAGE.md) - Storage architecture details
- [Development Guide](./PRODUCT_UPDATES_DEVELOPMENT_GUIDE.md) - How to extend endpoints
