Skip to content

Instantly share code, notes, and snippets.

@travishorn
Last active October 6, 2025 12:31
Show Gist options
  • Select an option

  • Save travishorn/c2b6111a4e63efdbf87a1de84c833ab1 to your computer and use it in GitHub Desktop.

Select an option

Save travishorn/c2b6111a4e63efdbf87a1de84c833ab1 to your computer and use it in GitHub Desktop.
Saving the images and videos from your ClassDojo storyline

Archived

Please see Patrick330's fork of this script.

ClassDojo changes their site regularly, so a script like this needs regular maintenance. I have decided to archive this project and stop providing updates. Patrick330's fork linked above may be a good alternative.

Original Purpose

ClassDojo is a classroom communication app used to share reports between parents and teachers. Teachers track student behavior and upload photos or videos. The gamification style system teaches developmental skills through real-time feedback.

When your child's teacher shares a photo, it goes on your parent "storyline". Unfortunately, ClassDojo does not provide any means of saving these photos. In fact, the photos are displayed in a <div> using style: background-image('...'); so right-clicking and choosing "Save image" is not an option.

@MrMPM
Copy link

MrMPM commented Sep 4, 2023

When I go on Class Dojo website, the address is: https://home.classdojo.com/#/story
Should I use this or as per scripts above, /api/storyFeed? Anyway, neither works for me... @Loksly script does something, but returns error, other scripts return "undefined" immediately.

@neurolizer
Copy link

https://home.classdojo.com/api/storyFeed?withStudentCommentsAndLikes=true&withArchived=false is still the correct URL. If you go there directly, you can see the json (or whatever) formatted data that Loksly's script parses. If you just need a few pictures/videos, you can find the links in the api/storyFeed data and download them manually.
The code I posted is designed to be used inside the Tampermonkey extension for Chrome. It errors a lot for me too though, probably downloads less than half the pictures/videos because of CORS errors that I haven't found a way to fix.

@reloadfast
Copy link

Huge thank you @Loksly!

@chunte
Copy link

chunte commented Jul 5, 2024

Thanks @Loksly for your code. I started the code 3 hours ago, and it's still chugging along.

@neurolizer I tried using Tampermonkey (a pretty cool extension), but somehow it misses many files. For example the download counter goes from to ..., 15, 16, to 405, 406, so on and maybe to 1003, 1004, ...

@vizyonok
Copy link

vizyonok commented Jul 7, 2024

@neurolizer, yes, it looks like the CORS isn't supported anymore, so try switching to "no-cors" instead.

Just change the word cors 2 times in @Loksly's script above to just no-cors. It worked out for me.

@sunghin
Copy link

sunghin commented Jun 17, 2025

Thanks @Loksly for the script!!

I didn't have enough posts on my feed to warrant downloading Tampermonkey, but like others pointed out, the download counter indicated that a lot of images were skipped, even using @Loksly's original script.

I updated his script with a 0.1s delay between downloads to make sure there's no skipped or overlapping downloads:

  • Replaced attachments.reduce(...) with a standard for loop.
  • Added await delay(100) between each download.
  • Added a delay() helper function that returns a Promise that resolves after 100 ms.

My updated script in full:

const FIRST_FEED = "https://home.classdojo.com/api/storyFeed?withStudentCommentsAndLikes=true&withArchived=false";

function getFeed(url) {
    return fetch(url, {
        "headers": {
            "accept": "*/*",
            "accept-language": "es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7",
            "cache-control": "no-cache",
            "pragma": "no-cache",
            "sec-ch-ua": "\"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"114\", \"Google Chrome\";v=\"114\"",
            "sec-ch-ua-mobile": "?0",
            "sec-ch-ua-platform": "\"Linux\"",
            "sec-fetch-dest": "empty",
            "sec-fetch-mode": "cors",
            "sec-fetch-site": "same-origin",
            "x-client-identifier": "Web",
            "x-sign-attachment-urls": "true"
        },
        "referrer": "https://home.classdojo.com/",
        "referrerPolicy": "strict-origin-when-cross-origin",
        "body": null,
        "method": "GET",
        "mode": "cors",
        "credentials": "include"
    }).then((response) => response.json());
}

const attachments = [];

async function d() {
    let feed = await getFeed(FIRST_FEED);
    grabFeedAttachments(feed);

    while (feed._links.next && feed._items.length > 0) {
        feed = await getFeed(feed._links.next.href);
        grabFeedAttachments(feed);
    }

    for (let i = 0; i < attachments.length; i++) {
        await downloadAttachment(attachments[i], i);
        await delay(100); // wait 100ms between downloads
    }
}

function grabFeedAttachments(feed) {
    feed._items.forEach((item) => {
        item.contents.attachments?.forEach((attachment) => {
            if (typeof attachment.path === "string") {
                attachments.push({
                    url: attachment.path,
                    time: item.time
                });
            }
        });
    });
}

function downloadAttachment(attachment, counter) {
    return fetch(attachment.url).then((t) => {
        return t.blob().then((b) => {
            let a = document.createElement("a");
            a.href = URL.createObjectURL(b);

            let filename = String(counter) + "_" + attachment.time.split('T')[0];

            a.setAttribute("download", filename);
            a.click();
        });
    });
}

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

d();

@Loksly
Copy link

Loksly commented Jun 17, 2025

Thanks for the info.

Please consider supporting Classdojo.

@brimaster
Copy link

Loksly's script with sunghin's edits worked perfectly! I just had to start a new Chrome session with web security disabled (do at your own risk!)

On Windows, simply create a shortcut with the target: "[path to chrome]\chrome.exe" --disable-web-security --disable-gpu --user-data-dir=%LOCALAPPDATA%\Google\chromeTemp

@MacManas
Copy link

MacManas commented Jul 4, 2025

If you have more than one kid, you can use:
const FIRST_FEED = "https://home.classdojo.com/api/storyFeed?withStudentCommentsAndLikes=true&studentId=<STUDENT_ID>";
The <STUDENT_ID> is the number that appears in the URL when you select the feed from only one kid.
Also, had to remove withArchived=false since otherwise it tends to mix feeds from the kids for some reason. Since I don't have access to the API docs, I cannot tell why this is the case

@rostmodern
Copy link

is there a way to also grab the text of the posts?
a simple text file also mentioning the corresponding jpeg files might be good start to archive this valuable info or metadata.

Is this possible via the api or any other way?? Is there a project already addressing this issue?
I could not find one...
Any help is greatly appreciated!

@8bitwilliam
Copy link

8bitwilliam commented Oct 6, 2025

is there a way to also grab the text of the posts? a simple text file also mentioning the corresponding jpeg files might be good start to archive this valuable info or metadata.

Is this possible via the api or any other way?? Is there a project already addressing this issue? I could not find one... Any help is greatly appreciated!

I have a Chromium-based extension that may help if you’re using browsers like Chrome or Edge.
https://github.com/8bitwilliam/Ninja

It saves the post information for each day to a text file, including the file name for reference, and of course downloads any photos and videos from the specified date range.

The text file looks something like...

[2025-01-01 12:]
Teacher:
Class:
Story:
Files:

  • 2025-01-01_8qfowp4b7a.jpg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment