Read content over the JSON:API

v0.0.1

Every site that is not set to Phoenix Only answers a JSON:API under /api/json. A request is for the site named by its Host header. This guide reads public posts, signs in to get a token, reads a site's pages with it, lets a browser frontend call the API and opens the API's own spec. Replace https://example.com in the examples with your site's address.

Before you begin

  • The site's Mode must be API Only or Hybrid. A Phoenix Only site refuses the API with a 403.
  • Admin routes check names that start with admin:api:. A name that starts with admin:dashboard:, wildcards included, does not count on the API. See How permissions work.
  • Send Accept: application/vnd.api+json on every request, and Content-Type: application/vnd.api+json on any request with a body.

Know the routes

Address under /api/jsonWho can call itWhat it holds
/v1/user/mishka-blog/post/publishedAnyonePublished posts. Add /:id for one post. The blog's tags, bloggers, teams and collections have routes beside it.
/v1/auth/user/sign-inAnyoneSign in. /me, /logout, /reset-request, /reset-password and /change-password sit beside it.
/v1/tenant/admin/runtime/pageAn account of the site with an admin:api: nameThe site's pages. Component, layout, error-page, js-hook, stylesheet, media, media-category and redirect work the same way.
/v1/master/admin/runtime/pageA masterEvery site's pages, on the master site's address.
/v1/tenant/admin/mishka-blog/postAn account of the site with an admin:api: nameAll of the site's posts, drafts included.

Read public posts without a token

bash
curl -g "https://example.com/api/json/v1/user/mishka-blog/post/published?page[limit]=5" \
  -H "Accept: application/vnd.api+json"

The -g flag stops curl from reading the brackets as a range. The answer lists up to five posts as JSON:API blog_post resources. A post locked to permissions is left out unless the token you send holds one of them.

Sign in and get a token

bash
curl -X POST "https://example.com/api/json/v1/auth/user/sign-in" \
  -H "Accept: application/vnd.api+json" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{"data": {"type": "user", "attributes": {"email": "[email protected]", "password": "your-password"}}}'

A 201 answer holds the account in data and the token in meta:

json
{
  "data": {"id": "<user id>", "type": "user", "attributes": {"email": "[email protected]"}},
  "meta": {"token": "<the token>"}
}

Send the token as Authorization: Bearer <the token> on later requests. Sign in on the address of the account's own site. An account with an authenticator app is answered 401 until it also sends totp_code, or recovery_code in its place, in attributes. The API never asks a captcha, but it counts attempts: 30 per five minutes from one address and 10 per 15 minutes for one account, by default. A refused attempt gets a 429 with a retry-after header.

A master signs in on the master site's address and adds -H "x-no-tenant: true". The master's token names no site, so send that header again on master routes. A route that works on one site answers a request without a site with tenant_required.

To check what a token holds, call GET /api/json/v1/auth/user/me?fields[user]=id,email,permission_values. To end it, POST to /api/json/v1/auth/user/logout with {"data": {"token": "<the token>"}}.

Read a site's pages

bash
curl -g "https://example.com/api/json/v1/tenant/admin/runtime/page?fields[page]=title,path" \
  -H "Accept: application/vnd.api+json" \
  -H "Authorization: Bearer $TOKEN"

Reading takes any name under admin:api. Writing takes the area's own name, admin:api:runtime:page for pages, or admin:api:code. The page routes show the pattern every runtime resource follows:

  • GET …/page lists, and GET …/page/:id reads one.
  • POST …/page creates, with a body of {"data": {"type": "page", "attributes": {…}}}. PATCH …/page/:id updates.
  • DELETE …/page/:id/archive archives, and PATCH …/page/:id/unarchive restores. GET …/page/archived lists what is archived.
  • GET …/page/:id/with-versions and GET …/page/:page_id/versions read the version history.

Call the API from a browser frontend

The API answers a browser origin only when it is the site's own host, the site's Frontend Domain, or in its Allowed Origins. The check compares hosts and ignores ports. An allowed origin gets its preflight answered with a 204 and these allowed headers: Accept, Authorization, Content-Type, X-Requested-With and x-no-tenant.

  1. Sign in as a master at /auth?master_user=true (see the documentation) and open Sites under Runtime.
  2. Click Edit on the site's row. A dialog titled with the site's name opens on its Configuration tab.
  3. Enter the frontend's host in Frontend Domain, or click Add Origin and enter an address such as https://app.example.com under Allowed Origins.
  4. Submit the form.

a site's dialog, titled with the site's name, on the Configuration tab with the Mode field set to Hybrid, a Frontend Domain and one address under Allowed Origins

A master sets the API's browser access on the site's form.

A site admin sees these fields read only.

Read the API spec

GET /api/json/open_api returns the OpenAPI document for every route, filter and field. /api/json/swaggerui is the interactive browser for it. Both answer a 403 unless the request carries a bearer token that holds a name under admin:api.

Fix common errors

StatusCodeCause
400missing_hostThe request has no Host header.
404domain_not_foundThe host is not a site's address.
403api_access_deniedThe site is Phoenix Only.
503website_unavailableThe site is switched off.
503site_offlineThe site is offline for maintenance. Only API admins and masters get through. Sign-in, me, logout and the password reset routes stay open.
403forbiddenThe token is missing or holds no admin:api: name for the route. A sign-in with a wrong password, or on another site's address, answers 403 too.
401second_factor_required, second_factor_invalidThe account has an authenticator app, and the sign-in sent no code or a wrong one.
400tenant_requiredA request without a site reached a route that works on one site.
429rate_limitedToo many sign-in attempts. Wait the seconds in retry-after.

Next steps