# Pulling Ads from the Little X Little API

This guide explains how your NGO website can fetch the ads (hero
banners, donation stories, slider items, etc.) you have published in
your Little X Little admin and render them on your own site.

---

## 1. Endpoint

```
GET https://api.littlexlittle.org/ads
GET https://api.littlexlittle.org/ads/{display}
```

| Item        | Value                                                           |
|-------------|-----------------------------------------------------------------|
| Method      | `GET`                                                           |
| Auth        | `Authorization: Bearer <YOUR_API_KEY>`                          |
| Permission  | `read_ads` (granted to every NGO key by default)                |
| Cache       | The response is `Cache-Control: public, max-age=60` — safe to   |
|             | cache for up to a minute on your edge / browser.                |
| Rate limit  | Per the limit on your API key (returned in `X-RateLimit-*`).    |

The `{display}` segment is **optional**. If supplied, only ads whose
`display` field matches that slot are returned (case-insensitive).
Common slot names from the admin UI:

- `home-slider-ads` → "Home Slider Ads"
- `donation-stories` → "Donation Stories"
- `home` → "Home"
- `anywhere` → no filter (same as omitting the segment)

The API converts dashes to spaces and title-cases the slug, so
`home-slider-ads` becomes `Home Slider Ads` for the lookup.

---

## 2. Query parameters

| Param   | Type   | Default | Range  | Meaning                              |
|---------|--------|---------|--------|--------------------------------------|
| `limit` | int    | `1`     | 1–12   | How many ads to return.              |
| `random`| 0 or 1 | `1`     | —      | `1` = pick at a random offset, `0` = newest first (`ORDER BY id DESC`). |

> **Tip — back-compat shape.** When `limit=1` (the default) the response
> `data` is a **single object**, not an array. When `limit > 1`, `data`
> is an **array of objects**. Code defensively (see section 5).

---

## 3. Response shape

### 3.1 Success (one ad, default)

```http
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59

{
  "status": "success",
  "code": 200,
  "data": {
    "code":        "ADS00000003",
    "title":       "Empowering Underprivileged Children in Uganda",
    "label":       "Transforming Lives, One Child at a Time",
    "url":         "https://your-ngo.org/donate",
    "link":        "Learn more",
    "target":      "_self",
    "size":        null,
    "display":     "Home Slider Ads",
    "tags":        null,
    "description": "<p>Full HTML body of the ad…</p>",
    "image":       "https://cdn.littlexlittle.org/.../hero.jpg",
    "images": [
      "https://cdn.littlexlittle.org/.../slide1.jpg",
      "https://cdn.littlexlittle.org/.../slide2.jpg"
    ]
  },
  "count": 1,
  "request_id": "8a3f…"
}
```

### 3.2 Success (multiple ads)

```json
{
  "status": "success",
  "code": 200,
  "data": [ { …ad 1… }, { …ad 2… }, { …ad 3… } ],
  "count": 3
}
```

### 3.3 No ads published yet

```json
{ "status": "info", "code": 404, "message": "No ads found" }
```

This is **not an error** — it just means you haven't published any ads
in that slot for that NGO. Your site should fall back to a static
hero in that case (see section 6).

### 3.4 Field reference

| Field         | Type       | Notes                                                          |
|---------------|------------|----------------------------------------------------------------|
| `code`        | string     | Ad identifier, e.g. `ADS00000003`. Use as a stable React key.  |
| `title`       | string     | Headline.                                                      |
| `label`       | string     | Sub-headline / kicker.                                         |
| `url`         | string     | Where the call-to-action button points.                        |
| `link`        | string     | Button text (e.g. "Learn more", "Donate now").                 |
| `target`      | string     | `_self` or `_blank`.                                           |
| `size`        | string\|null | Optional layout hint set in admin.                           |
| `display`     | string     | Slot name (matches the `{display}` filter).                    |
| `tags`        | string\|null | Comma-separated tags, may be null.                           |
| `description` | string     | Rich-HTML body. **Already sanitised on input, but always treat as HTML.** |
| `image`       | string     | Primary image URL (may be empty string `""`).                  |
| `images`      | string[]   | Up to 10 additional gallery image URLs (may be `[]`).          |

---

## 4. Examples

### 4.1 cURL — single random Home Slider ad

```bash
curl -H "Authorization: Bearer LXLPUBK-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-G" \
     "https://api.littlexlittle.org/ads/home-slider-ads"
```

### 4.2 cURL — three newest Donation Stories

```bash
curl -H "Authorization: Bearer LXLPUBK-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-G" \
     "https://api.littlexlittle.org/ads/donation-stories?limit=3&random=0"
```

### 4.3 JavaScript (browser / Node 18+)

```js
async function fetchAds({ slot = '', limit = 1, random = 1 } = {}) {
  const path = slot ? `/ads/${encodeURIComponent(slot)}` : '/ads';
  const url  = `https://api.littlexlittle.org${path}?limit=${limit}&random=${random}`;

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.LXL_API_KEY}` }
  });
  const json = await res.json();

  if (json.status !== 'success') return [];           // 404 = no ads, return []
  return Array.isArray(json.data) ? json.data : [json.data];
}

// Usage
const ads = await fetchAds({ slot: 'home-slider-ads', limit: 5, random: 0 });
ads.forEach(ad => console.log(ad.title, ad.url));
```

### 4.4 PHP (server-side)

```php
function lxl_fetch_ads(string $slot = '', int $limit = 1, int $random = 1): array {
    $path = $slot ? "/ads/" . rawurlencode($slot) : "/ads";
    $url  = "https://api.littlexlittle.org{$path}?limit={$limit}&random={$random}";

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('LXL_API_KEY')],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
    ]);
    $body = curl_exec($ch);
    curl_close($ch);

    $json = json_decode($body, true) ?: [];
    if (($json['status'] ?? '') !== 'success') return [];
    return is_array($json['data'] ?? null) && isset($json['data'][0])
         ? $json['data']
         : [$json['data']];
}
```

### 4.5 Python

```python
import os, requests

def fetch_ads(slot="", limit=1, random=1):
    path = f"/ads/{slot}" if slot else "/ads"
    r = requests.get(
        f"https://api.littlexlittle.org{path}",
        params={"limit": limit, "random": random},
        headers={"Authorization": f"Bearer {os.environ['LXL_API_KEY']}"},
        timeout=5,
    )
    j = r.json()
    if j.get("status") != "success":
        return []
    data = j["data"]
    return data if isinstance(data, list) else [data]
```

---

## 5. Defensive handling tips

1. **Always normalise `data` to a list** before iterating —
   `limit=1` returns a single object, `limit>1` returns an array.
2. **Treat `404 / "No ads found"` as an empty result, not an error.**
   It just means you haven't published any ad yet for that slot.
3. **Honour the `Cache-Control` header.** Don't hammer the endpoint on
   every page view — cache for 30–60 seconds at minimum.
4. **Render `description` as HTML.** It is sanitised on the way in but
   contains rich formatting (paragraphs, links, lists).
5. **Image URLs may be empty.** Always check `ad.image` before setting
   it as a `<img src>` and fall back to a placeholder if blank.
6. **Respect rate-limit headers** (`X-RateLimit-Remaining`, `Retry-After`
   on `429`).

---

## 6. Recommended fallback pattern

```js
const ads = await fetchAds({ slot: 'home-slider-ads', limit: 5 });

if (ads.length === 0) {
  renderStaticHero();           // your local hard-coded hero
} else {
  renderSlider(ads);
}
```

---

## 7. Errors you may encounter

| HTTP | `status`  | When                                    | What to do                            |
|------|-----------|-----------------------------------------|---------------------------------------|
| 200  | `success` | At least one published ad matched.      | Render it.                            |
| 401  | `error`   | Missing or wrong `Authorization` header.| Check your API key.                   |
| 403  | `error`   | Key lacks `read_ads`, or origin denied. | Contact support to update the key.    |
| 404  | `info`    | No ads published in that slot.          | Show your static fallback.            |
| 429  | `error`   | Too many requests this minute.          | Back off until `Retry-After` seconds. |
| 5xx  | `error`   | Platform issue.                         | Retry with exponential backoff.       |

---

## 8. Publishing ads

To make ads appear in the API response:

1. Sign in to your NGO admin at `https://admin.littlexlittle.org/`
   (or your custom admin URL).
2. Open **Marketing → Ads → New Ad**.
3. Fill in the title, label, URL, button text and upload a hero image.
4. Set **Display slot** (e.g. *Home Slider Ads*) — this is the
   `{display}` value clients will filter by.
5. Set **Status = Saved** and **Publish = Yes**.
6. The new ad becomes available through the API immediately
   (subject to your client's cache).

Only ads with `status = saved` AND `publish = 1` are returned.

---

## 9. Need help?

- API docs index: <https://developer.littlexlittle.org/>
- Support: support@littlexlittle.org

Please include your **NGO app code**, the **request URL**, and the
**`request_id`** from any unexpected response.
