Backend Work Instructions
Neut Widget Backend Guide
This guide covers the customer backend endpoint used by the browser quickstart.
The backend endpoint creates a fresh iframe URL for each widget mount. It does not call Neut while building
the URL. The hosted iframe calls Neut POST /api/widgets/bootstrap after it loads.
Your page may call this same endpoint again later if the iframe posts a
neut-widget-refresh-request message after its widget session expires.
Endpoint Contract
Create this endpoint on your website backend:
POST /api/widget-embed-params
Request body:
{
"widget_id": "bill-preview",
"instance_id": "bill-preview-article-123-main",
"bill_type": "HR",
"bill_number": 1,
"congress": 119,
"colorway": "Default"
}
Success response:
{
"widget_id": "bill-preview",
"embed_url": "https://widgets.neut.us/bill-preview-embed.html?tenant_id=..."
}
Error response:
{
"error": {
"code": "INVALID_WIDGET_REQUEST",
"message": "Unsupported widget_id."
}
}
Signing Contract
Your backend signs this canonical value:
tenant_id.widget_id.origin.timestamp.nonce.bootstrap
Where:
tenant_idisWIDGET_TENANT_IDwidget_idis the validated widget IDoriginisHOST_ORIGINtimestampis a whole-second Unix timestamp stringnonceis a fresh random valuebootstrapis the literal purpose string
The signature is HMAC-SHA256 using NEUT_MINT_SECRET, encoded as unpadded base64url.
bootstrap_timestamp must stay in the same base-10 whole-second form that was signed. Do not
sign fractional seconds, exponent notation, leading plus signs, or alternate textual forms.
Complete Node/Express Example
This example validates the current public-data widgets, signs a bootstrap assertion, and returns the hosted iframe URL.
const crypto = require("node:crypto");
const express = require("express");
const app = express();
app.use(express.json());
const WIDGET_PATHS = {
"bill-preview": "/bill-preview-embed.html",
"vote-breakdown": "/vote-breakdown-embed.html",
"legislation-by-subject": "/legislation-by-subject-embed.html",
};
const SUBJECTS = new Set([
"Animals & Agriculture",
"Business & Trade",
"Crime",
"Culture and Society",
"Education",
"Energy & Conservation",
"Finance & Taxes",
"Geopolitics",
"Government Operations",
"Health & Social Welfare",
"Human, Civil, & Immigration Rights",
"Infrastructure & Housing",
"Labor & Employment",
"National Security & International Affairs",
"STEM",
]);
const STATUS_FILTERS = new Set([
"All",
"Introduced",
"With First Chamber",
"With Second Chamber",
"Presented to President",
"Became Law",
"Vetoed by President",
"Passed Resolutions",
]);
const COLORWAYS = new Set([
"Default",
"Dark",
"Paper",
"WashedSage",
"Slate",
"MonoLight",
]);
function requiredEnv(name) {
const value = process.env[name];
if (!value) throw new Error(`Missing server config: ${name}`);
return value;
}
function requireText(input, key) {
const value = input[key];
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${key} is required.`);
}
return value.trim();
}
function optionalText(input, key) {
const value = input[key];
if (value === undefined || value === null || value === "") return undefined;
if (typeof value !== "string" || !value.trim()) {
throw new Error(`${key} must be a string.`);
}
return value.trim();
}
function requiredPositiveInteger(input, key) {
const raw = input[key];
if (raw === undefined || raw === null || raw === "") {
throw new Error(`${key} is required.`);
}
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`${key} must be a positive integer.`);
}
return value;
}
function optionalPositiveInteger(input, key, defaultValue) {
const raw = input[key] ?? defaultValue;
if (raw === undefined || raw === null || raw === "") return undefined;
const value = Number(raw);
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`${key} must be a positive integer.`);
}
return value;
}
function optionalBoolean(input, key, defaultValue) {
const raw = input[key];
if (raw === undefined || raw === null || raw === "") return defaultValue;
if (raw === true || raw === "true") return true;
if (raw === false || raw === "false") return false;
throw new Error(`${key} must be true or false.`);
}
function validateCommon(input) {
const widgetId = requireText(input, "widget_id");
if (!WIDGET_PATHS[widgetId]) {
throw new Error("Unsupported widget_id.");
}
const instanceId = requireText(input, "instance_id");
if (instanceId.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(instanceId)) {
throw new Error(
"instance_id must use only letters, numbers, dot, underscore, colon, or hyphen and be 128 characters or fewer."
);
}
const colorway = optionalText(input, "colorway") || "Default";
if (!COLORWAYS.has(colorway)) {
throw new Error("Unsupported colorway.");
}
return {
widget_id: widgetId,
instance_id: instanceId,
article_id: optionalText(input, "article_id"),
colorway,
};
}
function validatePagedLedgerFields(input) {
return {
congress: requiredPositiveInteger(input, "congress"),
page: optionalPositiveInteger(input, "page", 1),
limit: optionalPositiveInteger(input, "limit", 10),
};
}
function validateOptionalMemberFields(input) {
const memberType = optionalText(input, "member_type");
const lastName = optionalText(input, "last_name");
const stateCodeRaw = optionalText(input, "state_code");
const district = optionalPositiveInteger(input, "district");
const hasMemberInput = Boolean(memberType || lastName || stateCodeRaw || district);
if (!hasMemberInput) {
return {};
}
// This helper is shared across member-targeted widgets.
// vote-breakdown applies an extra House-only Representative check later.
if (!memberType || !lastName || !stateCodeRaw) {
throw new Error(
"member_type, last_name, and state_code are required when targeting a single member."
);
}
if (memberType !== "Representative" && memberType !== "Senator") {
throw new Error('member_type must be "Representative" or "Senator".');
}
const stateCode = stateCodeRaw.toUpperCase();
if (!/^[A-Z]{2}$/.test(stateCode)) {
throw new Error("state_code must be a two-letter state code.");
}
if (district && memberType !== "Representative") {
throw new Error('district is only allowed when member_type is "Representative".');
}
return {
member_type: memberType,
last_name: lastName,
state_code: stateCode,
district,
};
}
function validateWidgetRequest(input) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new Error("Request body must be a JSON object.");
}
const common = validateCommon(input);
if (common.widget_id === "bill-preview") {
const tempTextUrl = optionalText(input, "temp_text_url");
if (tempTextUrl) {
const parsed = new URL(tempTextUrl);
if (parsed.protocol !== "https:") {
throw new Error("temp_text_url must be an HTTPS URL.");
}
}
return {
...common,
bill_type: requireText(input, "bill_type"),
bill_number: requiredPositiveInteger(input, "bill_number"),
congress: requiredPositiveInteger(input, "congress"),
temp_text_url: tempTextUrl,
temp_title: optionalText(input, "temp_title"),
};
}
if (common.widget_id === "vote-breakdown") {
const memberFilters = validateOptionalMemberFields(input);
if (
memberFilters.member_type &&
memberFilters.member_type !== "Representative"
) {
throw new Error(
'vote-breakdown currently supports House roll call votes only; member_type must be "Representative".'
);
}
return {
...common,
...validatePagedLedgerFields(input),
...memberFilters,
};
}
const subject = requireText(input, "subject");
if (!SUBJECTS.has(subject)) {
throw new Error("Unsupported subject.");
}
const statusFilter = optionalText(input, "status_filter") || "All";
if (!STATUS_FILTERS.has(statusFilter)) {
throw new Error("Unsupported status_filter.");
}
return {
...common,
...validatePagedLedgerFields(input),
...validateOptionalMemberFields(input),
subject,
status_filter: statusFilter,
show_subject_filter: optionalBoolean(input, "show_subject_filter", true),
show_status_filter: optionalBoolean(input, "show_status_filter", true),
};
}
function validateOrigin(origin) {
const parsed = new URL(origin);
if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
throw new Error("HOST_ORIGIN must be scheme://host[:port] with no path/query/hash.");
}
return `${parsed.protocol}//${parsed.host}`;
}
function signBootstrapPayload(payload) {
const canonical = [
payload.tenant_id,
payload.widget_id,
payload.origin,
payload.timestamp,
payload.nonce,
"bootstrap",
].join(".");
return crypto
.createHmac("sha256", requiredEnv("NEUT_MINT_SECRET"))
.update(canonical)
.digest("base64url");
}
function createWidgetEmbedParams(input) {
const widgetPath = WIDGET_PATHS[input.widget_id];
const timestamp = String(Math.floor(Date.now() / 1000));
const bootstrapPayload = {
tenant_id: requiredEnv("WIDGET_TENANT_ID"),
widget_id: input.widget_id,
origin: validateOrigin(requiredEnv("HOST_ORIGIN")),
timestamp,
nonce: crypto.randomUUID(),
key_id: requiredEnv("NEUT_MINT_KEY_ID"),
};
const embedUrl = new URL(widgetPath, requiredEnv("WIDGET_ORIGIN"));
embedUrl.searchParams.set("tenant_id", bootstrapPayload.tenant_id);
embedUrl.searchParams.set("widget_id", bootstrapPayload.widget_id);
embedUrl.searchParams.set("customer_origin", bootstrapPayload.origin);
embedUrl.searchParams.set("bootstrap_timestamp", bootstrapPayload.timestamp);
embedUrl.searchParams.set("bootstrap_nonce", bootstrapPayload.nonce);
embedUrl.searchParams.set("bootstrap_key_id", bootstrapPayload.key_id);
embedUrl.searchParams.set(
"bootstrap_assertion",
signBootstrapPayload(bootstrapPayload)
);
for (const [key, value] of Object.entries(input)) {
if (value !== undefined && value !== null && key !== "widget_id") {
embedUrl.searchParams.set(key, String(value));
}
}
return { widget_id: input.widget_id, embed_url: embedUrl.toString() };
}
app.post("/api/widget-embed-params", (req, res) => {
try {
const input = validateWidgetRequest(req.body);
res.json(createWidgetEmbedParams(input));
} catch (error) {
res.status(400).json({
error: {
code: "INVALID_WIDGET_REQUEST",
message: error.message || "Widget setup failed.",
},
});
}
});
In this example, both widgets accept either:
- a general request with no member filters
- a member-targeted request with the full
member_type+last_name+state_codeset
Do not accept partial member filters when building the iframe URL.
vote-breakdown currently supports House roll call votes only. Member-targeted requests must use member_type=Representative, and general requests return the House roll call feed.
Refresh Handling
If the hosted iframe posts a neut-widget-refresh-request message to the parent page, the
browser should call this same backend endpoint again and replace the iframe src with the newly
returned embed_url.
That refresh request means the old widget session expired. Do not reuse the previous iframe URL. Mint a
fresh bootstrap nonce and assertion each time you return a replacement embed_url.
Security Checklist
- Keep
NEUT_MINT_SECRETon the server only. - Read
WIDGET_TENANT_IDandHOST_ORIGINfrom server configuration. - Do not accept
tenant_id,customer_origin, bootstrap params, or token values from the browser. - Validate all browser-provided widget variables before signing.
- Generate a fresh nonce for each embed URL.
- Treat each embed URL as one-time use.
-
Do not log full
embed_urlvalues, full query strings,bootstrap_assertion,bootstrap_nonce, request bodies sent to/api/widgets/bootstrap,Authorizationheaders, or access tokens. -
If your app runs behind a proxy, make sure the proxy overwrites client-provided
x-forwarded-hostandx-forwarded-protoheaders.
Authorization Boundary
The current widgets expose public civic data. The bootstrap assertion authorizes tenant, widget type, customer origin, timestamp, nonce, and purpose.
Widget display and data-selection params such as bill_number, subject,
page, or last_name are not part of the current authorization boundary. Users may
alter those params and request different public data. Your backend should still validate them for
correctness and user experience.
If a future widget exposes tenant-private, paid, embargoed, personalized, or otherwise restricted data selected by params, those protected params must be included in the signed authorization design.