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.
<!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>