Scenario
Your HubSpot forms have 500+ free-text submissions. You batch them into Mavera Chat with a customer persona and get structured categorization by pain point, urgency, and use case.Architecture
Code
import os, json, requests
from openai import OpenAI
HS = os.environ["HUBSPOT_ACCESS_TOKEN"]
MV = os.environ["MAVERA_API_KEY"]
# 1. Pull form submissions (paginated)
FORM_ID = "your-form-guid"
subs, after = [], ""
while len(subs) < 200:
params = {"limit": 50}
if after: params["after"] = after
r = requests.get(f"https://api.hubapi.com/form-integrations/v1/submissions/forms/{FORM_ID}",
headers={"Authorization": f"Bearer {HS}"}, params=params)
r.raise_for_status()
data = r.json()
subs.extend(data.get("results", []))
after = data.get("paging", {}).get("next", {}).get("after", "")
if not after: break
# 2. Extract free-text
texts = []
for s in subs:
for f in s.get("values", []):
if f.get("name") == "biggest_challenge" and f.get("value", "").strip():
texts.append(f["value"].strip())
# 3. Categorize in chunks
mavera = OpenAI(api_key=MV, base_url="https://app.mavera.io/api/v1")
schema = {"type": "json_schema", "json_schema": {"name": "cats", "strict": True, "schema": {
"type": "object", "required": ["categories"],
"properties": {"categories": {"type": "array", "items": {"type": "object",
"required": ["index", "pain_point", "urgency", "use_case"],
"properties": {
"index": {"type": "number"}, "pain_point": {"type": "string"},
"urgency": {"type": "string", "enum": ["high", "medium", "low"]},
"use_case": {"type": "string"},
}}}}}}}
all_cats = []
for i in range(0, len(texts), 50):
chunk = texts[i:i+50]
numbered = "\n".join(f"{j+1}. {t[:200]}" for j, t in enumerate(chunk))
result = mavera.responses.create(model="mavera-1",
input=[{"role": "user", "content": f"Categorize these {len(chunk)} form responses by pain point, urgency, use case.\n\n{numbered}"}],
extra_body={"persona_id": os.environ.get("CUSTOMER_PERSONA_ID",""), "response_format": schema})
parsed = json.loads(result.output[0].content[0].text)
all_cats.extend(parsed.get("categories", []))
# 4. Summary
points = {}
for c in all_cats:
points[c["pain_point"]] = points.get(c["pain_point"], 0) + 1
print(f"Categorized: {len(all_cats)} | Unique pain points: {len(points)}")
for pp, n in sorted(points.items(), key=lambda x: -x[1])[:10]:
print(f" {pp}: {n}")
print(f"High urgency: {sum(1 for c in all_cats if c['urgency']=='high')}/{len(all_cats)}")
const OpenAI = require("openai").default;
const HS = process.env.HUBSPOT_ACCESS_TOKEN;
const MV = process.env.MAVERA_API_KEY;
// 1. Pull form submissions
const FORM_ID = "your-form-guid";
const subs = [];
let after = "";
while (subs.length < 200) {
const params = new URLSearchParams({ limit: "50" });
if (after) params.set("after", after);
const data = await fetch(
`https://api.hubapi.com/form-integrations/v1/submissions/forms/${FORM_ID}?${params}`,
{ headers: { Authorization: `Bearer ${HS}` } }
).then(r => r.json());
subs.push(...(data.results || []));
after = data.paging?.next?.after || "";
if (!after) break;
}
// 2. Extract free-text
const texts = subs.flatMap(s =>
(s.values || []).filter(f => f.name === "biggest_challenge" && f.value?.trim()).map(f => f.value.trim())
);
// 3. Categorize
const mavera = new OpenAI({ apiKey: MV, baseURL: "https://app.mavera.io/api/v1" });
const allCats = [];
for (let i = 0; i < texts.length; i += 50) {
const chunk = texts.slice(i, i + 50);
const numbered = chunk.map((t, j) => `${j+1}. ${t.slice(0, 200)}`).join("\n");
const result = await mavera.responses.create({
model: "mavera-1",
input: [{ role: "user", content: `Categorize these ${chunk.length} form responses by pain point, urgency, use case.\n\n${numbered}` }],
extra_body: { persona_id: process.env.CUSTOMER_PERSONA_ID || "", response_format: {
type: "json_schema", json_schema: { name: "cats", strict: true, schema: {
type: "object", required: ["categories"],
properties: { categories: { type: "array", items: { type: "object",
required: ["index", "pain_point", "urgency", "use_case"],
properties: { index: { type: "number" }, pain_point: { type: "string" },
urgency: { type: "string", enum: ["high", "medium", "low"] }, use_case: { type: "string" } }
}}}
}}
}},
});
allCats.push(...(JSON.parse(result.output[0].content[0].text).categories || []));
}
// 4. Summary
const points = {};
allCats.forEach(c => { points[c.pain_point] = (points[c.pain_point] || 0) + 1; });
console.log(`Categorized: ${allCats.length} | Pain points: ${Object.keys(points).length}`);
Object.entries(points).sort(([,a],[,b]) => b-a).slice(0,10).forEach(([p,n]) => console.log(` ${p}: ${n}`));
Example Output
{ "categorized": 487,
"top_pain_points": [
{ "pain_point": "Manual reporting takes too long", "count": 89 },
{ "pain_point": "No single source of truth", "count": 72 },
{ "pain_point": "Data scattered across tools", "count": 64 }
], "urgency": { "high": 142, "medium": 231, "low": 114 } }
Error Handling
Form GUID
Form GUID
Forms API uses a GUID. Find it in HubSpot → Marketing → Forms → URL, or
GET /marketing/v3/forms.Short responses add noise
Short responses add noise
Filter out responses under 10 characters before sending to Mavera.