API V2 Beta Documentation

Each review object is the state of the single card after being successfully answered.

For example, you do a review for kanji 金. You answered meaning correct, but answered the reading incorrect twice before answering it correctly. This result gets recorded as a new review object. Every subsequent review of this kanji will be recorded as a separate review object.

If you are looking for the last “failed” review for a particular subject you’ll want to use the subject_ids (or assignment_ids) filter on the /reviews endpoint. Order the payload by descending data.created_at and select the first data.incorrect_meaning_answers != 0 || data.incorrect_reading_answers !=0

1 Like

Thanks this got me on the right track!

I wanted a script to generate me an export of everything I failed in the past 7 days.

If anyone is interested you can find my script here: https://github.com/pamput/wanikani-tools/tree/master/recent_fail

1 Like

I’m trying to do a really simple thing - export a list of kanji known to the user (that is, srs level 1 or greater). I’ve played around with the API a bit, but can’t seem to find a way of achieving this. The best I could do was:

https://api.wanikani.com/v2/assignments?subject_types=kanji&srs_stages=1,2,3,4,5,6,7,8,9

But that doesn’t give me directly the kanji, I’d have to fetch the subjects in a second call, using the ids as arguments.

Another thing I tried is: https://api.wanikani.com/v2/subjects?types=kanji&levels=1,2,3… giving the levels up until the user’s level. That would include the kanji, although non-unlocked ones for the current level as well, and buried pretty deep in a huge amount of JSON.

Is there a simpler way I’ve missed?

Since I’m not sure if you’re developing an app or just wanting to fetch your own list, here are three options:

Just using API fetches, there’s no way around having to fetch the whole /subjects endpoint and cross-referencing the subject_id of each result from your first query above.

But, if you’re using a browser, the Wanikani Open Framework makes this a lot easier since it does the fetches and cross-linking for you. If you have the framework installed, go to WK dashboard, and paste the block below into your Javascript console:

wkof.include('ItemData');
wkof.ready('ItemData')
.then(fetch_items)
.then(process_items);

function fetch_items() {
 return wkof.ItemData.get_items('assignments');
}

function process_items(items) {
  var kanji = items.filter((item) => item.object === 'kanji');
  var learned = kanji.filter((item) => {
    return (
      item.assignments &&
      item.assignments.unlocked_at &&
      item.assignments.srs_stage > 0
    );
  });
  var characters = learned.map((item) => item.data.slug);
  console.log(characters.join(','));
}

(edit: now that I’m at my computer, I was able to check the code above, and fixed a missing parenthesis)

Also, non-coders can go to wkstats and click on the “Not Learned” button to hide non-learned items.

2 Likes

Just curious, but is there (or are there plans to) publish a test user or a test server instance? I’m starting to write some automated tests for KameSame, and short of signing up a trial user for WK I don’t have a lot of great options for fetching a user in a known state.

(I could of course build fixtures for all the API responses I need, but it’d be nice to have at least one test suite that goes end-to-end to WK’s servers)

There’s nothing publicly available. No idea what they do internally.

I use a trial account for some of my testing, but most of what I do doesn’t need to be static.

I am currently just mocking out responses I need. Its not too cumbersome

@viet @oldbonsai When you have time during business hours, can you tell me if there is an easy way to get an aggregate value across all reviews of the percentages that show up on the review summary page after a review session is completed? Right now the only easy stat I know we can get is the percentage from answers during a review session, which is not nearly as useful.

If you know when the review has started, then you can use the timestamp with the updated_after filter on /review_statistics. This will return all review statistics information belong to subjects reviewed after the update_after timestamp. This works on the assumption review_statistic objects only get updated after a subject’s review is submitted, which is true right now.

The proper way would be to use the updated_after filter on /reviews, collect the subject_id, and then hit up /review_statistics with the subject_ids filter.

The former is one query versus the latter’s two queries.

Let me know if I misunderstand your question.

I think I got it, thanks!

CC @rfindley (though I assume you knew most of that already)

Will mnemonics be included in the response structure for kanji ?

So I’m interested in doing two things with the API - one thing seems easy, the other I’m not sure is even possible with the data provided.

The first thing is, like the bar graph on Anki that shows visually the number reviews coming day by day. I plan to have a stacked bar for each day (or zoomable by hour, day, month, etc), each bar colored by number of apprentice, guru, etc reviews.on that day. This project seems pretty straightforward by dumping the json, converting to csv, and then playing with pivot charts in excel.

The other thing I really want to do, though, is get a success rate statistic for each SRS level. How often am I forgetting at the guru 1 stage? How often am I blowing it at the enlightened stage? I’m expecting that all the stages would be about the same - IF the time intervals are right for me. But if one stage is significantly lower than the rest, maybe that time interval is just too long (for MY individual memory). I might want to “cheat” and do an extra review in that interval.

I’m not seeing an obvious way to get the data for the second thing. Any ideas?

The /reviews payload provides starting and ending srs stages for each registered review. You can infer the review was unsuccessful by ending srs stage < starting srs stage or by looking at the incorrect_* counts > 0.

This should provide the info you’ll need, if I am understanding your goal correctly.

Ah, yes, I see it now. Thank you sir!

Which makes me think, it would be super-cool if the time intervals for each SRS stage were user-adjustable in the app in an “advanced settings - change at your own risk” kind of way. Side benefit - all the people who think WK is too slow for them could speed it up until they crush themselves under the workload :smiley:

1 Like

@ctmf,

Pardon if you’re already aware of the Ultimate Timeline script… it sounds like what you’re trying to do, but maybe you’re doing some long-term tracking or something instead?

And regarding the accuracy for each SRS level…
If you have Wanikani Open Framework installed, you can get your accuracy for each SRS level by pasting the following into the Javascript console:

Click to view code
wkof.include('Apiv2');
wkof.ready('Apiv2').then(fetch_data).then(process_data);
function fetch_data() {
	return wkof.Apiv2.fetch_endpoint('/reviews');
}
function process_data(json) {
	let stages = ['Lessons', 'Apprentice 1', 'Apprentice 2', 'Apprentice 3', 'Apprentice 4', 'Guru 1', 'Guru 2', 'Master', 'Enlightened'];
	let reviews = json.data;
	let srs_total = new Array(9).fill(0);
	let srs_correct = new Array(9).fill(0);
	reviews.forEach(review => {
		srs_total[review.data.starting_srs_stage]++;
		if (review.data.ending_srs_stage > review.data.starting_srs_stage) {
			srs_correct[review.data.starting_srs_stage]++;
        }
	});
	console.log('Accuracy:');
	for (let i=1; i<9; i++) {
		console.log('  '+stages[i]+': '+(srs_correct[i] / srs_total[i] * 100).toFixed(2)+'%');
    }
}
1 Like

Thanks! That is pretty close to what I want to do. Half of the fun, though, is teaching myself to do all this. Already I’ve had to teach myself how to do a GET request with the authentication header, and now I’m working on converting anything but the simplest JSON to CSV. I’ve been using a handwritten Ruby script that’s evolving hour by hour. It passes the time between WK reviews :slight_smile:

The other half of the fun is having a pile of data to nerd around with in Excel pivot tables.

Thanks for the shortcut if I need it (and code to see one way to accomplish the goal)!

I wondered if that might be part of it.
Code away, and have fun!

Is there a more general documentation that is a little bit more explanatory about the certain purpose of the endpoints? I searched a lot but I could either not find it or I must have missed it.

Additionally, I could not find any functionality to update data of the account. Lets say I want to set the specific level of a Kanji, how would I do that? Is that even possible?

Thank you for helping me out.

The API is read-only, there is no way to make changes to anything. The purpose of this API is to expose information for easy stat tracking, and similar purposes.

/user gets your user information
/subjects gets information on radicals, kanji, and vocab
/assignments gets information on the status of a subject/set of subjects for the requesting user (SRS level, when it was unlocked, guru’d, burned, resurrected, etc)
/review_statistics tells you how well you’ve been doing on a subject
/study_materials gets your notes and synonyms for subjects
/summary gets your next review time, and shows what makes up your upcoming review and lesson sessions
/reviews shows how well you’ve answered for a subject in reviews
/level_progressions shows how you’ve been leveling up
/resets gets your reset history

ctmf:

I might have done what you’re trying to do in regards to graphing some statistics over time, and maybe you’d have fun replicating some of it.

I pull the API down into InfluxDB, then I have a little PHP + Chart.js front-end I use to look at it. Here’s all the code :slight_smile:

PHP Script - WaniKani API to InfluxDB
This file is on a cron-job every 6 hours and pulls down my statistics and pops them into a local instance of InfluxDB. You’ll need to set the WANIKANI_API_KEY environment variable.

<?php
/**
 * Pulls down Wanikani API data and installs it into the local InfluxDB instance
 */
function pullWaniKaniData() {
    $apiKey = getenv('WANIKANI_API_KEY');
    try {
        $wanikaniCurl = curl_init();
    } catch (Exception $e) {
        echo "Curl Initialization failed! Ensure php-curl is installed.\n";
        exit;
    }
    curl_setopt($wanikaniCurl, CURLOPT_URL, "https://www.wanikani.com/api/user/$apiKey/srs-distribution");
    curl_setopt($wanikaniCurl, CURLOPT_RETURNTRANSFER, 1);
    try {
        $output = curl_exec($wanikaniCurl);
        $jsonData = json_decode($output, true)['requested_information'];
    } catch (Exception $e) {
        echo "Exception!" . $e->getMessage();
    }
    curl_close($wanikaniCurl);
    return $jsonData;
}

/**
 * Adds skill-levels and item count to the wanikani InfluxDB Database
 * @param $wkSrsDistribution The key-value set of skill->counts data from WaniKani API v1.4
 *      This is the data encoded in the requested_information object.
 * @return $dataString string The String used as a POST request to the InfluxDB Backend
 */
function buildInfluxPostString($wkSrsDistribution) {
    $dataPoints = array();
    foreach ($wkSrsDistribution as $skillLevel=>$counts) {
        if (isset($counts['total'])) {
            array_push($dataPoints, "$skillLevel=" . $counts['total']);
        }
    }
    $dataString = 'progress ' . implode(',',  $dataPoints);
    return $dataString;
}

/**
 * Creates a new entry in the influxDB Database for Wanikani statistics.
 *
 * @param $influxPostString string The formatted POST data to create a new entry
 *      This should be formatedw ith the buildInfluxPostString function
 */
function insertInfluxEntry($influxPostString) {
    try {
        $influxCurl = curl_init();
    } catch (Exception $e) {
        echo "Curl Initialization failed! Ensure php-curl is installed.\n";
        exit;
    }
    print_r($influxPostString);
    curl_setopt_array($influxCurl, array(
        CURLOPT_URL => "http://localhost:8086/write?db=wanikani",
        CURLOPT_POST => 1,
        CURLOPT_POSTFIELDS => $influxPostString,
        CURLOPT_HEADER => 1,
        CURLOPT_HTTPHEADER => array(
            "Content-Type: text/plain",
            "cache-control: no-cache"
        ),
    ));

    try {
        $output = curl_exec($influxCurl);
        print_r($influxCurl);
    } catch (Exception $e) {
        echo "Exception!" . $e->getMessage();
    }
    curl_close($influxCurl);
}

/*****************************************************************************
 *                                   MAIN
 ****************************************************************************/
$jsonData = pullWaniKaniData();
$influxPostString = buildInfluxPostString($jsonData);
echo insertInfluxEntry($influxPostString);

PHP Backend for API Endpoint
This file sits on my webserver and is requested by the javascript front-end via GET request. I’m sure someone will cry at that sort function but hey, I was lazy…

<?php
/**
 * Queries the influxDB and loads the JSON into a chartsJS compatible format
 */
function queryInfluxDb() {
    try {
        $influxCurl = curl_init();
    } catch (Exception $e) {
        echo "Curl Initialization failed! Ensure php-curl is installed.\n";
        exit;
    }
    $query = urlencode("SELECT * FROM progress");
    curl_setopt_array($influxCurl, array(
        CURLOPT_URL => "http://localhost:8086/query?pretty=true&db=wanikani&q=" . $query,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => "GET",
        CURLOPT_RETURNTRANSFER => TRUE,
        CURLOPT_HTTPHEADER => array(
            "cache-control: no-cache"
        ),
    ));
    try {
        $output = json_decode(curl_exec($influxCurl), True);
    } catch (Exception $e) {
        echo "Exception!" . $e->getMessage();
    }
    curl_close($influxCurl);
    return $output['results'][0]['series'][0];
}

function generateChartData($influxQueryResult) {
    $labels = array();
    $datasets = array();
    $labelMap = $influxQueryResult['columns'];
    $colorMap = array('', '#dd0093', '#e49900', '#0093dd', '#882d9e', '#294ddb');

    // Initialize Datasets - Skip Column 0
    for( $i = 1; $i < sizeOf($influxQueryResult['columns']); $i++) {
        $datasets[$i] = array(
            "label" => $labelMap[$i],
            "data" => array(),
            "backgroundColor" => $colorMap[$i],
            "borderColor" => $colorMap[$i],
        );
    }

    // Build Time Labels and dataset values
    foreach($influxQueryResult['values'] as $row) {
        for ($i = 0; $i < sizeOf($row); $i++) {
            // Time Labels are always first
            if (0 == $i) {
                array_push($labels, $row[0]);
            } else {
                array_push($datasets[$i]['data'], $row[$i]);
            }
        }
    }

    // Re-order Datasets
    $orderedData[0] = $datasets[1];
    $orderedData[1] = $datasets[4];
    $orderedData[2] = $datasets[5];
    $orderedData[3] = $datasets[3];
    $orderedData[4] = $datasets[2];

    // Fix Names
    $orderedData[0]['label'] = "Apprentice";
    $orderedData[1]['label'] = "Guru";
    $orderedData[2]['label'] = "Master";
    $orderedData[3]['label'] = "Enlightened";
    $orderedData[4]['label'] = "Burned";

    return array(
        "labels" => $labels,
        "datasets" => $orderedData,
    );
}

$data = queryInfluxDb();
header('Content-type:application/json;charset=utf-8');
echo json_encode(generateChartData($data));

And the Javascript function to pull it to the web UI. I’ve scrubbed the URL I use below.

var loadWaniKani = (function loadWaniKaniChart() {
    fetch('https://www.example.com/api/wanikani/progress')
        .then((resp) => resp.json())
        .then(function (data) {
            var wkContext = document.getElementById("wkChart").getContext('2d');
            var wkChart = new Chart(wkContext, {
                type: 'line',
                data: {
                    labels: data.labels,
                    datasets: Object.values(data.datasets),
                },
                options: {
                    elements: {
                        point: {
                            radius: 0
                        }
                    }, 
                    animation: false, 
                    tooltips: {
                        mode: 'index',
                    },
                    hover: {
                        mode: 'index'
                    },
                    scales: {
                        xAxes: [{
                            type: 'time',
                            time: {
                                unit: 'day',
                                unitStepSize: 1,
                                displayFormats: {
                                    day: 'MMM D'
                                }
                            },
                            distribution: 'linear'
                        }],
                        yAxes: [{
                            stacked: true,
                            ticks: {
                                beginAtZero: true
                            }
                        }]
                    }
                }
            });
        })
        .catch(function (error) {
            console.log(error);
        });
})();

And then I wind up with this in the end!

I hope this helps!

2 Likes