Sooner or later a HubSpot page needs data from an API that requires a secret: a private app token, a weather service key, an internal pricing endpoint. Putting that key in front-end JavaScript means anyone can read it from the browser's network tab. HubSpot CMS serverless functions solve this by running a small Node.js function on HubSpot's side, so the secret never reaches the visitor.
Availability note: the official serverless functions pages we reviewed do not state which HubSpot subscription is required. Before you design around them, check that they are available in your account and read the current developer documentation, as plans and limits change.
Which model this article uses: the example below is a design-manager function. It receives (context, sendResponse) and answers by calling sendResponse. Functions built inside a HubSpot project use a different shape: an async function that receives context and returns an object such as { statusCode: 200, body: data }. Do not mix the two styles.
When a serverless function is the right tool
- You must call an API with a secret, and the call can be triggered from a public page.
- You want to reshape or filter a third-party response before the browser sees it.
- You need a small backend endpoint without running your own server.
It is not the right tool for long-running jobs or heavy processing. HubSpot documents these limits:
- 10 seconds of execution time per function, and 600 total execution seconds per minute per account
- No more than 100 endpoints per account
- No more than 50 secrets per account
- 128 MB of memory and a 6 MB invocation payload
- 4 KB of data per function log
Project layout
Functions live in a folder ending in .functions inside your theme, next to a serverless.json configuration file:
my-theme/
quote-proxy.functions/
serverless.json
get-quote.js
A typical serverless.json:
{
"runtime": "nodejs20.x",
"version": "1.0",
"environment": {},
"secrets": ["QUOTE_API_KEY"],
"endpoints": {
"quote": {
"method": "GET",
"file": "get-quote.js"
}
}
}
HubSpot's reference lists nodejs20.x as the current runtime, so use that and re-check the reference when you start a new function. The secrets array lists the names of secrets your function is allowed to read. The key in endpoints becomes part of the public URL.
Storing the secret
Add the secret with the HubSpot CLI instead of writing it into any file:
hs secrets add QUOTE_API_KEY
The CLI prompts for the value. Never commit it to Git. The name must also appear in the secrets array of serverless.json; inside the function the value is then available as process.env.QUOTE_API_KEY. Never return a secret in a response or write it to the logs, because that exposes it to the front end.
Writing the function
const axios = require('axios');
exports.main = async (context, sendResponse) => {
const city = context.params.city;
if (!city) {
sendResponse({ statusCode: 400, body: { error: 'city is required' } });
return;
}
try {
const res = await axios.get('https://api.example.com/quote', {
params: { city },
headers: { Authorization: `Bearer ${process.env.QUOTE_API_KEY}` }
});
sendResponse({ statusCode: 200, body: { quote: res.data.quote } });
} catch (err) {
sendResponse({ statusCode: 502, body: { error: 'Upstream request failed' } });
}
};
Notes on this example:
context.paramsholds query string values and HTML form-POSTed values. A JSONPOST(content typeapplication/json) arrives incontext.bodyinstead. Always validate both, because anyone can call a public endpoint.- Return only the fields the page needs. Do not forward the whole upstream response.
- Return a generic error message. Do not leak upstream details or the secret.
- Dependencies:
axios(and@hubspot/api-client) are preloaded, so no install is needed for this example. Design-manager functions do not support external dependencies on their own, so if you need other packages, bundle your modules with webpack so the bundled file is your function file. HubSpot documents apackage.jsonfor functions built with projects only, where it configures the dependencies the function needs.
Calling it from the page
Once deployed, the endpoint is served from your own connected domain under the /_hcms/api/ path:
fetch('/_hcms/api/quote?city=Manila')
.then(r => r.json())
.then(data => {
document.querySelector('#quote').textContent = data.quote;
});
Because it is same-origin, you avoid CORS headaches for pages on that domain.
Deploying and testing (CLI workflow)
- Authenticate the CLI with
hs auth. - Add the secret with
hs secrets add QUOTE_API_KEY. - Upload the theme with
hs cms upload(or runhs cms watchto upload on every change). - Call the endpoint directly in the browser or with curl, then from the page.
- When something fails, read the function logs with
hs logs. Remember that logs are limited to 4 KB per function log.
Secrets are cached, so a changed value can take about a minute to reach your function.
Security checklist
- Treat every endpoint as public. Validate and sanitize all input.
- Do not build a generic proxy that forwards any URL or any HubSpot API call a visitor asks for. Expose narrow, purpose-specific endpoints.
- If the function writes data (for example creating contacts), add a basic abuse control such as a honeypot field or a CAPTCHA verification before calling the API.
- Use the minimum scopes on any private app token you store.
- Cache where you can, so public traffic does not burn through the upstream API's rate limits.
Summary
Serverless functions let a HubSpot page use secret-protected APIs safely: store the key with hs secrets add, list its name in serverless.json, read it from process.env on the server side, validate input, and return only what the page needs. Start with one small endpoint and confirm availability and current limits in HubSpot's documentation before relying on it in production.
Sources
- HubSpot Developers: Serverless functions reference (secrets caching, /_hcms/api/ path)
- HubSpot Developers: Serverless functions (package.json for project-based functions, hs logs)
- HubSpot Developers: Project-based serverless functions
- HubSpot Developers: HubSpot CLI commands reference (hs cms upload, hs cms watch)
