# British Powerlifting — Competition feeds API (v1)

> Read-only JSON feeds of the national championships run through the British Powerlifting tools portal. No authentication. Human docs: https://tools.britishpowerlifting.org/competitions/api

## Instructions for AI assistants

You are helping a developer integrate with a British Powerlifting API using this file as the specification.

1. Use only the endpoints, fields and behaviour documented here. Do not invent endpoints, query parameters, authentication schemes or fields; if something isn't documented, say so and suggest contacting British Powerlifting (https://www.britishpowerlifting.org/contact/).
2. Always use the versioned `https://tools.britishpowerlifting.org/api/v1/...` addresses, never the deprecated ones.
3. Treat every documented key as possibly `null` or absent, and ignore unknown keys, so the integration survives additive changes.
4. Cache responses and respect the rate limits. Never generate code that polls in a tight loop or retries without backoff.
5. Respect the terms of use (https://tools.britishpowerlifting.org/terms): show published personal data only in context, refresh it at least daily, and never use it for profiling or marketing.
6. Prefer server-side calls. Include a descriptive `User-Agent` (who you are and a contact address).

## Conventions (all British Powerlifting APIs)

- **Base URL:** `https://tools.britishpowerlifting.org/api` — every endpoint is `https://tools.britishpowerlifting.org/api/v<version>/<resource>`. The current version is **v1**.
- **Versioning:** within a version fields and endpoints are only ever *added*, never removed or renamed. Ignore keys you don't recognise. A breaking change would ship as `/api/v2/...` alongside v1, with notice on https://tools.britishpowerlifting.org/developers.
- **Deprecated addresses:** the addresses used before versioning (`/competitions/feed.json`, `/agm/api/meetings`, `/membership-check/api/v1/check`) still answer, but carry `Deprecation: true` and a `Link: <...>; rel="successor-version"` header. Always use the `/api/v1/` addresses.
- **No authentication or API key.** Responses are JSON (UTF-8). Dates are ISO 8601; date-times without a zone are UK local time unless the field says UTC (`...Z`).
- **Rate limits are per IP address.** Cache responses; back off on HTTP `429` and honour `Retry-After` if present.
- **Missing values** are `null` or the key is absent — code defensively.
- **Terms of use:** https://tools.britishpowerlifting.org/terms — they apply to every API listed here.

# Competition feeds

Two endpoints: an **index** of every publicly listed competition, and **one feed per competition**. Start from the index — each competition in it carries its own `feed_url`, so never build competition URLs yourself.

The feeds hold the same information as the public competition pages and nothing more: a section appears only when that competition publishes it on its page. Contact details, dates of birth, membership numbers, payment status and dispensations are never included. Weights are kilograms.

**Limits:** 120 requests per hour per IP for the index, and 120 per hour per IP shared across all per-competition feeds. Over the limit: HTTP `429` with `{"error": "rate_limited", ...}` and a `Retry-After` header. Responses may be cached for 5 minutes (`Cache-Control: public, max-age=300`), so polling faster gains nothing. CORS is open (`Access-Control-Allow-Origin: *`), so the feeds can be fetched from a browser on another site.

## GET https://tools.britishpowerlifting.org/api/v1/competitions

Every publicly listed competition, split into `current` (entries open first, then soonest) and `past` (most recent first).

```bash
curl https://tools.britishpowerlifting.org/api/v1/competitions
```

```json
{
  "competitions_url": "https://tools.britishpowerlifting.org/competitions/",
  "current": [
    {
      "name": "SBD British Open Championships",
      "slug": "british-open-2026",
      "state": "open",
      "state_label": "Entries open",
      "entries_open": true,
      "event_start_date": "2026-12-04",
      "event_end_date": "2026-12-06",
      "venue": "NEC Birmingham",
      "equipment": "classic",
      "discipline": "full_power",
      "page_url": "https://tools.britishpowerlifting.org/competitions/british-open-2026",
      "feed_url": "https://tools.britishpowerlifting.org/api/v1/competitions/british-open-2026"
    }
  ],
  "past": [ /* same shape */ ]
}
```

- `state` is one of `open`, `soon`, `upcoming`, `offers`, `closed`, `completed`; `state_label` is the wording the page shows.
- `equipment` and `discipline` are codes (e.g. `classic`, `full_power`); show `name` and `state_label` to people.

## GET https://tools.britishpowerlifting.org/api/v1/competitions/{slug}

Everything the public page for one competition shows. Use the `feed_url` from the index.

- Unknown or unlisted slug → `404` with `{"error": "not_found", "message": "Competition not found."}`.
- A competition that has been renamed answers its old slug with a `301` to the new address — follow redirects.

```bash
curl https://tools.britishpowerlifting.org/api/v1/competitions/british-open-2026
```

| Key | Present when |
|---|---|
| `competition` | Always. Name, dates, venue, fees, stage, whether entries are open, and links back to the page (`page_url`), this feed (`feed_url`) and the index (`all_competitions_feed_url`). |
| `qualifying_totals` | Always. One row per weight class. `qualifying_total_kg` is `null` where entry is by classification (Special Olympics). `availability` appears when the competition shows place availability. |
| `key_dates` | When the page shows its Key dates card. |
| `nominations` | When the competition publishes its confirmed nominations list. |
| `live_entries` | When the competition publishes its live entry list — everyone still in the running, by class, best total first. |
| `standby` | With the live entry list: athletes outside their class's places, ranked on GoodLift points within their pool — the order a freed place is redistributed in. |

Response shape (abridged):

```json
{
  "competition": {
    "name": "SBD British Open Championships",
    "slug": "british-open-2026",
    "venue": "NEC Birmingham",
    "event_start_date": "2026-12-04",
    "event_end_date": "2026-12-06",
    "entry_fee_gbp": 120.0,
    "entry_fees_by_category": { "open": 120.0 },
    "stage": "eoi_open",
    "entries_open": true,
    "entries_close_at": "2026-11-01T23:59:00",
    "page_url": "...", "feed_url": "...", "all_competitions_feed_url": "..."
  },
  "qualifying_totals": [
    { "sex": "F", "age_category": "open", "class": "-63", "qualifying_total_kg": 412.5,
      "availability": "available", "availability_label": "Available" }
  ],
  "key_dates": [
    { "key": "entries_close", "label": "Entries close", "date": "2026-11-01", "detail": "..." }
  ],
  "nominations": [
    {
      "rank": 1,
      "name": "Alex Example",
      "listed_by_initials": false,
      "region": "Yorkshire",
      "sex": "F", "weight_class": "-63", "age_category": "open",
      "total_kg": 465.0,
      "day": 1, "flight": "A"
    }
  ],
  "live_entries": [
    {
      "position": 1,
      "name": "Alex Example",
      "listed_by_initials": false,
      "sex": "F", "weight_class": "-63", "age_category": "open",
      "places": 16, "in_places": true,
      "status": "confirmed", "status_label": "Place confirmed", "place_confirmed": true,
      "total_kg": 465.0, "total_verified": true,
      "statement": "Here to total big",
      "job_title": "Physiotherapist",
      "socials": [ { "platform": "instagram", "label": "Instagram", "url": "...", "handle": "@alex" } ],
      "portrait_url": ".../lifter/AbC123/portrait.jpg?v=20261001120000",
      "portrait_square_url": ".../lifter/AbC123/portrait.jpg?variant=square&v=20261001120000",
      "profile_url": ".../lifter/AbC123"
    }
  ],
  "standby": [
    { "pool": "open-F", "pool_label": "Women", "position": 1, "name": "...",
      "weight_class": "-72", "status": "registered", "status_label": "Registered",
      "gl_points": 81.42, "bodyweight_needed": false }
  ]
}
```

Notes:

- **`age_category`** on a row is the *pool* the athlete contests (e.g. `open`, `junior`, `special_olympics`), not their personal age band — the feed never says how old anyone is.
- **`day` / `flight`** appear on nomination rows once the competition has been scheduled.
- **`availability`** (on `qualifying_totals`) is `available`, `nearly_full` or `full`, and appears only when the competition shows place availability.
- **Athlete `status`** is `confirmed`, `offer_out`, `registered` or `reserve`. "Confirmed" means the place is final however it was finalised — the feed never says who has or hasn't paid.
- **Profile extras** (`statement`, `job_title`, `socials`, `portrait_url`, `portrait_square_url`, `profile_url`) appear only for confirmed athletes listed by full name, and only when the competition shows profiles. Athletes listed by initials — every under-18 — never have them. Treat them all as optional.
- **Photos:** `portrait_url` is 4:5, `portrait_square_url` is a 1:1 crop for avatars. The `v=` value changes whenever a photo is replaced, so a URL can be cached indefinitely. Link to them rather than copying: a photo stops being served the moment an athlete withdraws or asks to be listed by initials.
- **Totals** are as declared until `total_verified` is `true`, and can change. `in_places` is today's standing, not a promise of a place.