# 4kwallpaper API

Cloudflare Worker proxy for [4kwallpapers.com](https://4kwallpapers.com/).  
Forwards wallpaper metadata and download URLs to client apps with **KV cache** and **edge cache headers**. No object storage — data forwarding only.

| | |
|---|---|
| **Base URL** | `https://4kwallpaper-api.joeannsteinbock4351734.workers.dev` |
| **Version** | `v1` |
| **Auth** | Optional `X-API-Key` (if `API_KEY` is configured) |
| **CORS** | `Access-Control-Allow-Origin: *` |
| **Source** | Upstream HTML + `/search-api` suggest endpoint |

**Live links:** [Health](/v1/health) · [Categories](/v1/categories) · [Recent](/v1/wallpapers?feed=recent) · [This doc (HTML)](/docs) · [Raw Markdown](/docs.md)

---

## Quick start

```bash
BASE=https://4kwallpaper-api.joeannsteinbock4351734.workers.dev

# Health
curl "$BASE/v1/health"

# Home feed
curl "$BASE/v1/wallpapers?feed=recent&page=1"

# Search
curl "$BASE/v1/search?q=goku"

# Detail (path from list item)
curl "$BASE/v1/wallpapers/nature/stac-a-phris-arch-26748"

# Download 4K (302 redirect to origin CDN)
curl -I "$BASE/v1/download/nature/stac-a-phris-arch-26748?resolution=3840x2160"
```

---

## Authentication

By default the API is open.

If the worker env var `API_KEY` is set, every request (except `OPTIONS`) must include:

```http
X-API-Key: <your-key>
```

or

```http
Authorization: Bearer <your-key>
```

Missing/invalid key → `401`:

```json
{ "error": "Unauthorized" }
```

---

## Caching

Responses include cache metadata so clients and CDNs behave well.

| Header | Meaning |
|--------|---------|
| `Cache-Control` | `public, max-age=…, s-maxage=…, stale-while-revalidate=…` |
| `CDN-Cache-Control` | Edge-oriented max-age |
| `X-Cache` | `HIT` \| `MISS` \| `BYPASS` |
| `X-Cache-Source` | `kv` \| `edge` \| `origin` |

### Default TTLs (seconds)

| Data | Env var | Default |
|------|---------|---------|
| Lists / feeds / categories pages | `CACHE_TTL_LIST` | `900` |
| Wallpaper detail | `CACHE_TTL_DETAIL` | `86400` |
| Search results | `CACHE_TTL_SEARCH` | `1800` |
| Category catalog | `CACHE_TTL_CATEGORIES` | `86400` |
| Suggest | `CACHE_TTL_SUGGEST` | `3600` |
| Proxied images | `CACHE_TTL_IMAGE` | `604800` |

**Flow:** Client → CF edge (Cache API / CDN) → Worker → KV (JSON) → 4kwallpapers.com

---

## Endpoints

### `GET /` · `GET /docs`

Human-readable HTML documentation (this page).

### `GET /docs.md`

Raw Markdown source (`text/markdown`).

### `GET /v1` · `GET /v1/openapi.json`

Machine-readable endpoint map (JSON).

### `GET /v1/health`

Liveness probe.

**Response `200`**

```json
{
  "ok": true,
  "origin": "https://4kwallpapers.com",
  "time": "2026-07-20T17:37:30.309Z"
}
```

---

### `GET /v1/categories`

Catalog of primary categories and named feeds.

**Response `200`**

```json
{
  "categories": [
    { "slug": "anime", "name": "Anime", "path": "/anime/" },
    { "slug": "nature", "name": "Nature", "path": "/nature/" }
  ],
  "feeds": [
    { "slug": "recent", "name": "Recent", "path": "/" },
    { "slug": "popular", "name": "Popular", "path": "/most-popular-4k-wallpapers/" },
    { "slug": "featured", "name": "Featured", "path": "/best-4k-wallpapers/" },
    { "slug": "random", "name": "Random", "path": "/random-wallpapers/" }
  ]
}
```

Primary category slugs:

`abstract` · `animals` · `anime` · `architecture` · `bikes` · `black-dark` · `cars` · `celebrations` · `cute` · `fantasy` · `flowers` · `food` · `games` · `gradients` · `graphics-cgi` · `lifestyle` · `love` · `military` · `minimal` · `movies` · `music` · `nature` · `people` · `photography` · `quotes` · `sci-fi` · `space` · `sports` · `technology` · `world`

---

### `GET /v1/wallpapers`

List wallpapers from a named feed.

| Query | Type | Default | Description |
|-------|------|---------|-------------|
| `feed` | string | `recent` | `recent` \| `popular` \| `featured` \| `random` |
| `page` | int | `1` | Page number (≥ 1) |

**Example**

```http
GET /v1/wallpapers?feed=popular&page=2
```

**Response `200`** — see [Paginated list](#paginated-list)

---

### `GET /v1/categories/:slug`

List wallpapers for a **category** or free-form **tag** page.

| Param / query | Description |
|---------------|-------------|
| `slug` | e.g. `anime`, `nature`, `scotland`, `goku` |
| `page` | Page number (default `1`) |

**Example**

```http
GET /v1/categories/anime?page=1
GET /v1/categories/scotland?page=1
```

**Errors**

- `404` — empty first page for unknown slug

---

### `GET /v1/search`

Full-text / tag search (upstream search page).

| Query | Required | Description |
|-------|----------|-------------|
| `q` or `query` | yes | Search string (spaces → hyphens upstream) |
| `page` | no | Default `1` |

**Example**

```http
GET /v1/search?q=goku&page=1
```

---

### `GET /v1/search/suggest`

Tag autocomplete (proxies upstream `/search-api?action=tagsearch`).

| Query | Required |
|-------|----------|
| `q` or `query` | yes |

**Example**

```http
GET /v1/search/suggest?q=goku
```

**Response `200`**

```json
{
  "query": "goku",
  "suggestions": [
    "Son Goku",
    "Goku",
    "Ultra Instinct Goku",
    "Goku Black"
  ]
}
```

---

### `GET /v1/wallpapers/:category/:slugId`

Wallpaper detail: tags, thumbnails, **all resolutions**, related items.

| Path part | Example |
|-----------|---------|
| `category` | `nature` |
| `slugId` | `stac-a-phris-arch-26748` (must end with `-{numericId}`) |

**Example**

```http
GET /v1/wallpapers/nature/stac-a-phris-arch-26748
```

**Response `200`**

```json
{
  "id": 26748,
  "title": "Stac a Phris Arch Scotland",
  "category": "Nature",
  "categoryPath": "/nature/",
  "slug": "stac-a-phris-arch",
  "path": "/nature/stac-a-phris-arch-26748.html",
  "description": "Get this Cool Stac a Phris Arch background in 5120x3413 5K resolution…",
  "tags": ["Scotland", "Long exposure", "Sunset"],
  "keywords": ["Stac a Phris Arch", "Scotland", "5K"],
  "originalResolution": "5120x3413",
  "thumbnails": {
    "small": "https://4kwallpapers.com/images/walls/thumbs/26748.jpg",
    "medium": "https://4kwallpapers.com/images/walls/thumbs_2t/26748.jpg",
    "large": "https://4kwallpapers.com/images/walls/thumbs_3t/26748.jpg"
  },
  "resolutions": [
    {
      "width": 3840,
      "height": 2160,
      "label": "4K UHD",
      "group": "primary",
      "url": "https://4kwallpapers.com/images/wallpapers/stac-a-phris-arch-3840x2160-26748.jpg",
      "filename": "stac-a-phris-arch-3840x2160-26748.jpg"
    }
  ],
  "related": [],
  "sourceUrl": "https://4kwallpapers.com/nature/stac-a-phris-arch-26748.html"
}
```

#### Resolution groups

| `group` | Devices |
|---------|---------|
| `primary` | 4K + Original |
| `desktop` | 1080p → 5K, ultrawide |
| `mobile` | Phone portrait sizes |
| `tablet` | Square tablet sizes |
| `iphone` | iPhone-specific |
| `ipad` | iPad-specific |
| `other` | Unclassified fallback |

---

### `GET /v1/download/:category/:slugId`

Resolve a wallpaper + resolution and either **redirect** or **proxy** the image.

| Query | Default | Description |
|-------|---------|-------------|
| `resolution` or `res` | best 4K / first | e.g. `3840x2160`, `1080x1920` |
| `mode` | `redirect` | `redirect` = `302` to origin · `proxy` = stream via worker |

**Examples**

```http
GET /v1/download/nature/stac-a-phris-arch-26748?resolution=3840x2160
GET /v1/download/nature/stac-a-phris-arch-26748?resolution=1080x1920&mode=proxy
```

**`mode=redirect`** → `302` + `Location: https://4kwallpapers.com/images/wallpapers/...`

**`mode=proxy`** → `200` image body with long `Cache-Control` (no storage; streamed).

---

### `GET /v1/download`

Query-style download helper.

| Query | Description |
|-------|-------------|
| `path` | Origin path, e.g. `/nature/stac-a-phris-arch-26748.html` |
| `resolution` / `res` | Target size |
| `url` | Direct origin image URL (must be `/images/*` on origin host) |
| `mode` | `redirect` \| `proxy` |

**Examples**

```http
GET /v1/download?path=/nature/stac-a-phris-arch-26748.html&resolution=3840x2160
GET /v1/download?url=https://4kwallpapers.com/images/wallpapers/….jpg&mode=proxy
```

---

### `GET /v1/image`

Thumbnail / image proxy. Only allows `https://4kwallpapers.com/images/*`.

```http
GET /v1/image?url=https://4kwallpapers.com/images/walls/thumbs/26748.jpg
```

---

## Schemas

### Wallpaper list item

```json
{
  "id": 26748,
  "title": "Stac a Phris Arch",
  "category": "nature",
  "slug": "stac-a-phris-arch",
  "path": "/nature/stac-a-phris-arch-26748.html",
  "tags": ["Scotland", "Long exposure", "Sunset"],
  "thumbnails": {
    "small": "https://4kwallpapers.com/images/walls/thumbs/26748.jpg",
    "medium": "https://4kwallpapers.com/images/walls/thumbs_2t/26748.jpg"
  },
  "sourceUrl": "https://4kwallpapers.com/nature/stac-a-phris-arch-26748.html"
}
```

Use `category` + `slug` + `id` (or `path`) for detail and download calls.

### Paginated list

```json
{
  "items": [ /* Wallpaper list item */ ],
  "page": 1,
  "pageSize": 24,
  "totalPages": 950,
  "hasMore": true,
  "feed": "recent",
  "query": null,
  "category": null
}
```

`totalPages` may be `null` when upstream pagination is incomplete. Prefer `hasMore` for infinite scroll.

### Error

```json
{
  "error": "Human-readable message",
  "available": ["1920x1080", "3840x2160"]
}
```

| Status | When |
|--------|------|
| `400` | Bad params |
| `401` | API key required/invalid |
| `404` | Not found |
| `405` | Method not allowed (only `GET` / `OPTIONS`) |
| `500` / `502` | Worker or origin failure |

---

## Client integration

### Flutter / mobile sketch

```dart
final base = 'https://4kwallpaper-api.joeannsteinbock4351734.workers.dev/v1';

// Feed
final feed = await http.get(Uri.parse('$base/wallpapers?feed=recent&page=1'));
final items = jsonDecode(feed.body)['items'] as List;

// Detail
final w = items.first;
final detail = await http.get(Uri.parse(
  '$base/wallpapers/${w['category']}/${w['slug']}-${w['id']}',
));

// Best phone wallpaper URL
final resolutions = jsonDecode(detail.body)['resolutions'] as List;
final phone = resolutions.firstWhere(
  (r) => r['width'] == 1080 && r['height'] == 1920,
  orElse: () => resolutions.first,
);

// Prefer redirect (lets OS/WebView hit origin CDN)
final downloadUrl =
  '$base/download/${w['category']}/${w['slug']}-${w['id']}'
  '?resolution=${phone['width']}x${phone['height']}';
```

### TypeScript sketch

```ts
const API = "https://4kwallpaper-api.joeannsteinbock4351734.workers.dev/v1";

const feed = await fetch(`${API}/wallpapers?feed=recent&page=1`).then((r) =>
  r.json(),
);

const w = feed.items[0];
const detail = await fetch(
  `${API}/wallpapers/${w.category}/${w.slug}-${w.id}`,
).then((r) => r.json());

const res4k =
  detail.resolutions.find(
    (r: { width: number; height: number }) =>
      r.width === 3840 && r.height === 2160,
  ) ?? detail.resolutions[0];

// Option A — open origin URL from detail (fastest)
window.open(res4k.url);

// Option B — go through API redirect
window.open(
  `${API}/download/${w.category}/${w.slug}-${w.id}?resolution=${res4k.width}x${res4k.height}`,
);
```

### Recommended resolution picks

| Device class | Prefer |
|--------------|--------|
| Desktop 4K | `3840x2160` (`group: primary` or `desktop`) |
| Desktop FHD | `1920x1080` |
| Ultrawide | `3440x1440` / `2560x1080` |
| Android FHD | `1080x1920` |
| Android QHD | `1440x2560` |
| iPhone 14/15/16 | `1179x2556` |
| iPhone Pro Max | `1290x2796` |
| Original | match `originalResolution` or `group: primary` “Original” |

---

## Design notes

1. **No storage** — Worker does not keep images in R2/Disk. Images are redirected or streamed.
2. **Upstream has no public JSON API** — listings/detail are parsed from stable HTML; suggest uses `/search-api`.
3. **Prefer `mode=redirect`** for full-size downloads so large files stay on the origin CDN.
4. **List items always include `path` / `slug` / `id` / `category`** — detail URLs cannot be built from numeric id alone.
5. **Upstream markup may change** — empty `items` or missing `resolutions` usually means a parser break.

---

## Rate limiting & fair use

This proxy is intended for a personal/client app. Respect [Terms of Service](https://4kwallpapers.com/terms-of-service/) and [Copyright](https://4kwallpapers.com/copyright/) of 4kwallpapers.com. Do not hammer the origin; lean on the provided cache TTLs. Consider setting `API_KEY` before exposing the worker publicly.

---

## Deploy

```bash
pnpm install
pnpm deploy   # wrangler deploy
```

| Binding / var | Purpose |
|---------------|---------|
| `CACHE` (KV) | Parsed JSON cache |
| `ORIGIN` | Upstream base URL |
| `API_KEY` | Optional gate |
| `CACHE_TTL_*` | Tunable TTLs |

Repo layout:

```
docs/API.md      ← this document (source of truth)
src/index.ts     ← router
src/parse.ts     ← HTML → JSON
src/cache.ts     ← KV + edge cache
src/origin.ts    ← upstream fetch + allowlist
wrangler.toml
```

---

## Changelog

| Version | Notes |
|---------|--------|
| `1.0.0` | Initial public API: feeds, categories, search, detail, download, KV + edge cache, online docs |
