Akindi’s API documentation

Akindi’s API provides an <iframe> that lets your users download bubble sheets and upload scanned responses, then sends the parsed and normalized responses back to your application.

+-----------------------------------------------------------------------+
| < Your application >                                                  |
|                                                                       |
| Fall 2016 Math Midterm                                                |
|                                                                       |
| +-------------------------------------------------------------------+ |
| | < Akindi iframe >                                                 | |
| |                                                                   | |
| | [ Print Bubblesheets ] [ Upload PDFs ]                            | |
| | 5/12 students, 1 exception                                        | |
| |                                                                   | |
| |              | Q1 | Q2 | Q3 | Q4 | ...                            | |
| | Jane Doe     |  A |  B |  A |  C | ...                            | |
| | Alex Smith   |  A |  B |  A |  C | ...                            | |
| | ...                                                               | |
| |                                                                   | |
| +-------------------------------------------------------------------+ |
+-----------------------------------------------------------------------+

Integration Overview

1. Receive your Akindi API credentials

You’ll be given a subdomain and two API key pairs: one for testing, one for production:

  • Subdomain: https://yourcompany.akindi.com
  • Production public / secret key: pk_abc123 / sk_xyz789
  • Testing public / secret key: test_pk_aaaa / test_sk_bbbb

As implied by the names, the secret keys should be kept secret.

2. Embed the Akindi iframe in your application

For example, your application likely has a view which shows the details of an assessment. This might be a reasonable place to embed the assessment iframe which lets the user upload bubble sheets:

<iframe
  style="width: 800px; height: 800px; border: none"
  src="https://yourcompany.akindi.com/api/v1/assessments/{{ assessment.id }}/iframe?
        url={{ assessment.get_akindi_data_url }}">
</iframe>

See iframe Options for complete iframe-related documentation.

3. Implement an Assessment endpoint

Your application needs to implement an endpoint which Akindi can call on to receive assessment details:

GET /your-api/akindi/assessment?id=a1234 HTTP/1.1
Host: yourcompany.com
User-Agent: Akindi API
X-Ak-Expires: 1397614508
X-Ak-Key: pk_abc123
X-Ak-Signature: ...
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{
    "id": "a1234",
    "url": "https://yourcompany.com/your-api/akindi/assessment?id=a1234",
    "name": "Fall 2016 Midterm",
    "course_name": "1st Period Math",
    "responses_url": "https://yourcompany.com/your-api/akindi/responses?assessment-id=a1234",
    "roster_id": "r4567",
    "roster_url": "https://yourcompany.com/your-api/akindi/roster?id=r4567",
}

See also: Assessments.

4. Implement a Student Roster endpoint

We recommend that you implement an endpoint which Akindi can call on to receive the student roster for assessments. Akindi uses the roster to create pre-filled bubble sheets, and help the end user identify and correct sheets with missing or incorrect student numbers.

GET /your-api/akindi/roster?id=r4567 HTTP/1.1
Host: yourcompany.com
User-Agent: Akindi API
X-Ak-Expires: 1397614508
X-Ak-Key: pk_abc123
X-Ak-Signature: ...
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8

{
    "id": "r4567",
    "url": "https://yourcompany.com/your-api/akindi/akindi/roster?id=r4567",
    "students": [
        { "id": "s7000", "student_num": "0001", "first_name": "Jane", "last_name": "Doe" },
        { "id": "s7131", "student_num": "0002", "first_name": "Alex", "last_name": "Smith" },
        { "id": "s7393", "student_num": "0003", "first_name": "Ming", "last_name": "Le" },
        { "id": "s7473", "student_num": "0004", "first_name": "Jose", "last_name": "Silva" },
    ],
}

See also: Student Roster.

5. Implement a Responses endpoint

Finally, your application needs to implement an endpoint which will receive student responses. Akindi will POST all of the assessment responses to this endpoint each time new responses become available or the end user edits existing responses.

POST /your-api/akindi/responses?assessment-id=a1234 HTTP/1.1
Host: yourcompany.com
User-Agent: Akindi API
Content-Type: application/json; charset=UTF-8
Content-Encoding: gzip
X-Ak-Expires: 1397614508
X-Ak-Key: pk_abc123
X-Ak-Signature: ...

{
    "assessment_id": "a1234",
    "responses": [
        {
            "ak_id": "r9853",
            "student_id": "s7000",
            "fields": {
                "date": "012115",
                "questions": ["A", "AB", "C", "B", "C", "", ""],
            },
            "regions": [
                { "name": "handwritten-name", "page_idx": 0, "url": "https://.../handwritten-name.png" },
                { "name": "signature", "page_idx": 1, "url": "https://.../signature.png" },
            ],
            "pages": [
                { "id": "p9041", "idx": 0, "num": 11, "url": "https://.../page.png" },
                { "id": "p9763", "idx": 1, "num": 12, "url": "https://.../page.png" },
            ],
        },
        {
            "ak_id": "r9853",
            "student_id": "s7000",
            "fields": {
                "date": "012115",
                "questions": ["A", "AB", "C", "B", "C", "", ""],
            },
            "regions": [
                { "name": "handwritten-name", "page_idx": 0, "url": "https://.../handwritten-name.png" },
                { "name": "signature", "page_idx": 1, "url": "https://.../signature.png" },
            ],
            "pages": [
                { "ak_id": "p9103", "idx": 0, "num": 19, "url": "https://.../page.png" },
                { "ak_id": "p9510", "idx": 1, "num": 18, "url": "https://.../page.png" },
            ],
        },
    ],
}

See also: Student Responses.

Endpoints

Assessments

Your application doesn’t need to explicitly define assessments. Rather, they are created when they are first accessed, with Akindi fetching their definition from your application.

Your application defines assessments with:

  • id: The ID your application will use to refer to this assessment. It can be any ASCII string between 1 and 32 characters, and need only be unique to each API key (ie, testing and production keys can share IDs).
  • url: The assessment’s canonical URL, which Akindi will fetch each time the assessment needs to be refreshed.
  • name: The assessment’s name which will be displayed to the end user. Up to 32 Unicode characters.
  • course_name: The name of the “course” in which this assessment resides. This will be show to the user and printed on bubble sheets. Up to 32 Unicode characters.
  • responses_url: The URL Akindi will use to POST back assessment results. See also: Student Responses.
  • roster_id (optional): The ID of the assessment’s student roster. See also: Student Roster.
  • roster_url (optional): The URL which Akindi can use to fetch this assessment’s student roster. See also: Student Roster.

For example:

{
    "id": "a1234",
    "url": "https://yourcompany.com/your-api/akindi/assessment?id=a1234"
    "name": "Fall 2016 Midterm",
    "course_name": "1st Period Math",
    "responses_url": "https://yourcompany.com/your-api/akindi/responses?assessment-id=a1234",
    "roster_id": "r4567",
    "roster_url": "https://yourcompany.com/your-api/akindi/roster?id=r4567",
}
GET /api/v1/assessments/(assessment-id)/iframe?url=(assessment-url)

Returns an HTML page suitable for embedding in an <iframe>.

This page will provide facilities for the user to download more bubble sheets, upload their results, resolve exceptions, and view and edit the existing results.

See also: iframe Options.

Parameters:
  • assessment-id – Your application’s ID for this assessment (between 1 and 32 ASCII characters, unique per API key).
  • assessment-url – A URL Akindi can use to fetch the assessment definition.
GET /api/v1/assessments/(assessment-id)/responses

Returns the responses for this assessment. See also: Student Responses.

Student Roster

Akindi can use an assessment’s student roster to:

  • Pre-fill the student’s name and student number on bubble sheets.
  • Automatically identify and correct sheets with incorrect student numbers (we call this “resolving exceptions”).
  • Associate your student IDs with each response it sends back.

The fields vary based on the bubble sheet template, but in general they are:

  • id: The ID your application will use to refer to this student. It may be any ASCII string between 1 and 32 characters.
  • student_num (optional): The number the student will bubble in on the “student number” field on the bubble sheet. Generally up to 10 digits.
  • first_name and last_name (optional): If provided, the student’s name will be shown during exception resolution and printed on pre-filled sheets.

For example:

{
    "id": "r4567",
    "url": "https://yourcompany.com/your-api/akindi/akindi/roster?id=r4567",
    "students": [
        { "id": "s7000", "student_num": "0001", "first_name": "Jane", "last_name": "Doe" },
        { "id": "s7131", "student_num": "0002", "first_name": "Alex", "last_name": "Smith" },
        { "id": "s7393", "student_num": "0003", "first_name": "Ming", "last_name": "Le" },
        { "id": "s7473", "student_num": "0004", "first_name": "Jose", "last_name": "Silva" },
    ],
}

We strongly recommend that you provide a student roster.

Student Responses

As they become available, Akindi will send student responses back to your application.

Each response contains:

  • ak_id: Akindi’s internal ID for this response (an ASCII string up to 32 characters long).

  • student_id: If Akindi was able to match the student’s number on the bubble sheet to a student number in the roster then your application’s student ID will be included in the response. See also: Student Roster.

  • fields: The fields from the bubble sheets. These will vary based on the specific template being used, but in general most templates have:

    • student_num: The student’s student number (note: does not necessarily match the student’s student_num).
    • questions: A list of the student’s responses to the multiple choice questions on the test. Values may be blank (student did not answer), a single letter, or multiple letters (if the student filled multiple bubbles).
  • regions: Specific regions of the page that might be important. These will vary based on the specific template being used, but in general moist templates have:

    • handwritten-name: the portion of the page where the student will hand-write their name.
  • pages: The list of pages used to create this response. Each page has:

    • ak_id: Akindi’s internal ID for this page.
    • idx: The index of this page in the response.
    • num: The number of this page in the PDF.
    • url: An image of the page (note: request for this image must be signed; see also: Signing GET Requests).
{
    "responses": [
        {
            "ak_id": "r9853",
            "student_id": "s7000",
            "fields": {
                "date": "012115",
                "student_num": "0001",
                "questions": ["A", "AB", "C", "B", "C", "", ""],
            },
            "regions": [
                { "name": "handwritten-name", "page_idx": 0, "url": "https://.../handwritten-name.png" },
                { "name": "signature", "page_idx": 1, "url": "https://.../signature.png" },
            ],
            "pages": [
                { "id": "p9041", "idx": 0, "num": 11, "url": "https://.../page.png" },
                { "id": "p9763", "idx": 1, "num": 12, "url": "https://.../page.png" },
            ],
        },
        {
            "ak_id": "r9853",
            "student_id": "s7473",
            "fields": {
                "date": "012  5",
                "student_num": "00--",
                "questions": ["A", "AB", "C", "B", "C", "", ""],
            },
            "regions": [
                { "name": "handwritten-name", "page_idx": 0, "url": "https://.../handwritten-name.png" },
                { "name": "signature", "page_idx": 1, "url": "https://.../signature.png" },
            ],
            "pages": [
                { "ak_id": "p9103", "idx": 0, "num": 19, "url": "https://.../page.png" },
                { "ak_id": "p9510", "idx": 1, "num": 18, "url": "https://.../page.png" },
            ],
        },
    ],
}

iframe Options

  • The different iframes available
  • Size options
  • Style options

iframe Authentication

All iframe URLs must be signed:

<iframe src="/api/v1/.../iframe?
    ...
    ak_key=pk_abc123&
    ak_expires=1397614508&
    ak_signature=W6etG99lMuDlT0OzCOooy4Wtscs%3D"></iframe>

See the “Signing GET Requests” section of Authentication for complete documentation.

Optional Parameters

In addition to the required authentication parameters, all iframe endpoints accept the following optional-but-recommended parameters:

GET /api/v1/*/iframe
Parameters:
  • user-id – (optional) a unique ID for the user currently logged in to the client’s application. Used by Akindi to troubleshoot end-user issues, and in troubleshooting communications between Akindi and the client.
  • user-email
  • user-first-name
  • user-last-name – (optional) the e-mail address and name of the user currently logged in to the client’s application. Used by Akindi exclusively to provide support to the end-user, if end-user support is included in the client’s support plan.

Authentication

All requests made into Akindi must be authenticated, and Akindi will authenticate all calls it makes back to your application.

Akindi uses a custom HMAC-based authentication scheme in which pertinent request variables are concatenated and HMAC’d to create a signature, and this signature is included along with the request. A request which includes a signature is called a “signed request”.

For example, an authenticated GET request for an iframe looks like:

GET /api/v1/.../iframe?
    ...
    ak_key=pk_abc123&
    ak_expires=1397614508&
    ak_signature=W6etG99lMuDlT0OzCOooy4Wtscs%3D

Where ak_key is your public key, ak_expires is a UTC timestamp after which the request will no longer be considered valid, and ak_signature is the signature, described below.

During development the key “user” can be used, and the currently logged in user’s identity will be used as the key and the ak_signature parameter will be ignored. See authentication during development.

Signature Algorithm

The signature is:

base64(hmac-sha1(api_secret_key, string_to_sign))

Where the string_to_sign is:

ak_expires + "\n" + http_method + "\n" + url_path_and_query [ + "\n" + post_data ]

And:

  • ak_expires is a UTC timestamp after which the request will no longer be considered valid. For example: 1397614508.
  • http_method is the request’s HTTP method. For example: GET or POST.
  • url_path_and_query is the request’s URL and query parameters, excluding signing parameters. Before signing, the URL should be UTF-8 encoded if it contains unicode and query parameters should be %-encoded. For example: /api/v1/assessments/a1234/iframe?url=https%3A%2F%2Fyourcompany.com%2Fyour-api%2Fakindi%2Fassessment%3Fid%3Da1234.
  • post_data is the body of the POST request. See details in Signing POST requests.

For example, in Python:

import hashlib
import hmac
import json

def ak_sign(secret_key, expires, http_method, url_path_and_query, post_data=None):
    string_to_sign = "%s\n%s\n%s" %(expires, http_method, url_path_and_query)
    if isinstance(string_to_sign, unicode):
        string_to_sign = string_to_sign.encode("utf-8")
    if post_data is not None:
        string_to_sign = "%s\n%s" %(string_to_sign, post_data)
    signature_hmac = hmac.new(secret_key, string_to_sign, hashlib.sha1)
    return signature_hmac.digest().encode("base64").strip()
>>> url_path_and_query = "/api/v1/assessments/a1234/iframe?url=https%3A%2F%2Fyourcompany.com%2Fyour-api%2Fakindi%2Fassessment%3Fid%3Da1234"
>>> ak_expires = "1397614508"
>>> ak_secret = "sk_xyz789"
>>> ak_signature = ak_sign(ak_secret, ak_expires, "GET", url_path_and_query)
>>> print ak_signature
vRc2I4bleg9BrQpxourWXvIr+Ng=

And note that the signature must be URL encoded if it’s being sent as a GET parameter:

>>> import urllib
>>> print urllib.quote(ak_signature, safe="")
vRc2I4bleg9BrQpxourWXvIr%2BNg%3D

Signing GET Requests

All GET requests should be signed by including three additional query parameters: ak_key, ak_expires, and ak_signature.

For example, in Python:

def ak_sign_get_request(public_key, secret_key, expires, url_path_and_query):
    signature = ak_sign(secret_key, expires, "GET", url_path_and_query)
    query_args = urllib.urlencode({
        "ak_key": public_key,
        "ak_expires": expires,
        "ak_signature": signature,
    })
    return "%s%s%s" %(
        url_path_and_query,
        "&" if "?" in url_path_and_query else "?",
        query_args,
    )
>>> url_path_and_query = "/api/v1/assessments/a1234/iframe?url=https%3A%2F%2Fyourcompany.com%2Fyour-api%2Fakindi%2Fassessment%3Fid%3Da1234"
>>> ak_expires = "1397614508"
>>> ak_secret = "sk_xyz789"
>>> ak_public = "pk_abc123"
>>> print ak_sign_get_request(ak_public, ak_secret, ak_expires, url_path_and_query)
/api/v1/assessments/a1234/iframe?
    url=https%3A%2F%2Fyourcompany.com%2Fyour-api%2Fakindi%2Fassessment%3Fid%3Da1234&
    ak_key=pk_abc123&
    ak_expires=1397614508&
    ak_signature=vRc2I4bleg9BrQpxourWXvIr%2BNg%3D

Note that all GET requests must be signed, including request for resources for resources when the URL has been returned by Akindi (such as, for example, page images).

Signing POST Requests

Akindi will sign all POST requests it makes into your application using same algorithm as GET requests, except the ak_key, ak_expires, and ak_signature fields are sent as the HTTP headers X-Ak-Key, X-Ak-Expires, and X-Ak-Signature, and the POST data are included in the signature.

Note: the body will be included in the signature even if the body is empty.

For example:

.. doctest::
>>> import json
>>> url_path_and_query = "/your-api/akindi/responses?assessment-id=a1234"
>>> ak_expires = "1397614508"
>>> ak_secret = "sk_xyz789"
>>> ak_public = "pk_abc123"
>>> body = json.dumps({"responses": "[]"})
>>> signature = ak_sign(ak_secret, ak_expires, "POST", url_path_and_query, body)
>>> headers = {
...     "X-Ak-Key": ak_public,
...     "X-Ak-Expires": ak_expires,
...     "X-Ak-Signature": signature,
... }
>>> print body
{"responses": "[]"}
>>> print headers
{'X-Ak-Expires': '1397614508', 'X-Ak-Key': 'pk_abc123', 'X-Ak-Signature': 'wVTAz2vWcP7yKoxxRPGUKhCTU1A='}

Authentication During Development

During development and interactive testing the special key “user” can be used to bypass signature checks. Akindi will create a new API key for the currently authenticated user and signature checks will not be performed. A suffix can also be added to the user key to test with multiple keys.

For example, if ak_key=user is used, Akindi will create and use the API key user_1234 (where 1234 is the ID of the currently authenticated user). If ak_key=user_key1 is used, Akindi will create and use the API key user_1234_key1 will be used.

Akindi will not check either ak_signature or ak_expires when a user key is used.

Attempting to use the user key when not logged in will return an HTTP unauthorized error.

To prevent cross-site request forgery Akindi will only allow the user key to be used when the HTTP referrer matches /(.*\.)?localhost/, or when Akindi is accessed directly (ie, there is no referrer).

When signing outbound requests, Akindi will use hex(sha1(ak_key) as the secret key. For example, if your server receives a request with ak_key=user_key1, the secret key will be hex(sha1('user_key1')). Only user API keys will have the "user" prefix, so you can safely use if ak_key.startswith("user") to detect the presence of a user key.

>>> import hashlib
>>> hashlib.sha1('user_key1').hexdigest()
'f85b0b5eff11c50c019607ee9cd5119997febb46'