MPC Observing Target List ('WhatsUp'): a Python Convenience Wrapper¶
This tutorial shows how to drive the MPC's Observing Target List web form from Python, and read the results back into a pandas table.¶
The Observing Target List (reachable in the site menu under Observers, Other Observer Services) generates a list of the brightest asteroids, near-Earth asteroids, or comets visible from a given location during a given time window.
It is a web form, not a REST API: there is no JSON endpoint, and requests are protected by a session token. This tutorial wraps the form so it can be used like an API. To do so, it:
- Fetches the form page to obtain an authentication token.
- Submits the same POST request a browser would.
- Parses the returned HTML table with
pandas.
Some important framing before we start:
- This is an unofficial convenience — if the page layout changes, the wrapper may need updating.
- Please be considerate: this drives the same production service as the web page, so keep query volumes modest.
- If you need this kind of service at scale, please request it from the MPC via the Jira Helpdesk — knowing that there's demand for such a utility would motivate a real JSON API.
Import Packages¶
Here we import the standard Python packages we use in this tutorial. In addition to requests, this tutorial needs pandas and lxml (pip install requests pandas lxml).
import re
from io import StringIO
import requests
import pandas as pd
How the Form Works¶
The wrapper reproduces the three steps a browser performs:
- GET the form page at https://data.minorplanetcenter.net/whatsup/index.html. This sets a session cookie, and the page contains a hidden
authenticity_token(the site's cross-site-request-forgery protection). - POST the form fields, plus that token, to https://data.minorplanetcenter.net/whatsup/index.
- Parse the results table out of the returned HTML.
An aside on URLs: the URL https://minorplanetcenter.net/whatsup works in a browser but returns 403 to generic HTTP clients (the server only answers requests that declare Accept: text/html), which is why the wrapper starts from the static index.html page instead.
Query Parameters¶
| Parameter | Description |
|---|---|
latitude, longitude |
Observer location in decimal degrees; positive latitude for the Northern Hemisphere, positive longitude east of Greenwich |
year, month, day, hour, minute |
UTC start of the observing window |
duration_hours |
Length of the observing window in hours |
max_objects |
Maximum number of objects to return |
min_altitude |
Minimum altitude above the horizon, in degrees |
min_solar_elong, min_lunar_elong |
Minimum solar and lunar elongation, in degrees |
object_type |
"mp" (asteroids), "neo" (near-Earth asteroids), "cmt" (comets) |
The Wrapper Function¶
The function below performs the GET-token-POST-parse sequence described above and returns the results as a pandas DataFrame.
FORM_URL = "https://data.minorplanetcenter.net/whatsup/index.html"
SUBMIT_URL = "https://data.minorplanetcenter.net/whatsup/index"
def get_observing_targets(latitude, longitude,
year, month, day, hour, minute,
duration_hours=1, max_objects=10, min_altitude=30,
min_solar_elong=45, min_lunar_elong=20,
object_type="mp", session=None):
"""Query the MPC Observing Target List and return the results as a DataFrame.
object_type: "mp" (asteroids), "neo" (near-Earth asteroids), "cmt" (comets)
"""
s = session or requests.Session()
# Step 1: load the form to obtain the session cookie and CSRF token
r = s.get(FORM_URL, timeout=30)
r.raise_for_status()
token = re.search(r'name="authenticity_token"[^>]*value="([^"]+)"', r.text).group(1)
# Step 2: submit the form exactly as a browser would
payload = {
"utf8": "✓", "authenticity_token": token,
"latitude": latitude, "longitude": longitude,
"year": year, "month": month, "day": day,
"hour": hour, "minute": minute,
"duration": duration_hours, "max_objects": max_objects,
"min_alt": min_altitude, "solar_elong": min_solar_elong,
"lunar_elong": min_lunar_elong,
"object_type": object_type, "submit": "Submit",
}
r2 = s.post(SUBMIT_URL, data=payload, timeout=90)
r2.raise_for_status()
# Step 3: parse the results table -- it is the (only) 16-column table.
# (Its header row is not read by pandas, so we assign the names ourselves.)
columns = ["Designation", "Mag", "Solar Elong", "Lunar Elong",
"Begin Time", "Beg RA", "Beg Dec", "Beg Alt",
"Max Time", "Max RA", "Max Dec", "Max Alt",
"End Time", "End RA", "End Dec", "End Alt"]
for t in pd.read_html(StringIO(r2.text)):
if t.shape[1] == len(columns):
t.columns = columns
return t
raise ValueError("No results table found in the response "
"(no visible objects, or the page layout has changed)")
Example 1: Bright Asteroids from Cambridge, MA¶
Here we ask for the brightest asteroids visible from Cambridge, Massachusetts (latitude +42.4, longitude -71.1) for one hour starting at 22:00 UTC on Sept 4th, 2026
df = get_observing_targets(42.4, -71.1, 2026, 9, 4, 22, 0, object_type="mp")
df
| Designation | Mag | Solar Elong | Lunar Elong | Begin Time | Beg RA | Beg Dec | Beg Alt | Max Time | Max RA | Max Dec | Max Alt | End Time | End RA | End Dec | End Alt | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | (409) | 12.0 | 118 | 152 | 2026 9 4.934 (22:25 UT) | 18 44 31.2 | -09 41 10 | 30.3 | 2026 9 4.958 (23:00 UT) | 18 44 31.6 | -09 41 14 | 33.8 | 2026 9 4.958 (23:00 UT) | 18 44 31.6 | -09 41 14 | 33.8 |
| 1 | (51) | 12.2 | 104 | 163 | 2026 9 4.917 (22:00 UT) | 17 43 41.1 | -12 11 34 | 31.5 | 2026 9 4.958 (23:00 UT) | 17 43 42.9 | -12 11 49 | 34.9 | 2026 9 4.958 (23:00 UT) | 17 43 42.9 | -12 11 49 | 34.9 |
| 2 | (216) | 12.3 | 101 | 160 | 2026 9 4.917 (22:00 UT) | 17 35 52.7 | -09 07 59 | 35.0 | 2026 9 4.958 (23:00 UT) | 17 35 53.8 | -09 08 09 | 38.2 | 2026 9 4.958 (23:00 UT) | 17 35 53.8 | -09 08 09 | 38.2 |
| 3 | (397) | 12.4 | 138 | 127 | 2026 9 4.944 (22:40 UT) | 20 12 04.8 | +02 41 08 | 30.3 | 2026 9 4.958 (23:00 UT) | 20 12 04.6 | +02 41 02 | 33.6 | 2026 9 4.958 (23:00 UT) | 20 12 04.6 | +02 41 02 | 33.6 |
| 4 | (387) | 12.5 | 50 | 128 | 2026 9 4.917 (22:00 UT) | 14 13 47.2 | +01 49 44 | 41.6 | 2026 9 4.917 (22:00 UT) | 14 13 47.2 | +01 49 44 | 41.6 | 2026 9 4.958 (23:00 UT) | 14 13 51.2 | +01 49 12 | 33.2 |
| 5 | (57) | 12.6 | 114 | 151 | 2026 9 4.917 (22:00 UT) | 18 28 54.6 | -04 57 26 | 33.3 | 2026 9 4.958 (23:00 UT) | 18 28 54.9 | -04 57 39 | 39.5 | 2026 9 4.958 (23:00 UT) | 18 28 54.9 | -04 57 39 | 39.5 |
| 6 | (849) | 12.6 | 116 | 143 | 2026 9 4.917 (22:00 UT) | 18 40 17.2 | +02 13 19 | 37.9 | 2026 9 4.958 (23:00 UT) | 18 40 18.1 | +02 13 04 | 45.2 | 2026 9 4.958 (23:00 UT) | 18 40 18.1 | +02 13 04 | 45.2 |
| 7 | (92) | 12.7 | 74 | 155 | 2026 9 4.917 (22:00 UT) | 15 40 44.4 | -15 08 23 | 32.0 | 2026 9 4.917 (22:00 UT) | 15 40 44.4 | -15 08 23 | 32.0 | 2026 9 4.944 (22:40 UT) | 15 40 45.9 | -15 08 33 | 30.2 |
| 8 | (28) | 13.0 | 86 | 162 | 2026 9 4.917 (22:00 UT) | 16 28 35.1 | -13 29 13 | 34.0 | 2026 9 4.927 (22:15 UT) | 16 28 35.6 | -13 29 16 | 34.1 | 2026 9 4.958 (23:00 UT) | 16 28 37.1 | -13 29 26 | 33.2 |
| 9 | (345) | 13.0 | 129 | 142 | 2026 9 4.955 (22:55 UT) | 19 28 03.1 | -07 33 30 | 30.6 | 2026 9 4.958 (23:00 UT) | 19 28 03.1 | -07 33 31 | 31.2 | 2026 9 4.958 (23:00 UT) | 19 28 03.1 | -07 33 31 | 31.2 |
Example 2: Near-Earth Asteroids¶
The same site and time, but now requesting near-Earth asteroids (object_type="neo") and limiting the list to five objects. We display a subset of the columns.
df_neo = get_observing_targets(42.4, -71.1, 2026, 9, 4, 22, 0,
object_type="neo", max_objects=5)
df_neo[["Designation", "Mag", "Beg RA", "Beg Dec", "Beg Alt"]]
| Designation | Mag | Beg RA | Beg Dec | Beg Alt | |
|---|---|---|---|---|---|
| 0 | (1980) | 15.6 | 16 41 34.6 | +21 43 05 | 68.4 |
| 1 | 2012 LE11 | 16.1 | 20 29 31.4 | +09 41 33 | 30.7 |
| 2 | (5863) | 16.2 | 18 01 20.1 | +01 21 30 | 42.2 |
| 3 | (24445) | 16.6 | 19 54 06.6 | +22 17 25 | 39.9 |
| 4 | (1943) | 16.9 | 18 32 12.6 | -03 40 59 | 34.0 |
Example 3: Comets¶
Finally, comets (object_type="cmt"). Comet designations come back unpacked, e.g. C/2024 J3 (ATLAS).
df_cmt = get_observing_targets(42.4, -71.1, 2026, 9, 4, 22, 0,
object_type="cmt", max_objects=5)
df_cmt[["Designation", "Mag", "Beg RA", "Beg Dec", "Beg Alt"]]
| Designation | Mag | Beg RA | Beg Dec | Beg Alt | |
|---|---|---|---|---|---|
| 0 | C/2024 J3 (ATLAS) | 13.7 | 18 35 11.5 | +43 15 34 | 63.9 |
| 1 | C/2023 R1 (PANSTARRS) | 14.8 | 15 30 14.3 | -15 26 25 | 31.4 |
| 2 | C/2026 A2 (Bok) | 16.2 | 16 35 25.6 | +74 24 55 | 57.9 |
| 3 | C/2024 R4 (PANSTARRS) | 17.4 | 20 29 28.8 | +49 25 00 | 46.0 |
| 4 | C/2024 G6 (ATLAS) | 17.9 | 15 06 19.3 | +15 21 03 | 59.5 |
Reading the Results¶
Each row is one object visible under the requested constraints during the window:
- Mag is the predicted visual magnitude.
- Solar Elong and Lunar Elong are in degrees.
- The Begin / Max / End column triplets give the time (UT), RA and Dec (J2000), and altitude at the start of visibility, at maximum altitude, and at the end of visibility within the requested window.
- On the web page, the designations link through to the MPC database entry for each object.
Caveats¶
- This wrapper is unofficial: it automates the public web form, so results are identical to the website, but if the page layout or form fields change, the wrapper may break and need updating.
- All times are UTC.
- A
"No results table found"error means either no objects matched your constraints, or the page layout changed.
Summary¶
This tutorial demonstrated how to drive the MPC Observing Target List web form from Python:
- Form URL:
https://data.minorplanetcenter.net/whatsup/index.html(GET, to obtain the session cookie andauthenticity_token) - Submit URL:
https://data.minorplanetcenter.net/whatsup/index(POST, form fields plus the token) - Parsing: the returned HTML table is read with
pandas.read_htmlinto a DataFrame - Object types:
"mp"(asteroids),"neo"(near-Earth asteroids),"cmt"(comets)
Further Resources¶
For questions or feedback, contact the MPC via the Jira Helpdesk.