Getting Started with the mpc_client Python Package¶
This tutorial demonstrates how to use the mpc_client package to interact with the Minor Planet Center's REST APIs.¶
The mpc_client package provides a single MPCClient class that wraps all of the MPC's public APIs into convenient Python methods. Instead of writing raw HTTP requests, you can call methods like mpc.identify("Ceres") or mpc.get_orbit("Bennu").
Features:
- Single
MPCClientclass covering all MPC APIs - Typed response models with attribute access (e.g.
orbit.COM.coefficient_values) - Input validation with clear error messages
- Optional pandas DataFrame output for analysis-friendly results
- Custom exception hierarchy for structured error handling
Installation:
pip install mpc-client # core (returns typed objects)
pip install mpc-client[dataframe] # adds pandas DataFrame methods
Individual API tutorials that show both raw requests and mpc_client approaches are available at:
Documentation regarding the individual APIs is available at:
Install Required Packages¶
Run the cell below to ensure the mpc_client package and its dependencies are installed in the current Python environment. If you already have them installed, this cell will complete quickly.
# Install required packages (run this cell if packages are not already installed)
import subprocess
import sys
# Install mpc-client with the dataframe extra (includes pandas)
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", "mpc-client[dataframe]"])
print("Installation complete.")
Installation complete.
[notice] A new release of pip is available: 25.2 -> 26.0.1 [notice] To update, run: pip3 install --upgrade pip
Import Packages¶
Here we import the mpc_client package along with standard libraries for display.
from mpc_client import MPCClient
import json
import pandas as pd
# Create a client instance (used for all API calls)
mpc = MPCClient()
print(mpc)
MPCClient()
Designation Identifier¶
The identify() method resolves object names, numbers, and provisional designations into canonical MPC identifiers.
It accepts a single string or a list of strings.
# Single object lookup
result = mpc.identify("Sedna")
print(f"{type(result["Sedna"])=}\n")
# We use the built-in Pydantic v2 BaseModel `model_dump` method to convert the DesignationInfo instance to a plain dict.
print(json.dumps({k: v.model_dump() for k, v in result.items()}, indent=4))
type(result["Sedna"])=<class 'mpc_client._identifier.DesignationInfo'>
{
"Sedna": {
"found": 1,
"permid": "90377",
"name": "Sedna",
"iau_designation": "(90377)",
"object_type": [
"Minor Planet",
0
],
"citation": "(90377) Sedna = 2003 VB12<br><br>Sedna is the Inuit goddess of the sea and the mother of all sea creatures. She rewards the people of the land with food from the sea. Without her blessing, hunts fail and the people starve. She is thus one of the most important figures in Inuit legend. ",
"disambiguation_list": null,
"dual_status_info": null,
"orbfit_name": "90377",
"packed_permid": "90377",
"packed_primary_provisional_designation": "K03V12B",
"packed_secondary_provisional_designations": [],
"unpacked_primary_provisional_designation": "2003 VB12",
"unpacked_secondary_provisional_designations": []
}
}
# Multi-object lookup in a single call
result = mpc.identify(["Ceres", "2023 BU", "Apophis"])
for name, info in result.items():
print(f"{name:12s} permid={str(info.permid):>8s} type={info.object_type[0]}")
2023 BU permid= None type=Minor Planet Apophis permid= 99942 type=Minor Planet Ceres permid= 1 type=Minor Planet
Orbits¶
The get_orbit() method returns an OrbitalElements object with the orbital elements in the mpc_orb format. It returns None when the object is not found.
# Retrieve orbital elements for Bennu
orbit = mpc.get_orbit("Bennu")
if orbit:
print(f"Object: {orbit.designation_data.permid}")
print(f"Designation: {orbit.designation_data.unpacked_primary_provisional_designation}")
print(f"H magnitude: {orbit.magnitude_data.H}")
# Print cometarian orbital elements
com = orbit.COM
print("\nCometarian orbital elements:")
for name, val, unc in zip(com.coefficient_names, com.coefficient_values, com.coefficient_uncertainties):
print(f" {name:>10s} = {val:>15.8f} +/- {unc}")
Object: 101955
Designation: 1999 RQ36
H magnitude: 20.684
Cometarian orbital elements:
q = 0.89659064 +/- 9.1402e-09
e = 0.20373108 +/- 8.17369e-09
i = 6.03290398 +/- 1.22908e-06
node = 1.97241281 +/- 2.55606e-06
argperi = 66.39437769 +/- 3.04048e-06
peri_time = 60675.73787173 +/- 1.90921e-06
# A non-existent object returns None (not an error)
result = mpc.get_orbit("NotARealAsteroid")
print(f"Result: {result}")
Result: None
Raw Orbit Response¶
The get_orbit_raw() method returns the full, unprocessed JSON from the API — useful when you need fields outside the parsed OrbitalElements model.
# get_orbit_raw() returns the full API response as a list of dicts
raw = mpc.get_orbit_raw("Bennu")
print(f"Type: {type(raw)}")
print(f"Top-level keys: {list(raw[0].keys()) if raw else 'empty'}")
# Access the nested mpc_orb data
if raw and "mpc_orb" in raw[0]:
orb = raw[0]["mpc_orb"][0]
print(f"\nmpc_orb keys: {list(orb.keys())}")
print(f"Designation: {orb['designation_data'].get('unpacked_primary_provisional_designation')}")
Type: <class 'list'> Top-level keys: ['ele220', 'eq0', 'eq0_dict', 'eq1', 'eq1_dict', 'mpc_orb', 'rwo', 'rwo_dict'] mpc_orb keys: ['CAR', 'COM', 'categorization', 'designation_data', 'epoch_data', 'magnitude_data', 'moid_data', 'non_grav_booleans', 'orbit_fit_statistics', 'software_data', 'system_data'] Designation: 1999 RQ36
Observations¶
The get_observations() method returns an ObservationsResult object with attributes for each requested format (e.g. .OBS80, .XML, .ADES_DF). For data analysis, the get_observations_df() convenience method returns a pandas DataFrame directly.
# Get observations in the classic 80-column format
result = mpc.get_observations("2023 BU", output_format="OBS80")
lines = result.OBS80.strip().split("\n")
print(f"Total observations: {len(lines)}")
print("\nFirst 3:")
for line in lines[:3]:
print(f" {line}")
Total observations: 1784
First 3:
K23B00U C2023 01 21.34299109 50 04.192+43 47 13.29 21.19RU~6CJqI41
K23B00U C2023 01 21.44005909 49 14.025+43 47 08.78 20.15RU~6CJqI41
K23B00U*KC2023 01 21.99514 09 48 47.36 +43 41 03.0 19.5 GX~6CJqL51
# Get observations as a pandas DataFrame (requires pandas)
df = mpc.get_observations_df("Bennu")
print(f"Shape: {df.shape[0]} observations x {df.shape[1]} columns")
print(f"\nColumns: {list(df.columns)}")
print(f"\nFirst 5 rows (selected columns):")
display_cols = [c for c in ["permid", "provid", "obstime", "ra", "dec", "mag", "stn"] if c in df.columns]
print(df[display_cols].head())
Shape: 603 observations x 76 columns Columns: ['Obstype', 'artsat', 'astcat', 'band', 'com', 'ctr', 'dec', 'decstar', 'delay', 'deltadec', 'deltara', 'deprecated', 'disc', 'dist', 'doppler', 'exp', 'fltr', 'frq', 'localuse', 'logsnr', 'mag', 'mode', 'notes', 'nstars', 'nucmag', 'obscenter', 'obsid', 'obssubid', 'obstime', 'pa', 'permid', 'photap', 'photcat', 'pos1', 'pos2', 'pos3', 'poscov11', 'poscov12', 'poscov13', 'poscov22', 'poscov23', 'poscov33', 'precdec', 'precra', 'prectime', 'prog', 'provid', 'ra', 'rastar', 'rcv', 'ref', 'remarks', 'rmscorr', 'rmsdec', 'rmsdelay', 'rmsdist', 'rmsdoppler', 'rmsfit', 'rmsmag', 'rmspa', 'rmsra', 'rmstime', 'seeing', 'shapeocc', 'stn', 'subfmt', 'subfrm', 'sys', 'trkid', 'trkmpc', 'trksub', 'trx', 'unctime', 'vel1', 'vel2', 'vel3'] First 5 rows (selected columns): permid provid obstime ra dec mag stn 0 101955 1999 RQ36 1999-09-11T09:44:59.136Z 24.47875 -27.07431 15.1 704 1 101955 1999 RQ36 1999-09-11T10:06:56.736Z 24.50075 -27.06656 15.1 704 2 101955 1999 RQ36 1999-09-11T10:29:31.488Z 24.52337 -27.05839 15.3 704 3 101955 1999 RQ36 1999-09-11T10:51:03.168Z 24.54512 -27.05031 15.4 704 4 101955 1999 RQ36 1999-09-11T11:12:12.384Z 24.56679 -27.04233 15.2 704
NEOCP Observations¶
The get_neocp_observations() method returns an ObservationsResult object for objects currently on the NEO Confirmation Page (NEOCP). The get_neocp_observations_df() convenience method returns results as a pandas DataFrame.
Note: NEOCP objects are transient — they are removed once confirmed or retracted. The tracklet ID used below may no longer be active when you run this cell.
# Retrieve NEOCP observations for a trksub that is currently on the NEOCP
current_trksub = pd.read_html("https://minorplanetcenter.net/iau/NEO/toconfirm_tabular.html")[0].iloc[0]['Temp Desig']
try:
result = mpc.get_neocp_observations(current_trksub, output_format="OBS80")
print(f"Type: {type(result).__name__}")
print(f"OBS80 (first 80 chars): {result.OBS80[:80]}...")
except Exception as e:
print(f"NEOCP object may no longer be active: {e}")
Type: ObservationsResult OBS80 (first 80 chars): KLIr1In !C2026 03 22.07965 11 08 33.64 -24 41 50.4 24.4 rW W84...
# Retrieve NEOCP observations as a DataFrame
try:
df_neocp = mpc.get_neocp_observations_df(current_trksub)
print(f"Shape: {df_neocp.shape}")
print(df_neocp.head())
except Exception as e:
print(f"NEOCP object may no longer be active: {e}")
Shape: (4, 76) Obstype artsat astcat band com ctr dec decstar delay deltadec \ 0 optical None Gaia3 r None None -24.69733 None None None 1 optical None Gaia3 r None None -24.70164 None None None 2 optical None Gaia3 r None None -24.70833 None None None 3 optical None Gaia3 r None None -24.71272 None None None ... subfrm sys trkid trkmpc trksub trx unctime vel1 vel2 \ 0 ... None None 000000ILFHN0 None KLIr1In None None None None 1 ... None None 000000ILFHN0 None KLIr1In None None None None 2 ... None None 000000ILFHN0 None KLIr1In None None None None 3 ... None None 000000ILFHN0 None KLIr1In None None None None vel3 0 None 1 None 2 None 3 None [4 rows x 76 columns]
Observatory Codes¶
Look up observatory information by code, retrieve all observatories as a DataFrame, or search by name.
# Single observatory lookup — returns an Observatory object
obs = mpc.get_observatory("F51")
print(f"Code: {obs.obscode}")
print(f"Name: {obs.name}")
print(f"Type: {obs.observations_type}")
Code: F51 Name: Pan-STARRS 1, Haleakala Type: optical
# Search observatories by name (returns a DataFrame)
results = mpc.search_observatories("Palomar")
print(results[["obscode", "name", "observations_type"]])
obscode name observations_type 261 261 Palomar Mountain-DSS optical 644 644 Palomar Mountain/NEAT optical 675 675 Palomar Mountain optical I41 I41 Palomar Mountain--ZTF optical K10 K10 Micro Palomar, Reilhanette optical
# Get all observatories as a DataFrame and analyze
all_obs = mpc.get_all_observatories_df()
print(f"Total observatories: {len(all_obs)}")
print("\nBy type:")
print(all_obs["observations_type"].value_counts())
Total observatories: 2687 By type: observations_type optical 2646 satellite 22 radar 15 occultation 2 roving 2 Name: count, dtype: int64
MPECs (Minor Planet Electronic Circulars)¶
Search for MPECs by object designation or wildcard pattern. The get_discovery_mpec() convenience method returns the earliest MPEC for an object.
# Search for MPECs about Apophis — returns Dict[str, List[MPEC]]
result = mpc.get_mpecs("Apophis")
mpecs = result.get("Apophis", [])
print(f"Found {len(mpecs)} MPECs for Apophis")
print("\nMost recent 3:")
for mpec in mpecs[:3]:
print(f" {mpec.fullname:12s} {mpec.pubdate[:10]} {mpec.title[:50]}")
Found 9 MPECs for Apophis Most recent 3: 2004-Y25 Mon, 20 De 2004 MN4 2004-Y60 Sat, 25 De 2004 MN4 2004-Y63 Sun, 26 De 2004 MN4
# Get the discovery MPEC for an object
discovery = mpc.get_discovery_mpec("`Oumuamua")
if discovery:
print(f"Discovery MPEC for `Oumuamua:")
print(f" MPEC: {discovery.fullname}")
print(f" Title: {discovery.title}")
print(f" Published: {discovery.pubdate}")
print(f" Link: {discovery.link}")
Discovery MPEC for `Oumuamua: MPEC: 2017-V38 Title: `Oumuamua Published: Fri, 10 Nov 2017 19:15:00 GMT Link: https://www.minorplanetcenter.net/mpec/K17/K17V38.html
Check Near-Duplicates (CND)¶
Check whether observations already exist in the MPC database. Returns a dict mapping each input observation to a list of NearDuplicateMatch objects. Useful before submitting observations to avoid duplicates.
# Check a single observation in MPC 80-column format
obs = "f9671 C2020 02 21.46921410 05 10.27 +04 52 25.7 19.37oV~3n2UT08"
results = mpc.check_near_duplicates(obs)
# Each match is a NearDuplicateMatch object with attribute access
for obs_line, matches in results.items():
print(f"Input: {obs_line[:60]}...")
print(f" Found {len(matches)} match(es)")
for match in matches:
print(f" obs80: {match.obs80[:60]}...")
print(f" time_separation_s: {match.time_separation_s}")
print(f" angle_separation_arcsec: {match.angle_separation_arcsec}")
# Count duplicates (convenient for batch checking)
observations = [
"f9671 C2020 02 21.46921410 05 10.27 +04 52 25.7 19.37oV~3n2UT08",
" abc123 C2020 03 23.46921410 05 13.27 +04 52 25.9 19.37oV T08",
]
counts = mpc.count_near_duplicates(observations)
for obs_line, count in counts.items():
status = f"{count} match(es)" if count > 0 else "no matches"
print(f" {obs_line[:40]}... -> {status}")
abc123 C2020 03 23.46921410 05 13... -> no matches f9671 C2020 02 21.46921410 05 10... -> 1 match(es)
Submission Status¶
Check whether a previously submitted observation file was accepted or rejected by the MPC pipeline. Returns a SubmissionStatus object.
# Check status of a known accepted submission
status = mpc.get_submission_status("2026-01-01T00:05:07.453_0000BhCE")
print(f"Accepted: {status.accepted}")
print(f"Pipeline entry time: {status.pipeline_entry_time}")
print(f"Fault events: {status.fault_events}")
Submitting Observations¶
The submit_xml() and submit_psv() methods submit ADES-formatted observation files to the MPC and return a SubmissionResponse object. By default they use the test endpoint (test=True) so you can safely experiment.
You can pass a file path or raw content as a string.
import tempfile, os, atexit, requests
# Download sample XML file from the MPC
sample_url = "https://www.minorplanetcenter.net/media/ades/goodsubmit.xml.txt"
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
f.write(requests.get(sample_url).text)
sample_xml_path = f.name
atexit.register(lambda p=f.name: os.unlink(p) if os.path.exists(p) else None)
print(f"Sample XML saved to: {sample_xml_path}")
# Submit to test endpoint
result = mpc.submit_xml(
sample_xml_path,
ack="mpc_client tutorial",
ac2="my@email.adr",
obj_type="NEO",
test=True,
)
print(f"Status: {result.status_code}")
print(f"Response: {result.message}")
Submitting PSV Files¶
The submit_psv() method works identically to submit_xml(), but accepts an ADES PSV (pipe-separated values) file instead of XML.
# Download a sample PSV file from the MPC
sample_psv_url = "https://www.minorplanetcenter.net/media/ades/goodsubmit.psv"
with tempfile.NamedTemporaryFile(mode='w', suffix='.psv', delete=False) as f:
f.write(requests.get(sample_psv_url).text)
sample_psv_path = f.name
atexit.register(lambda p=f.name: os.unlink(p) if os.path.exists(p) else None)
print(f"Sample PSV saved to: {sample_psv_path}")
# Submit PSV to test endpoint
result = mpc.submit_psv(
sample_psv_path,
ack="mpc_client tutorial PSV",
ac2="my@email.adr",
obj_type="NEO",
test=True,
)
print(f"Status: {result.status_code}")
print(f"Response: {result.message}")
Action Codes¶
Request retrieval of an action code for a previous submission. The code is emailed to the original submitter (not returned in the response). Returns an ActionCodeResponse object.
# Replace with your actual submission ID
result = mpc.request_action_code("2026-01-01T00:05:07.453_0000BhCE")
print(f"Status: {result.status}")
print(f"Message: {result.message}")
Error Handling¶
The mpc_client package provides a custom exception hierarchy for structured error handling:
MPCAPIError (base)
+-- MPCRequestError # Network/timeout failures
+-- MPCResponseError # Non-2xx HTTP status codes
| +-- MPCNotFoundError # 404 specifically
+-- MPCValidationError # Local input validation failures
from mpc_client import MPCValidationError, MPCNotFoundError
# Input validation catches bad arguments before making an API call
try:
mpc.get_observations("Bennu", output_format="INVALID_FORMAT")
except MPCValidationError as e:
print(f"Validation error: {e}")
# CND parameter range validation
try:
mpc.check_near_duplicates("some obs", time_separation_s=999)
except MPCValidationError as e:
print(f"Validation error: {e}")
Validation error: Invalid output_format 'INVALID_FORMAT'. Must be one of {'OBS_DF', 'ADES_DF', 'OBS80', 'XML'}
Validation error: time_separation_s must be between 0 and 60
# MPCNotFoundError is raised for 404 responses
try:
mpc.get_submission_status("2000-01-01T00:00:00.000_0000FAKE")
except MPCNotFoundError as e:
print(f"Not found (expected): {e}")
Not found (expected): Not found (404): https://data.minorplanetcenter.net/api/submission-status
Summary¶
The mpc_client package provides a convenient Python interface to all MPC public APIs. All methods return typed response objects with attribute access.
Key methods:
| Method | Returns | Description |
|---|---|---|
identify() |
Dict[str, DesignationInfo] |
Resolve object designations |
get_orbit() |
OrbitalElements |
Get orbital elements (mpc_orb format) |
get_observations() |
ObservationsResult |
Retrieve observations |
get_neocp_observations() |
ObservationsResult |
NEOCP observations |
get_observatory() |
Observatory |
Single observatory lookup |
get_mpecs() / get_discovery_mpec() |
Dict[str, List[MPEC]] / MPEC |
MPEC search |
check_near_duplicates() |
Dict[str, List[NearDuplicateMatch]] |
Duplicate checking |
get_submission_status() |
SubmissionStatus |
Check submission status |
submit_xml() / submit_psv() |
SubmissionResponse |
Submit observations |
request_action_code() |
ActionCodeResponse |
Retrieve action codes |
Installation: pip install mpc-client (or pip install mpc-client[dataframe] for DataFrame support)
Source code: https://github.com/Smithsonian/mpc-public/tree/main/mpc-client
Individual API tutorials: https://docs.minorplanetcenter.net/tutorials/api_tutorials/
For questions or feedback, contact the MPC via the Jira Helpdesk.