Quote form examples

Complete example pages to copy, from the paste-in snippet to a quote form backed by your own token server.

Example pages are complete web pages that show the quote form in use. Each card below is one page. Choose View and copy to read and copy its code, or Download to save the file. Then put it on a test page of your website.

  1. Make a quote form in the dashboard (see Quick start).
  2. Copy the page and replace the placeholders. A comment above each one says what goes there:
    • qfk_YOUR_KEY: the data-key in "The code to paste" on the quote form's page.
    • YOUR_PRODUCT_SLUG: the data-product in the same code.
    • YOUR_JOURNEY_SLUG: the data-journey in the same code.
    • YOUR_PRESENTATION_SLUG: optional. Another presentation of the journey. Leave it out for the default.
  3. Open it on one of the quote form's websites. The page must be on a website address listed in the quote form's Websites that may use it, such as a test page of your website. Opening the file from your computer won't work.

The own-server example#

06-own-token-server.html gets its tokens from your own server instead of a key. token-server.mjs is a small token server for it to use:

token-server.mjs ViewDownload
// A tiny token endpoint for your web pages. It swaps a browser API
// application's client id and secret for a short-lived access token (an OAuth
// "client credentials" request) and gives the page only
// { access_token, expires_in }. The secret never reaches the browser.
//
// Used by 06-own-token-server.html. The same as the access tokens guide's
// token-server.mjs (https://subcontext.com/developers/examples/oauth/token-server.mjs).
//
//   SUBCONTEXT_CLIENT_ID=... SUBCONTEXT_CLIENT_SECRET=... node token-server.mjs
//
// It answers GET (or POST) http://localhost:8081/subcontext-token.
//
// SUBCONTEXT_CLIENT_ID and SUBCONTEXT_CLIENT_SECRET: from the Subcontext
//   dashboard, Integrations, API applications. Use an application with your
//   website under "Allowed origins" and only the read and write scopes ticked.
//   The client secret is shown only once, when you create the application.
// ALLOWED_ORIGIN (optional): the website whose pages may ask for a token,
//   like https://www.example.com. Default http://localhost:4100 (the examples
//   on your computer). It must also be in the API application's allowed origins.
// PORT (optional): default 8081.
// SUBCONTEXT_BASE_URL (optional): the Subcontext address. Default
//   https://subcontext.com.
//
// Needs Node 18 or later. Uses no packages.
//
// On a real website this is a route on your own server, on the same website as
// the page, so the page can call '/subcontext-token' and the cross-origin
// (CORS) headers below aren't needed.
import { createServer } from 'node:http'

const baseUrl = (process.env.SUBCONTEXT_BASE_URL || 'https://subcontext.com').replace(/\/+$/, '')
const clientId = process.env.SUBCONTEXT_CLIENT_ID
const clientSecret = process.env.SUBCONTEXT_CLIENT_SECRET
const allowedOrigin = process.env.ALLOWED_ORIGIN || 'http://localhost:4100'
const port = Number(process.env.PORT || 8081)

// Get a new token when the cached one has less than this left, so a page
// always receives a token with at least a few minutes to run.
const RENEW_EARLY_SECONDS = 300

if (!clientId || !clientSecret) {
  console.error('Set SUBCONTEXT_CLIENT_ID and SUBCONTEXT_CLIENT_SECRET (see the comment at the top of this file).')
  process.exit(1)
}

let cached = null // { accessToken, expiresAt (milliseconds) }
let pending = null // a token request already on its way, shared by everyone waiting

// One token is shared by every visitor until it nearly runs out. Asking
// Subcontext for a new token per visitor would soon hit the rate limit.
async function currentToken() {
  if (!cached || Date.now() > cached.expiresAt - RENEW_EARLY_SECONDS * 1000) {
    pending ||= requestToken().finally(() => { pending = null })
    cached = await pending
  }
  return {
    access_token: cached.accessToken,
    // What is left of it, in seconds, not the full two hours.
    expires_in: Math.floor((cached.expiresAt - Date.now()) / 1000)
  }
}

async function requestToken() {
  const response = await fetch(baseUrl + '/oauth/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: clientId,
      client_secret: clientSecret,
      // What the quote form needs. Never give a browser token the crm scope.
      scope: 'read write',
      // What the token is for: the Subcontext API. Required.
      resource: baseUrl + '/api/v1'
    })
  })
  const body = await response.json().catch(() => ({}))
  if (!response.ok) {
    throw new Error('token request failed (' + response.status + '): ' + (body.error_description || body.error || 'no details'))
  }
  return { accessToken: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 }
}

createServer(async (request, response) => {
  // Only your website's pages may read the answer.
  response.setHeader('Access-Control-Allow-Origin', allowedOrigin)
  response.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
  response.setHeader('Vary', 'Origin')
  if (request.method === 'OPTIONS') return response.writeHead(204).end()

  const path = new URL(request.url, 'http://localhost').pathname
  if (path !== '/subcontext-token' || !['GET', 'POST'].includes(request.method)) {
    return response.writeHead(404).end()
  }

  // A page from another website gets nothing. (This only stops browsers.
  // Anyone can still call this endpoint from a script, so a browser token
  // must only be able to do what a visitor may do anyway.)
  const origin = request.headers.origin
  if (origin && origin !== allowedOrigin) {
    return response.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ error: 'origin not allowed' }))
  }

  try {
    const token = await currentToken()
    response.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' })
    response.end(JSON.stringify(token))
  } catch (error) {
    console.error(error.message)
    response.writeHead(502, { 'Content-Type': 'application/json' })
    response.end(JSON.stringify({ error: 'could not get a token' }))
  }
}).listen(port, () => console.log('Token server on http://localhost:' + port + '/subcontext-token for pages on ' + allowedOrigin))
  1. Create an API application (Integrations, API applications) with the read and write scopes and your website (or http://localhost:4100) under Allowed origins. Use the same organisation as the product you quote.
  2. Start the example token server with its Client ID and Client secret (Node 18 or later):
sh
   SUBCONTEXT_CLIENT_ID=... SUBCONTEXT_CLIENT_SECRET=... node token-server.mjs

It answers on http://localhost:8081/subcontext-token. Optional settings: PORT (default 8081), ALLOWED_ORIGIN (the page's origin, default http://localhost:4100) and SUBCONTEXT_BASE_URL (default https://subcontext.com). 3. In 06-own-token-server.html, replace https://YOUR-SERVER.example.com/subcontext-token with your token endpoint (http://localhost:8081/subcontext-token for the example server), set the product and journey, and open the page.

On a real website, the token endpoint is a route on your own server, on the same website as the page, so the page can call it with a relative address such as /subcontext-token.

Stuck, or something missing?

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

Contact us