NASA EPIC and APOD APIs in Python#

By Sebastian Shirk and Avery Fernandez

The NASA Earth Polychromatic Imaging Camera (EPIC) API provides access to daily imagery of Earth captured by the DSCOVR satellite, offering valuable data for climate research and Earth observation. The Astronomy Picture of the Day (APOD) API delivers daily images or videos of astronomical phenomena, accompanied by brief explanations written by professional astronomers.

Please see the following resources for more information on API usage:

NOTE: The NASA APIs limit requests to a maximum of 1,000 per hour.

These recipe examples were tested on May 7, 2025.

Setup#

Import Libraries#

The following external libraries need to be installed into your enviornment to run the code examples in this tutorial:

We import the libraries used in this tutorial below:

import requests
from PIL import Image, ImageDraw, ImageFont
from dotenv import load_dotenv
import os
from pprint import pprint

Import API Key#

An API key is required to access the APOD API. You can sign up for one at the APOD Developer Portal.

We keep our API key in a separate file, a .env file, and use the dotenv library to access it. If you use this method, create a file named .env in the same directory as this notebook and add the following line to it:

APOD_API_KEY=PUT_YOUR_API_KEY_HERE
load_dotenv()
try:
    API_KEY = os.environ["APOD_API_KEY"]
except KeyError:
    print("API key not found. Please set 'APOD_API_KEY' in your .env file.")
else:
    print("Environment and API key successfully loaded.")
Environment and API key successfully loaded.

Create Images Folder#

All images will be saved in an images folder that will be created in the current working directory.

if not os.path.exists("images"):
    os.makedirs("images")

1. Get the Latest Images of Earth (EPIC)#

This will get the latest images of Earth from the NASA EPIC API and download them as PNGs to your local directory.

Change the collection variable to see different collections of images.

BASE_URL = "https://api.nasa.gov/"
endpoint = "EPIC/api/"
params = {
    'api_key': API_KEY,
}

# Collection options: natural, enhanced, cloud, aerosol
collection = "natural"

try:
    response = requests.get(f"{BASE_URL}{endpoint}{collection}", params=params)
    # Raise an error for bad responses
    response.raise_for_status()  
    data = response.json()
    pprint(data[0], depth=1)
except requests.exceptions.RequestException as e:
    print(f"Error fetching data from API: {e}")
    data = None
{'attitude_quaternions': {...},
 'caption': "This image was taken by NASA's EPIC camera onboard the NOAA "
            'DSCOVR spacecraft',
 'centroid_coordinates': {...},
 'coords': {...},
 'date': '2025-05-05 00:50:27',
 'dscovr_j2000_position': {...},
 'identifier': '20250505005515',
 'image': 'epic_1b_20250505005515',
 'lunar_j2000_position': {...},
 'sun_j2000_position': {...},
 'version': '03'}
endpoint = 'EPIC/archive/'

# This code block downloads the latest 20 images of Earth available through EPIC, which takes
# a picture about every 5 minutes
images = []
for item in data:
    year, month, day = item["date"].split(" ")[0].split("-")
    image = item["image"]
    try:
        response = requests.get(
            f"{BASE_URL}{endpoint}{collection}/{year}/{month}/{day}/png/{image}.png",
            params=params
        )
        # Raise an error for bad responses
        response.raise_for_status()
        image_content = response.content

        with open(f"images/{image}.png", "wb") as img_file:
            img_file.write(image_content)

        img = Image.open(f"images/{image}.png")
        draw = ImageDraw.Draw(img)
        font = ImageFont.load_default(100)
        date_position = (20, 10)
        time_position = (20, 100)
        draw.text(date_position, item["date"].split(" ")[0], font=font, fill="white")
        draw.text(time_position, item["date"].split(" ")[1], font=font, fill="white") 
        img.save(f"images/{image}.png")  

        images.append(f"images/{image}.png")
    except requests.exceptions.RequestException as e:
        print(f"Error fetching image {image}: {e}")

Example Image

NASA Image

2. Get Earth Images from a Specific Date (EPIC)#

Use the get_valid_dates() function defined below to gather a list of all valid dates where images are available through the EPIC API.

Note that most dates from the launch of the API on June 13, 2015 are valid. However, there are several missing dates, as you can see below.

endpoint = "EPIC/api/"

try:
    response = requests.get(f"{BASE_URL}{endpoint}{collection}/all", params=params)
    # Raise an error for bad responses
    response.raise_for_status()  
    data = response.json()
except requests.exceptions.RequestException as e:
    print(f"Error fetching data from API: {e}")
    data = None
dates = [item["date"].split(" ")[0] for item in data]

# Print the last 10 elements in the list
dates[:-10:-1]
['2015-06-13',
 '2015-06-16',
 '2015-06-17',
 '2015-06-18',
 '2015-06-20',
 '2015-06-21',
 '2015-06-22',
 '2015-06-27',
 '2015-06-30']

Notice the gaps in the above results. Before we retrieve the images for a given date, let’s ensure that the date is available through the API:

# Note that this date is available
if '2016-05-15' in dates:
    print('2016-05-15 is valid')

# Note that this date is not available
if '2022-06-15' not in dates:
    print('2022-06-15 is not valid')
2016-05-15 is valid
2022-06-15 is not valid
endpoint = "EPIC/api/"
date = "2016-05-15"

try:
    response = requests.get(
        f"{BASE_URL}{endpoint}{collection}/date/{date}",
        params=params
    )
    # Raise an error for bad responses
    response.raise_for_status()  
    data = response.json()
except requests.exceptions.RequestException as e:
    print(f"Error fetching data from API: {e}")
    data = None
endpoint = 'EPIC/archive/'

# Download images from the specified data
images = []
if data:
    for item in data:
        year, month, day = item["date"].split(" ")[0].split("-")
        image = item["image"]
        try:
            response = requests.get(
                f"{BASE_URL}{endpoint}{collection}/{year}/{month}/{day}/png/{image}.png",
                params=params
            )
            # Raise an error for bad responses
            response.raise_for_status()
            image_content = response.content

            with open(f"images/{image}.png", "wb") as img_file:
                img_file.write(image_content)

            img = Image.open(f"images/{image}.png")
            draw = ImageDraw.Draw(img)
            font = ImageFont.load_default(100)
            date_position = (20, 10)
            time_position = (20, 100)
            draw.text(date_position, item["date"].split(" ")[0], font=font, fill="white")
            draw.text(time_position, item["date"].split(" ")[1], font=font, fill="white") 
            img.save(f"images/{image}.png")  

            images.append(f"images/{image}.png")
        except requests.exceptions.RequestException as e:
            print(f"Error fetching image {image}: {e}")

Stitch the Images Together#

This will stitch the images together to create one image containing all the images for easier viewing.

loaded_images = [Image.open(image) for image in images]

# Split the images into two rows
halfway = len(loaded_images) // 2
first_row_images = loaded_images[:halfway]
second_row_images = loaded_images[halfway:]

# Get dimensions of the first image
widths, heights = zip(*(i.size for i in loaded_images))

total_width_first_row = sum(width.size[0] for width in first_row_images)
total_width_second_row = sum(width.size[0] for width in second_row_images)
max_width = max(total_width_first_row, total_width_second_row)
max_height = max(heights)

# Create a new blank image with the max width and twice the max height
stitched_image = Image.new('RGB', (max_width, max_height * 2))

# Paste each image into the blank image
x_offset = 0
for im in first_row_images:
    stitched_image.paste(im, (x_offset, 0))
    x_offset += im.size[0]

x_offset = 0
for im in second_row_images:
    stitched_image.paste(im, (x_offset, max_height))
    x_offset += im.size[0]
stitched_image.save("images/Earth_Image_Stitched.png")

Stitched Image

3. Get the Astronomy Picture of the Day (APOD)#

This will get the Astronomy Picture of the Day from the NASA APOD API and download it as a PNG to your local directory.

You can get a random APOD image from their collection instead by uncommenting the two commented lines.

Note that the APOD API can only be called 30 times per IP address per hour and only 50 times per day.

date = "2016-05-15" # Set to None if you want to get a random image

endpoint = "planetary/apod"

if date:
    params["date"] = date
    try:
        response = requests.get(f"{BASE_URL}{endpoint}", params=params)
        # Raise an error for bad responses
        response.raise_for_status()  
        data = response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error fetching data from API: {e}")
        data = None
else:
    try:
        response = requests.get(f"{BASE_URL}{endpoint}", params=params)
        # Raise an error for bad responses
        response.raise_for_status()  
        data = response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error fetching data from API: {e}")
        data = None
# Download Astronomy Picture of the Day
image_url = data["url"]
media_type = data["media_type"]

if media_type == "image":
    image_path = f"images/APOD_Image.png"
    try:
        response = requests.get(image_url)
        # Raise an error for bad responses
        response.raise_for_status()  
        with open(image_path, "wb") as img_file:
            img_file.write(response.content)
    except requests.exceptions.RequestException as e:
        print(f"Error fetching image: {e}")
else:
    print("The media type is not an image.")
    print("You can check the URL in your browser:")
    print(image_url)

Example Image

NASA APOD Image