# Ads API

Serve SME advertisements by **placement** and **location**. Consumer sites (ServDiary, JCT CRM, Got 2 Eat, and others) call the same public endpoint; ads are not exclusive to a publisher.

Base URL path: `/api`

Send `Accept: application/json` on all requests.

## Placements

| Value | Typical use |
|-------|-------------|
| `full_page_bg` | Full-page background |
| `web_banner` | Desktop / web feed banner |
| `mobile_banner` | Mobile feed banner |

Media types: `static` (image) or `video`.

## Authentication

| Action | Auth |
|--------|------|
| Serve ads | None (public), throttled 120/min per IP |
| Record impression or click | None (public), throttled 60/min per IP |

Campaigns and creatives are managed in the Order Market admin UI, not via this API.

## Serve ads

```
GET /api/ads/serve?placement=web_banner
GET /api/ads/serve?placement=web_banner&town=waterton
GET /api/ads/serve?placement=mobile_banner&postcode=sw1a-1aa
GET /api/ads/serve?placement=full_page_bg&lat=51.5014&lng=-0.1419&limit=1
```

| Parameter | Required | Notes |
|-----------|----------|--------|
| `placement` | yes | `full_page_bg`, `web_banner`, or `mobile_banner` |
| `town` | no | Town slug, or unique town name |
| `postcode` | no | Postcode slug (for example `sw1a-1aa`) or code |
| `lat` + `lng` | no | WGS84; resolved via nearest live postcode (max 20 km) |
| `limit` | no | Number of creatives to return (default 1, max 10) |

Location precedence: `postcode`, then `town`, then `lat`/`lng`.

### Targeting

A campaign with **no area targets** is nationwide and can show anywhere (including requests with no location).

A campaign targeted to a country, region, county, or town matches a request whose resolved town sits in that area (town matches its county, region, and country).

- Unknown `town` or `postcode` returns an empty `data` array.
- Coordinates with no nearby postcode fall back to nationwide campaigns only.
- Requests with no location return nationwide campaigns only.

Only **active** campaigns within their optional start/end window, from **active** advertisers, with a matching placement, are eligible. Selection among matches is weighted by campaign `weight`.

### Example

```bash
curl -H "Accept: application/json" \
  "https://example.com/api/ads/serve?placement=web_banner&town=waterton"
```

Response (`data` is a collection, not paginated):

```json
{
  "data": [
    {
      "id": 12,
      "placement": "web_banner",
      "media_type": "static",
      "media_url": "https://example.com/storage/ads/creatives/1/banner.jpg",
      "click_url": "https://sme.example.com/offer",
      "alt_text": "Spring offer",
      "headline": "Order from your local bakery",
      "width": 728,
      "height": 90,
      "advertiser": {
        "id": 3,
        "name": "Waterton Bakery",
        "slug": "waterton-bakery"
      }
    }
  ]
}
```

Render `media_url` as an image (`static`) or video (`video`), and send users to `click_url`.

## Record events

Call after showing or clicking a creative. `source` is optional analytics (which app called), not used for eligibility.

```
POST /api/ads/creatives/{id}/events
```

```bash
curl -X POST -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"type":"impression","source":"servdiary"}' \
  "https://example.com/api/ads/creatives/12/events"
```

| Field | Values |
|-------|--------|
| `type` | `impression` or `click` |
| `source` | Optional string, max 64 characters (for example `servdiary`, `jctcrm`, `got2eat`) |

Returns `201` with the recorded event under `data`.
