# British Powerlifting — Meetings API (v1)

> Read-only JSON feed of British Powerlifting's general meetings (AGM, EGM, FastVote). No authentication. Human docs: https://tools.britishpowerlifting.org/agm/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.

# Meetings API

Publicly visible information about general meetings — the same as the public meeting pages, nothing more. Text fields (`description`, `motion_text`, …) contain **Markdown source**; render them with a Markdown library. Individual ballots are never exposed — results are aggregate tallies only.

**Limits:** 60 requests per minute per IP (over the limit: HTTP `429`, JSON body, `Retry-After` header). CORS is open (`Access-Control-Allow-Origin: *`), so the API can be called from a browser on another site.

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

All publicly listed meetings, split into `current_meetings` (a stage open, or the date still ahead; soonest first) and `past_meetings` (most recent first).

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

```json
{
  "current_meetings": [
    {
      "slug": "agm-2026",
      "title": "Annual General Meeting 2026",
      "subtitle": "...",
      "meeting_type": "agm",
      "meeting_type_label": "AGM",
      "meeting_date": "2026-06-15T14:00:00",
      "description": "Markdown content...",
      "withdrawn": false,
      "active_stage": "voting",
      "stages": {
        "motion_submission_open": false,
        "comments_open": false,
        "amendments_open": false,
        "voting_open": true
      },
      "visibility": {
        "show_motions_publicly": true,
        "show_reports_publicly": true,
        "show_comments_publicly": true,
        "show_results_publicly": false
      },
      "meeting_details": {
        "format": "hybrid", "format_label": "In person & online",
        "location": "Venue name, town", "online_url": "https://zoom.us/j/..."
      },
      "pre_meeting": {
        "starts_at": "2026-06-08T19:00:00", "duration_minutes": 60,
        "format": "online", "format_label": "Online",
        "location": null, "online_url": "https://zoom.us/j/..."
      },
      "url": "https://tools.britishpowerlifting.org/agm/agm-2026/"
    }
  ],
  "past_meetings": [ /* same shape */ ]
}
```

- `meeting_details` (how to attend) and `pre_meeting` (the pre-AGM discussion session) are present only when set, and never for a withdrawn meeting.
- `withdrawn` is `true` for a FastVote the Board has withdrawn: show it as withdrawn, not upcoming. Withdrawn meetings stay listed.
- `meeting_type` is `agm`, `egm` or `fastvote`; `active_stage` is the stage currently open, or `null`.

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

One meeting. Unknown or unlisted slug → `404` with `{"error": "Meeting not found", "code": "not_found"}` (branch on `code`).

Query parameter `include` — comma-separated optional sections: `motions`, `results`.

```bash
curl "https://tools.britishpowerlifting.org/api/v1/meetings/agm-2026?include=motions,results"
```

```json
{
  "slug": "agm-2026",
  "title": "Annual General Meeting 2026",
  "meeting_type": "agm",
  "meeting_date": "2026-06-15T14:00:00",
  "url": "https://tools.britishpowerlifting.org/agm/agm-2026/",
  "motions": [
    {
      "public_id": "abc123",
      "reference": "1.1",
      "title": "Motion title",
      "kind": "motion",
      "kind_label": "Motion",
      "voting_threshold": "ordinary",
      "voting_threshold_label": "Ordinary (simple majority)",
      "voting_structure": "for_against",
      "voting_structure_label": "For / against / abstain",
      "decision_method": "online_ballot",
      "decision_method_label": "Online ballot",
      "status": "accepted",
      "status_label": "Accepted",
      "sort_order": 1,
      "intro": "Markdown...",
      "rationale": "Markdown...",
      "motion_text": "Markdown...",
      "intended_outcome": "Markdown...",
      "board_statement": "Markdown or null",
      "created_at": "2026-05-01T10:00:00",
      "updated_at": "2026-05-10T15:30:00"
    }
  ],
  "results": {
    "quorum_required": 20,
    "quorum_met": true,
    "total_ballots": 142,
    "motions": [
      {
        "motion_public_id": "abc123",
        "motion_reference": "1.1",
        "motion_title": "Motion title",
        "votes_for": 95,
        "votes_against": 30,
        "votes_abstain": 17,
        "total_cast": 142,
        "percent_for": 76.0,
        "threshold_percent": 50.0,
        "passed": true,
        "quorum_blocked": false,
        "is_withdrawn": false,
        "decision_method": "online_ballot"
      }
    ]
  }
}
```

Notes:

- **Visibility flags control what's returned.** If you ask for `include=motions` (or `results`) before they are public, the key is `null` and `motions_note` (or `results_note`) explains why.
- **Director's reports** (`kind` = `director_report`) carry `report_content` instead of `intro` / `rationale` / `motion_text` / `intended_outcome`, and appear only once the meeting makes reports public.
- **`decision_method`** is `online_ballot`, `show_of_hands` or `no_vote`. Most director's reports are simply received (`no_vote`) and have no tally; show-of-hands outcomes are recorded by the chair, so their vote counts may be zero — use `passed` and `result_override`.
- **`result_override`** / **`result_override_reason`** appear when the chair has declared an outcome (e.g. `deferred`); **`casting_vote`** / **`casting_vote_reason`** when a tie was broken by the chair.
- **Multi-choice / multi-select motions** carry an `options` array with option labels, and vote counts in results.
- **`url`** links to the meeting on the portal — use it for "View details" or "Vote now" buttons. Voting itself happens only on the portal; there is no voting API.