API V2 Beta Documentation

So here is the game plan for the API v2:

What needs to be done before entering beta

  • Creating proper API documentation (and providing Swagger and Insomnia imports)

What needs to be done before entering out of beta and into sunsetting mode for v1:

  • Soft deletion attributes
  • Inclusion of asset content (audio, sentences, radical SVG and PNG) [Pending terms and conditions]
  • JSON authentication endpoint for better extraction of API key
  • Possible moving the API to a subdomain (along with the WK app itself)
1 Like

When a user’s subscription lapses, do all of the endpoints only dispense data for the free levels?
Any thoughts on supporting up to the level that the user reached instead?

(My subtext for asking is that my subscription ends soon, and I’m wondering about supporting userscripts. In hindsight, a lifetime subcription would have been helpful for that sole purpose, but I didn’t even know what userscripts were when I first subscribed to WK :slight_smile: )

We are planning to make all content accessible (some restriction on the subjects though, per terms and conditions we are still drafting up), regardless of the user’s subscription state. I believe thats how the API v2 is set up right now.

1 Like

If anyone else is trying this in PowerShell, here’s a snippet I used:

$Headers = @{ "Authorization" = "Token token=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" }
$User = Invoke-RestMethod -Headers $Headers "https://www.wanikani.com/api/v2/user"
$User.data


username                    : doncr
level                       : 38
profile_url                 : https://www.wanikani.com/users/doncr
started_at                  : 2014-06-28T09:56:56.716474Z
subscribed                  : True
current_vacation_started_at :

New to Authorization Token (and also new to Javascript.) Any easy way to put in subject_id and return the real character?

var v2key = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX';
    var url = 'https://www.wanikani.com/api/v2/';
    var path = 'assignments';
    var id = '?srs_stages=9';
    $.ajax({ // ajax call starts
        url: url + path + id,
        dataType: 'json', // Choosing a JSON datatype
        method: 'GET',
        headers : { 'Authorization' : 'Token token={' + v2key + '}' },
    })
    .done(function(data) {
        console.log(data);
        debugger;
        for(i=0; i< data.data.length; i++){
            id = '/' + data.data[i].data.subject_id;
            path = 'subjects';
            $.ajax({ // ajax call starts
                url: url + path + id,
                dataType: 'json', // Choosing a JSON datatype
                method: 'GET',
                headers : { 'Authorization' : 'Token token={' + v2key + '}' },
            })
            .done(function(data2) {
                console.log(data2.data.character);
                //$('col-md-9').append(JSON.stringify(data, undefined, 2) + '<br>');
            }).fail(function(err) {
                console.error(err);
            });
        }
    }).fail(function(err) {
	  	alert(err);
	});

It seems like if I request too much, there is error, and the request stops working.

Rather, I might even ask, what knowledge is required?

Can you tell me what exactly you are trying to accomplish?

To me it looks like you are doing the following:

  1. Looking up for all your burned assignments which have no been resurrected
  2. And looking up the subjects which belong to the assignments and returning the subject characters.

If this is the case, I can go over what I would do.

But before we start I want to address the 403s you are getting.

The reason you are getting 403 is because you are hitting the rate limiter we have in place (10 requests per second and 60 requests per minute). The code you are using is doing a request for each assignment to retrieve the subject. If you have a lot of burned items (can be in the multiple 1000s given your level), you are going to exceed the rate limit really quickly.

Anyway, back to achieving your goal.

I am going to demonstrate two ways to do it. The first way I will use your existing code and alter it so it is more efficient. The second way is how I would personally write out the code to achieve your goal.

How to achieve your goal using your existing code (jQuery)

var apiKey = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX',
    url = 'https://www.wanikani.com/api/v2/',
    endpoint = 'assignments',
    parameters = '?srs_stages=9';

$.ajax({
  url: url + endpoint + parameters,
  dataType: 'json',
  method: 'GET',
  headers : { 'Authorization' : 'Bearer ' + apiKey },
}).done(function(responseBody) {
  var subject_ids = responseBody.data.map(a => a.data.subject_id).join(),
      endpoint2 = 'subjects',
      parameters2 = '?ids=' + subject_ids;

  $.ajax({
    url: url + endpoint2 + parameters2,
    dataType: 'json',
    method: 'GET',
    headers : { 'Authorization' : 'Bearer ' + apiKey },
  }).done(function(responseBody2) {
    var subject_characters = responseBody2.data.map(s => s.data.characters).join(', ');
    
    console.log(subject_characters);
  }).fail(function(error) {
    alert(error);
  });
}).fail(function(error) {
  alert(error);
});

How to achieve your goal using my way (Javascript)

var apiBaseUrl = 'https://www.wanikani.com/api/v2/';
var apiToken = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX';
var requestHeaders =
  new Headers({
    'Wanikani-Revision': '20170710',
    Authorization: 'Bearer ' + apiToken,
  });

var assignmentsApiEndpointPath = 'assignments';
var assignmentsFilters = '?srs_stages=9';
var assignmentsApiEndpoint =
  new Request(apiBaseUrl + assignmentsApiEndpointPath + assignmentsFilters, {
    method: 'GET',
    headers: requestHeaders
  });

fetch(assignmentsApiEndpoint)
  .then(function(response) { return response.json(); })
  .then(function(responseBody) { return responseBody.data.map(a => a.data.subject_id).join(); })
  .then(function(subject_ids) {
    var subjectsApiEndpointPath = 'subjects';
    var subjectsFilters = '?ids=' + subject_ids;
    var subjectsApiEndpoint =
      new Request(apiBaseUrl + subjectsApiEndpointPath + subjectsFilters, {
        method: 'GET',
        headers: requestHeaders
      });

    fetch(subjectsApiEndpoint)
      .then(function(response) { return response.json(); })
      .then(function(responseBody) { console.log(responseBody.data.map(s => s.data.characters).join(', ')); });
  });

The key thing in both examples: only two requests are made to the API. This keeps you under the rate limit.

What I did is collect all the subject_ids from the assignments response, join them in a comma-delimited string, and leverage the ids filter on the subjects endpoint to get the subjects related to the assignments.

If you are going to be hitting the API a lot to look for subjects, it is best to cache the subjects locally and do a find on the cache rather that hitting the API every time you want the subject data.

Ideally, you would know a programming language. But if you rather not program, I suggest using something like Insomnia, which is free and excellent.

1 Like

Thanks, but

Anyway, I’ll try to solve it myself, based on this info

BTW, I think the answer is localStorage / jStorage.

I think the parameters being passed is too large… Going to see if I can replicate and offer an alternative solution.

Also, best way to deal with pagination of more than 1,000 items? Best way to make use of responseBody.pages.next_url?

I am trying to detect leeches amongst burned items, i.e. calculate leech score; and resurrect according to leechness (probably mass resurrection via Burn Manager).

I figured that even 100-parameters are too large; however, 50 is acceptable.

I have a plan; still, I have yet to learn how to wait for Request-Per-Minute. Also, how to automate pagination.

My plan is

  1. Cache all items’ subject id, and correspondent Radical/Kanji/Vocab (x9000 in Subject)
    • Separate all items into 4 categories: Radicals with Unicode, Kanji, Vocab and Radicals with Image
  2. Cache all burned items’ subject id (x7000 in Assignments)
  3. Cache all items’ statistics (x9000 in Review Statistics)
  4. Sort out cached data

So, I’ll need to deal with 25k items. I have to tell the program to learn to wait.

Now, I have a problem with over-caching, more than 5 MB of localStorage… (especially for the statistics alone.)

Indexeddb has a storage limit of about 50MB, but it’s not as easy to use. There are libraries you can use to make it easier.

I imagine you wrap the ajax in a while loop which is triggered by a flag variable. You’ll need to define a starting url variable and an empty data array variable outside the function.

Inside the while loop you’ll do the ajax call by passing in the variable with the starting URL, push the data from the response body to the array variable, and then check if pages.next_url exists. If it does, you leave the flag variable alone and set the pages.next_url to the url variable. If pages.next_url does not exist, then you toggle the flag variable to false and the loop is finished.

That’ll be my first approach to getting through the pagination.

I switched to Python instead. Actually, I just want to write Javascript to save to file.

import requests
import json

def get_pretty_print(json_object):
    return json.dumps(json_object, sort_keys=True, indent=4, separators=(',', ': '))

apiKey = 'XXXXX'
url = 'https://www.wanikani.com/api/v2/'
endpoint = 'review_statistics'
parameters = ''
HEADERS = { 'Authorization' : 'Bearer {}'.format(apiKey) }

with requests.Session() as s:
    s.headers.update(HEADERS)
    r = s.get(url+endpoint+parameters)

fout = open('stat.tsv', "w")

j = 0
while True:
	j = j+1
	for i in range(0, len(r.json()['data'])):
		print 'Printing line ', i, j
		fout.write(str(r.json()['data'][i]['data']['subject_id']))
		fout.write('\t')
		fout.write(json.dumps(r.json()['data'][i]['data']))
		fout.write('\n')

	full_url = r.json()['pages']['next_url']
	print 'Opening', full_url
	if full_url == None:
		break
	r = s.get(full_url)

fout.close()

The response code when the rate limit is triggered has been update to 429. It was previously a 403.

2 Likes

FYI, the /subjects endpoint’s type filter doesn’t seem to accept multiple types. I wasn’t sure if that was intentional.

https://www.wanikani.com/api/v2/subjects?type=radical,kanji&levels=1
=> 422 Unprocessable Entity

https://www.wanikani.com/api/v2/subjects?type=radical&levels=1
=> 200 OK

https://www.wanikani.com/api/v2/subjects?type=kanji&levels=1
=> 200 OK

Thats how it was specced out and built. If the filter is not pluralize then its a safe assumption it can only take one entry.

I am assuming there are use cases for multiple types?

Ahh, I’m sure I read that, but apparently forgot.

In my Burn Manager script, when the user is selecting what items they want to select from for resurrection or retirement, they can select by various criteria such as level and type. Under the old API, I’m just sending multiple requests, so it’s no problem to continue doing so. I’m content with APIv2 as-is.

Gotcha. We had a quick discussion about this and we see nothing wrong with expanding out type to be types. We’ll refactor and have it accept multiple types. Will let you know when it is live.

We’ll also cascade this change to endpoints which take in subject_type

2 Likes

So just a heads up, next round of updates coming out soonish will have subject_type filter on Assignment, Review Statistics, and Study Material endpoints updated to subject_types. And the Subject endpoints type filter will be updated to types. This will be a breaking change.