Example file

call-the-api-from-the-browser.html

Your own JavaScript listing your products with a token from your token endpoint.

To try this page, download it, replace the placeholders and put it on a test page of your website. Opening the file from your computer won't work, because the embeds check the website address they're on.

call-the-api-from-the-browser.html · HTML Download
<!doctype html>
<!--
  Using an access token in your own JavaScript. This page gets a token from
  your server and lists your organisation's products with it.

  The page asks your token endpoint (token-server.mjs, next to this page, is a
  small working example) for { access_token, expires_in }, keeps the token
  until shortly before it runs out, and sends it to the Subcontext API in an
  Authorization header. If Subcontext answers 401, it gets a new token and
  tries once more.

  Replace YOUR_TOKEN_ENDPOINT with your own address (the comment in the script
  says what goes there). The page must be on one of the API application's
  allowed origins.
-->
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Call the API from the browser</title>
  <style>
    body { font: 16px/1.55 system-ui, sans-serif; max-width: 760px; margin: 2rem auto; padding: 0 16px; }
    code { font-size: 14px; }
    .error { color: #b42318; }
    li { margin: .25rem 0; }
  </style>
</head>
<body>
  <h1>Your products</h1>
  <p>Read live from the Subcontext API with a token from your own server.</p>
  <p><button id="load" type="button">Load the products</button></p>
  <p id="status" role="status"></p>
  <ul id="products"></ul>

  <script>
    // Your token endpoint, a route on your own server that returns { access_token, expires_in }.
    // On the same website as this page, a relative address like '/subcontext-token' works.
    // Running token-server.mjs on your computer, it's 'http://localhost:8081/subcontext-token'.
    const TOKEN_ENDPOINT = 'https://YOUR_TOKEN_ENDPOINT.example.com/subcontext-token'

    // The Subcontext address.
    const SUBCONTEXT = 'https://subcontext.com'

    let token = null // { value, expiresAt (milliseconds) }

    // A token that is good for at least another 30 seconds.
    async function getToken() {
      if (token && Date.now() < token.expiresAt - 30000) return token.value
      const response = await fetch(TOKEN_ENDPOINT, { cache: 'no-store' })
      if (!response.ok) throw new Error('Could not get a token from your server (' + response.status + ')')
      const body = await response.json()
      token = { value: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 }
      return token.value
    }

    // Calls the Subcontext API with the token. On a 401 (the token ran out or
    // was withdrawn), gets a new token and tries once more.
    async function callApi(path, retried) {
      const response = await fetch(SUBCONTEXT + path, {
        headers: { Authorization: 'Bearer ' + (await getToken()), Accept: 'application/json' },
        credentials: 'omit'
      })
      if (response.status === 401 && !retried) {
        token = null
        return callApi(path, true)
      }
      const body = await response.json().catch(() => null)
      if (!response.ok) {
        // Errors look like { "error": { "code": "forbidden", "message": "..." } }.
        const message = body && body.error ? body.error.message : 'no details'
        throw new Error('Subcontext answered ' + response.status + ': ' + message)
      }
      return body
    }

    document.getElementById('load').addEventListener('click', async () => {
      const status = document.getElementById('status')
      const list = document.getElementById('products')
      status.className = ''
      status.textContent = 'Loading...'
      list.replaceChildren()
      try {
        // GET /api/v1/products lists your organisation's products. Needs the read scope.
        const result = await callApi('/api/v1/products')
        status.textContent = result.data.length + ' product(s).'
        for (const product of result.data) {
          const item = document.createElement('li')
          const slug = document.createElement('code')
          slug.textContent = product.slug
          item.append(product.name + ' (' + product.lifecycle_state + '): ', slug)
          list.append(item)
        }
      } catch (error) {
        status.className = 'error'
        status.textContent = error.message
      }
    })
  </script>
</body>
</html>

Stuck, or something missing?

Get in touch and a person on the team will help.

Contact us