# UK Petition Threshold ETA & Velocity Tracker (`hllerdgn80/uk-petition-threshold-tracker`) Actor

Tracks open UK Parliament e-petitions and predicts, from signature velocity since the previous run, how many hours until each one crosses the 10,000-signature government-response line or the 100,000 debate line. Also flags the top signing constituencies and their MP.

- **URL**: https://apify.com/hllerdgn80/uk-petition-threshold-tracker.md
- **Developed by:** [Halil Erdogan](https://apify.com/hllerdgn80) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.01 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## UK Petition Threshold ETA & Velocity Tracker

Tracks UK Government / Parliament e-petitions (petition.parliament.uk) and
predicts, from signature velocity between runs, how many hours are left
until a petition crosses one of the two statutory lines that trigger a
government reaction. Built entirely on the official, public, keyless
Petitions site JSON API - no scraping, no browser, no login.

### Why this exists

Every open petition page on petition.parliament.uk already shows a live
signature count. What it does not show - and no other public tool tracks

- is **how fast that count is moving and when it will cross a threshold**:

- **10,000 signatures** - the government must respond in writing.

- **100,000 signatures** - the petition is considered for a House of
  Commons debate.

### What it does

1. **Lists petitions** from the official site (`petitions.json`, OGL
   open data), filtered by state (open/closed/all) and an optional search
   term, exactly like the site's own search box.
2. **Scores a shortlist in full detail** - petitions at or above your
   signature floor, plus any petition ID you explicitly watch - by
   fetching each one's own record (`petitions/<id>.json`), which carries
   its constituency-by-constituency, region-by-region and country-by-country
   signature breakdown and its threshold timestamps.
3. **Diffs against the previous run** - the Actor's own key-value store
   remembers each scored petition's signature count and the time it was
   last checked. On the next run it computes `signatures_per_hour` from
   the delta, and turns that rate into `eta_hours_to_next_threshold` and
   `eta_next_threshold_at` for whichever line (10,000 or 100,000) is still
   ahead. First run for a petition has no velocity yet (`velocity_basis:
   "no_previous_run_recorded"`) - run it again later (e.g. hourly) to get
   a real ETA. **This ETA engine is the feature**: the site itself, and
   every other public petitions tool, only ever answers "what is the count
   right now" - never "at this rate, when".
4. **Flags threshold crossings** - `crossed_response_threshold_since_previous_run`
   and `crossed_debate_threshold_since_previous_run` go `true` the run a
   petition's own official timestamp for that threshold first appears,
   compared with what was stored last time - a genuine changed-since-last-check
   signal, not just a snapshot.
5. **Names the top signing constituencies and their MP** - each scored
   petition includes its highest-signing constituencies from the
   official breakdown, each with the sitting MP's name as published by
   the Petitions site itself (useful for local-angle journalism or
   lobbying research - "who in my MP's seat signed this").
6. **Lets you run a lean alert feed** - `onlyApproachingOrJustCrossedThreshold`
   drops every petition that is not within your alert window of its next
   threshold and has not just crossed one, so a scheduled run only ever
   surfaces petitions that changed in a way worth reading.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `state` | enum | `open` | `open`, `closed`, or `all`. |
| `searchQuery` | string | - | Same free-text search as the site's search box (`q=`). |
| `watchPetitionIds` | array of numbers | `[]` | Petition IDs (the number in `petition.parliament.uk/petitions/<id>`) to always score. |
| `maxPetitionsToScan` | integer | 150 | How many petitions to pull from the listing (25/page) before filtering. |
| `minSignatureCount` | integer | 500 | Skip petitions below this count in the listing pass. |
| `maxPetitionsToScore` | integer | 40 | Cap on full-detail fetches (= charged events) per run. |
| `onlyApproachingOrJustCrossedThreshold` | boolean | `false` | Lean alert mode - see above. |
| `etaAlertWindowHours` | integer | 24 | ETA (hours) inside which a petition counts as "approaching". |
| `topConstituenciesCount` | integer | 5 | Top signing constituencies (with MP) to attach per petition. |

### Output (one dataset row per scored petition)

```json
{
  "petition_id": 762815,
  "url": "https://petition.parliament.uk/petitions/762815",
  "action": "Example petition action text",
  "state": "open",
  "signature_count": 82717,
  "next_threshold": {"name": "debate_consideration", "count": 100000},
  "signatures_per_hour": 200.0,
  "hours_since_previous_run": 6.0,
  "eta_hours_to_next_threshold": 86.4,
  "eta_next_threshold_at": "2026-10-01T06:59:43+00:00",
  "velocity_basis": "delta_since_previous_run",
  "approaching_threshold": true,
  "crossed_response_threshold_since_previous_run": false,
  "crossed_debate_threshold_since_previous_run": false,
  "top_constituencies": [
    {"name": "Banbury", "mp": "Sean Woodcock MP", "ons_code": "E14001072", "signature_count": 299}
  ],
  "checked_at": "2026-09-27T18:40:00+00:00"
}
```

`velocity_basis` is one of `no_previous_run_recorded` (first time this
petition is scored), `delta_since_previous_run` (a real rate was
computed), `previous_run_too_recent` (elapsed time was zero or negative -
clock skew guard), or `stalled_or_declining` (rate is zero or negative, so
no ETA is given - a petition can plateau or, rarely, have its count
corrected downward).

### Pricing model

Pay-per-event: one `petition-scored` event is charged per petition row
pushed to the dataset. Petitions in the listing pass that never make the
signature floor or the scoring cap cost nothing; a petition ID that 404s
(withdrawn/rejected, e.g. only reachable through `watchPetitionIds`) is
reported in run stats and not charged.

### Data source and licence

All data comes directly from `petition.parliament.uk`, the official UK
Government and Parliament e-petitions site, via its own public JSON API
(the `.json` suffix on every page - documented informally at
`petition.parliament.uk/help`). No API key, no login, no rate-limit
workaround. Data is Crown/Parliamentary copyright published under the
Open Government Licence v3.

### Known constraints

- **First run has no velocity.** `signatures_per_hour` and the ETA fields
  are only populated once a petition has been scored on a previous run
  (state carried in the Actor's key-value store). Schedule the Actor to
  run periodically (e.g. hourly or every few hours) for the ETA feature
  to do anything.
- **ETA is a straight-line projection** from the rate observed between
  the previous run and this one - it does not model campaigns, media
  spikes, or petitions closing. Treat it as "at the current rate", not a
  guarantee.
- **Older-TLS-stack retries**: this development machine's Python/TLS
  stack occasionally raises `http.client.IncompleteRead` against some UK
  government hosts; `src/http.py` catches it (and 429/5xx/connection
  resets) inside the same retry-with-backoff loop used by the other
  Actors in this project, so a transient blip does not fail the whole run.

# Actor input Schema

## `state` (type: `string`):

Which petitions to pull from the official UK Parliament Petitions site: 'open' (still collecting signatures), 'closed', or 'all'. 'open' is what almost every use of this Actor wants.

## `searchQuery` (type: `string`):

Optional: only fetch petitions whose title/action text matches this search (same 'q' search box as petition.parliament.uk). Leave blank to scan every petition in the chosen state.

## `watchPetitionIds` (type: `array`):

Optional: specific petition IDs (the number in a petition's URL, e.g. 762640 for petition.parliament.uk/petitions/762640) to always fetch and score, even if they would otherwise be filtered out or fall outside the first pages scanned.

## `maxPetitionsToScan` (type: `integer`):

How many petitions (newest/most-recently-updated first, 25 per page of the official listing) to pull from the summary listing before filtering by signature count. Raise this to cover more of a long tail; each one costs one extra lightweight listing request, not a charged event.

## `minSignatureCount` (type: `integer`):

Skip petitions with fewer signatures than this. Use it to cut noise from petitions that will never realistically approach 10,000.

## `maxPetitionsToScore` (type: `integer`):

After the listing and minimum-signature filter, this caps how many petitions get a full detail fetch (constituency/region breakdown + velocity + ETA). Each scored petition is one dataset row and one charged event, so this is the main cost control.

## `onlyApproachingOrJustCrossedThreshold` (type: `boolean`):

If enabled, drop petitions that are neither within the 'ETA alert window' of their next threshold (10,000 government-response signatures, or 100,000 debate signatures) nor crossed one since the previous run. Use this for a lean day-to-day alert feed; leave off to see every scored petition's full status.

## `etaAlertWindowHours` (type: `integer`):

A petition counts as 'approaching' its next threshold when its predicted ETA (based on signature velocity since the previous run) is less than this many hours away. Only used when 'Only approaching or just-crossed a threshold' is on, or to set the 'approaching\_threshold' flag.

## `topConstituenciesCount` (type: `integer`):

How many of the highest-signing parliamentary constituencies (with their sitting MP's name, from the petition's own official constituency breakdown) to include per scored petition.

## Actor input object example

```json
{
  "state": "open",
  "watchPetitionIds": [],
  "maxPetitionsToScan": 150,
  "minSignatureCount": 500,
  "maxPetitionsToScore": 40,
  "onlyApproachingOrJustCrossedThreshold": false,
  "etaAlertWindowHours": 24,
  "topConstituenciesCount": 5
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("hllerdgn80/uk-petition-threshold-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("hllerdgn80/uk-petition-threshold-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call hllerdgn80/uk-petition-threshold-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hllerdgn80/uk-petition-threshold-tracker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Rs7JU5SGQfIofeppi/builds/BcGf5wWrbhyaDNzMx/openapi.json
