The Third Bear

Just Right.

Custom public API endpoints for ActionKit data

egj actionkit

(12/20/2018) UPDATE: ActionKit now lets you enable CORS for specific hostnames.  This can be used to eliminate the JSONP wrapper described below, if you know in advance what websites should be allowed to access your API.

You can use heavily customized ActionKit templatesets to define your own JSON+JSONP API endpoints for ActionKit data.  These endpoints will be read-only and should be carefully limited to only provide data that you're comfortable exposing fully publicly, or at most to recognized users or logged-in (e.g. event host) members.  They'll also be limited to what you can access directly in an ActionKit template context.  But by building your own API endpoints you can greatly expand the scope of what your ActionKit pages can do, and in conjunction with the standard public endpoints for creating actions, you can even build completely custom serverless user interfaces and applications, both hosted in ActionKit and offsite.

The core of this is very simple: just create a new templateset (let's call it `json_v1`) and completely obliterate its `wrapper.html` code.  All page types extend the wrapper.html template first, so by wiping out that template's contents, you don't need to worry about the contents of all the other templates in your set at all; they're all in {% block %}s that will never get activated.

Now ... start publishing some JSON data!  Let's say we want to publish the basic core data about any generic page -- its title, name, type and ID.  Just write out some JSON by hand in wrapper.html:

{% load actionkit_tags %}
{
  "name": {{ page.name|json }},
  "title": {{ page.title|json }},
  "type": {{ page.type|json }},
  "id": {{ page.id }}
}

(Note the |json filter applied to most of the inputs there -- that ensures that JSON-breaking characters in strings -- like quotation marks -- will be escaped, by passing the strings through Python's json.dumps function.)

Now when you visit any page with a ?template_set=json_v1 suffixed to its URL, you'll get this machine-readable output instead of the rendered page.  This can now be fetched and parsed from any client code like jQuery.getJSON, etc.

Accessing data from offsite

The above will let you access your page data from within ActionKit pages themselves, but if you try to fetch that data in a browser from any origin other than your ActionKit domain, you won't be successful -- the browser's cross-origin security policy will kick in to block the request, since ActionKit doesn't have any built-in support for CORS.

Fortunately, there's an easy and standard workaround: just implement your own JSONP wrapper:

{% load actionkit_tags %}
{% if args.jsonp %}
{{ args.jsonp }}(
{% endif %}
{
  "name": {{ page.name|json }},
  "title": {{ page.title|json }},
  "type": {{ page.type|json }},
  "id": {{ page.id }}
}
{% if args.jsonp %}
);
{% endif %}

Now you can access your pages from offsite by suffixing ?template_set=json_v1&jsonp=myJavascriptCallbackFunction to the URL; or, using jQuery, just include a "jsonp=?" in the URL when calling $.ajax / $.getJSON, and it will handle the details of making your callback function run.

Managing multiple endpoints

You'll likely end up wanting to set up multiple endpoints -- one for accessing basic page data, perhaps another for accessing data about an action after it was submitted, maybe another that's specifically for survey questions, etc.  The cleanest way to do this is with multiple custom templates in a single json_v1 templateset; just have the wrapper.html contain your jsonp wrapper and then key off a query string parameter to decide what subtemplate to include:

{% if args.jsonp %}
{{ args.jsonp }}(
{% endif %}

{% if args.endpoint == "form_data" %}
{% include "./json_form_data.html" %}
{% elif args.endpoint == "action_data" %}
{% include "./json_action_data.html" %}
{% else %}
{"status": 404}
{% endif %}

{% if args.jsonp %}
);
{% endif %}
Securing some pages

You may want to restrict API access to only a certain set of pages, either overall or on an endpoint-by-endpoint basis.  A custom page field is an easy way to set up either a whitelist or a blacklist:

{% if args.jsonp %}
{{ args.jsonp }}(
{% endif %}

{% if page.custom_fields.api_disabled %}
  {"status": 400}
{% else %}
  {% if args.endpoint == "form_data" %}
  {% include "./json_form_data.html" %}
  {% elif args.endpoint == "action_data" %}
  {% include "./json_action_data.html" %}
  {% else %}
  {"status": 404}
  {% endif %}
{% endif %}

{% if args.jsonp %}
);
{% endif %}
Restricting access to recognized users

Lastly, you may want to make API data available only to recognized users, or in the case of after-action data, only to the recognized user who matches the action's user.  These are also easy to set up; a `user` variable will be available in the context if there is a recognized user thanks to a valid ?akid parameter, and an `action` variable will be available if the query string contains both a valid ?action_id and a valid ?akid:

{% if args.jsonp %}
{{ args.jsonp }}(
{% endif %}

{% if page.custom_fields.api_disabled %}
  {"status": 400}

{% elif not user %}
  {"status": 401}
{% elif args.action_id and not action or user != action.user %}
  {"status": 403}

{% else %}
  {% if args.endpoint == "form_data" %}
  {% include "./json_form_data.html" %}
  {% elif args.endpoint == "action_data" %}
  {% include "./json_action_data.html" %}
  {% else %} 
  {"status": 404}
  {% endif %}
{% endif %}

{% if args.jsonp %}
);
{% endif %}

In a series of followup posts I'll give some practical examples of API endpoints you might implement, and what you can do with them.