GroupWisdom API

Send your team's data. Get wisdom back.

POST items to a project → GroupWisdom decides whether there is anything worth saying, and usually there is not → when there is, wisdom is POSTed to your webhook_url and available via GET anytime.

Live demo

Pick a scenario and click Run demo. Real data goes in via the API, the engine runs live, and the right panel traces it.

DATA GOING IN — 4 items from 3 contributors
Engine
GROUPWISDOM ENGINE
Click Run Demo to watch the engine work...

JavaScript / TypeScript SDK

The easiest way to integrate GroupWisdom. Install once, call functions — no HTTP boilerplate required.

npm install @groupwisdom/sdk

Your personal API key is on your account page.

Quick start

import GroupWisdom from '@groupwisdom/sdk'

const gw = new GroupWisdom({ apiKey: process.env.GROUPWISDOM_API_KEY })

// Create a project
const project = await gw.createProject('my-project')

// Send data — attributed to a specific person
await gw.ingest(project.id, {
  title: 'Q3 user research findings',
  content: 'Users in APAC want offline mode...',
  contributed_by: 'Sarah'
})

// Read the wisdom when ready
const wisdom = await gw.listWisdom(project.id)
wisdom.data.forEach(w => console.log(w.title, "—", w.body))

SDK reference

MethodDescription
new GroupWisdom({ apiKey })Create a client. Pass baseUrl to point at a self-hosted instance.
gw.createProject(name, { webhook_url? })Create a new project. Returns a Project object.
gw.listProjects()List all projects you have access to.
gw.getProject(projectId)Get a single project by ID.
gw.updateProject(projectId, { webhook_url })Update a project's webhook URL.
gw.ingest(projectId, item | item[])Send one or more items. Triggers analysis. Each item can include contributed_by.
gw.listItems(projectId)List all items in a project.
gw.listWisdom(projectId, kind?, { format? })Get the project's wisdom. Default returns { id, title, body }. Pass format: "full" to include kind, status, created_at, confidence, do_next, caveat, and missing_voice.
gw.listInsights(...)The former name for listWisdom. Still present and returns identical data, so existing code needs no change.
gw.analyze(projectId)Trigger a full re-analysis of the project immediately, bypassing the automatic debounce. Returns when the request is accepted — analysis runs in the background.

Full TypeScript types are included — your editor will autocomplete every field.

Authentication

There are two types of API key:

Key typeFormatAccess
Personal keygw_...All your projects. Found in Developer API → Dashboard. Use for setup scripts and trusted backends.
Project keygw_proj_...One specific project only. Create via API or dashboard. Use in production apps — if it leaks, revoke it without affecting your account.
Authorization: Bearer gw_proj_your_project_key

Base URL

https://testgroupwisdom.com/v1

All v1 endpoints are under /v1. The existing web app lives at /api and is unchanged.

Errors

All errors return JSON with an error field and a standard HTTP status code.

StatusMeaning
401Missing or invalid API key
400Bad request — missing required field
404Project not found or not yours
202Accepted — ingest queued, not yet processed

Create project

POST/v1/projects
FieldTypeDescription
namestringProject name required
webhook_urlstringURL to receive wisdom callbacks optional
curl -X POST https://testgroupwisdom.com/v1/projects \
  -H "Authorization: Bearer gw_yourkey" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Q3 User Research",
    "webhook_url": "https://your-server.com/groupwisdom-hook"
  }'
{
  "id": "5d96994a-9164-44bb-891c-caf57eb6760d",
  "name": "Q3 User Research",
  "created_at": "2026-06-26T14:22:00Z",
  "webhook_url": "https://your-server.com/groupwisdom-hook",
  "counts": { "items": 0, "wisdom": 0, "insights": 0 }
}

List projects

GET/v1/projects

Returns all projects you have access to.

curl https://testgroupwisdom.com/v1/projects \
  -H "Authorization: Bearer gw_yourkey"

Get project

GET/v1/projects/:id

Returns a single project with current item and wisdom counts.

Update project

PATCH/v1/projects/:id

Update project settings. Only fields you send are changed.

FieldTypeDescription
webhook_urlstring | nullSet to null to remove the webhook
enginestringclaude (default) or muse-spark — sets which model runs analysis for this project
curl -X PATCH https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d \
  -H "Authorization: Bearer gw_yourkey" \
  -H "Content-Type: application/json" \
  -d '{"engine": "muse-spark"}'

Ingest items

POST/v1/projects/:id/ingest

Send one item or an array of items. GroupWisdom queues analysis immediately — wisdom is delivered to your webhook_url within seconds and available via GET anytime.

Returns 202 Accepted — analysis is async.

Item fields

FieldTypeDescription
titlestringShort title or headline required*
contentstringFull text, notes, or description
urlstringLink to source
typestringnote · link · file · thought (default: note)
contributed_bystringName of the person who contributed this item. The engine uses it to attribute findings by name — so one member's completed work can be handed directly to another (do_next, missing_voice). Highly recommended.
channelstringWhere this came from: a room, thread, or channel id from your own app (max 100 characters). Changes what a finding may be built from — see How the engine works. Optional.

* At least one of title, content, or url is required per item.

Building a chat integration? Send content alone. Chat messages have no title, and you do not need to invent one — post the message text as content and we derive the title ourselves. A reviewer building against this API synthesized titles from first sentences before noticing the footnote above, so it is worth stating plainly: {"content": "…", "contributed_by": "Sarah"} is a complete item.

Single item

curl -X POST https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/ingest \
  -H "Authorization: Bearer gw_yourkey" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Users keep abandoning checkout on mobile",
    "content": "Support tickets up 30% this month. Affects iOS more than Android.",
    "type": "note",
    "contributed_by": "Sarah"
  }'

Bulk ingest

curl -X POST https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/ingest \
  -H "Authorization: Bearer gw_yourkey" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {"title": "Mobile checkout drop-off up 30%", "type": "note"},
      {"title": "Competitor added Apple Pay last week", "type": "note"},
      {"url": "https://example.com/analytics-report", "type": "link"},
      {"title": "Team decision: prioritise mobile payment flow", "type": "thought"}
    ]
  }'
{
  "accepted": 4,
  "items": [
    {"id": "item_001", "title": "Mobile checkout drop-off up 30%", "type": "note"},
    ...
  ],
  "message": "Items queued for analysis. Insights will be POSTed to your webhook_url when ready."
}

List items

GET/v1/projects/:id/items

Returns items in a project, newest first. Paginated — default 50 per page, max 200.

Query paramDefaultDescription
limit50Items per page (max 200)
offset0Number of items to skip
curl "https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/items?limit=20&offset=0" \
  -H "Authorization: Bearer gw_proj_yourkey"
{
  "data": [...],
  "total": 142,
  "limit": 20,
  "offset": 0,
  "has_more": true
}

Delete item

DELETE/v1/projects/:id/items/:itemId

Permanently removes an item from a project.

curl -X DELETE https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/items/item_001 \
  -H "Authorization: Bearer gw_proj_yourkey"

How the engine works

Analysis runs automatically a few seconds after each /ingest. Most of the time it produces nothing, and that is the intended result.

Every batch passes through a series of checks before anything is written. The first ones ask whether there is anything here worth thinking about at all: a question to a colleague, a status ping, or a message that restates what the project already knows does not go any further. Only what survives is drafted, and a final review decides whether the draft is strong enough to keep, sharpens the wording, and attaches confidence, do_next, caveat and missing_voice. Weak candidates are dropped rather than softened.

The practical effect is that the great majority of ingested messages never reach the drafting stage, so they cost a fraction of a full analysis.

After it speaks, it pauses

Once a finding is posted, the project stays quiet for 10 minutes regardless of what arrives in the meantime. Two good findings a minute apart still read as one wall of text in a chat client, so the cooldown protects the thing restraint is for.

This is the most common reason a working integration looks broken: you send more items, nothing comes back, and the engine appears to have stopped. It has not. GET /gate-records will show Spoke within the last 10 minutes. against the batch. Set GW_WISDOM_COOLDOWN_MIN if you are self-hosting and want a different window.

Cost does not grow with your project

The engine reasons from a compact working memory of what your group has established, not from your accumulated history. That memory is bounded and folded forward as new items arrive, so a scan on a project's five hundredth day costs about what it cost on the first. Adding history makes the findings better without making the analysis more expensive.

POST /v1/projects/:id/analyze is the exception, and the reason it exists as a separate call: it re-reads the project in full. Use it deliberately rather than on a schedule.

Why it stayed quiet

Silence is a decision, and every one is recorded with its reason. If you are wondering why a message you expected to produce something did not, you can read the record rather than guess — see Why it stayed quiet.

What a finding is allowed to draw on

If you send a channel with your items, findings become channel-aware: what the engine says in one place is built only from what that place can see, while its understanding of the project as a whole still spans everything you send. Anything it can trace to a different channel stays there. Leave channel off and the project is treated as one audience, which is the right choice when everyone sees everything.

Rate limits and allowance

Two separate things bound what you can do: how fast you may call the API, and how much analysis your account may run.

Rate limits

Counted per API key, in a rolling one-minute window. Exceeding one returns 429 with a JSON body, never an HTML page.

RoutesRequests per minute
/v1/* — everything in this reference240
Sign-in and account routes10
Connecting a Buzz community5

Ingest accepts arrays, so a batch of items is one request. Sending them one at a time is the usual way to reach the limit for no benefit.

Analysis allowance

Analysis costs real money to run, so every account has an allowance covering all of its projects together. Deleting a project does not return any of it.

Check where you stand with GET /v1/usage, which returns percent_used and limit_reached. Nothing about it is silent: when the allowance is spent, /ingest keeps accepting and storing your items and the engine stops analysing them until it resets, so no data is lost and nothing fails unexpectedly.

Published plans and per-item pricing are not settled yet. If you are building something that needs a specific ceiling, get in touch before you depend on the current one.

What it has understood

GET/v1/projects/:id/memory

Returns the working memory the engine reasons from: the project's purpose as it understands it, the facts it has established and who contributed them, the decisions it has seen made, and the questions it still considers open. Each fact carries the items it came from.

This is the fastest way to answer "is it actually following what we are doing". If a finding looks wrong, this shows you what it was built on. It is also the first place to look when the engine has gone quiet and you expected otherwise.

memory is null on a project that has not accumulated anything yet.

curl "https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/memory" \
  -H "Authorization: Bearer gw_proj_yourkey"
{
  "memory": {
    "purpose": "Shipping the mobile checkout redesign",
    "facts": [
      { "fact": "The redesign tested 22% faster than the current flow", "by": "Sarah", "sources": ["a1b2c3d4"] }
    ],
    "decisions": [
      { "decision": "Ship behind a flag", "sources": ["e5f6a7b8"] }
    ],
    "open_questions": ["Who owns the migration for saved cards?"]
  },
  "updated_at": "2026-08-17 21:40:11"
}

Why it stayed quiet

GET/v1/projects/:id/gate-records

The engine says nothing most of the time, on purpose. Every one of those decisions is recorded with the reason for it, so silence is auditable rather than mysterious. Supports ?limit (default 50, max 200), newest first.

verdict is one of silent (nothing was worth saying), suppressed (something was drafted and the review dropped it), spoken (a finding was surfaced, with its insight_id), or error.

If your integration seems too quiet, read this before assuming it is broken. Most of the time it will tell you the messages were requests, restatements, or a single person thinking aloud, none of which the engine treats as something to combine.

curl "https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/gate-records?limit=20" \
  -H "Authorization: Bearer gw_proj_yourkey"
{
  "records": [
    { "stage": "scan",   "verdict": "silent",     "reason": "No new contribution: a question to a colleague adds nothing to combine", "created_at": "..." },
    { "stage": "review", "verdict": "suppressed", "title": "Team is aligned on timing", "reason": "confidence low", "created_at": "..." },
    { "stage": "review", "verdict": "spoken",     "title": "Paid ads must start before partners commit", "insight_id": "...", "created_at": "..." }
  ]
}

Usage

GET/v1/usage

How much of your analysis allowance you have used, across every project on your account. Returns percent_used and limit_reached. When the limit is reached, ingest still accepts your items and the engine stops analysing them until the allowance resets, so nothing is lost.

Requires your personal API key. Project keys cannot read account-wide usage.

curl "https://testgroupwisdom.com/v1/usage" \
  -H "Authorization: Bearer gw_yourkey"

List wisdom

GET/v1/projects/:id/wisdom

Returns the wisdom the engine has surfaced, newest first. Supports ?limit, ?offset, and ?kind filters.

GET /v1/projects/:id/insights is the former name for this endpoint and still works, identically. Nothing you have running needs to change.

By default returns a minimal shape — id, title, and body. Pass ?format=full to include the review fields: kind, status, created_at, confidence, do_next, caveat, and missing_voice.

# Default (minimal)
curl "https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/wisdom" \
  -H "Authorization: Bearer gw_proj_yourkey"

# Full — includes the review fields
curl "https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/wisdom?format=full" \
  -H "Authorization: Bearer gw_proj_yourkey"
// Default response
{
  "data": [
    {
      "id": "9f2c1e04-7b3a-4d51-9c88-1a6e2f0b47d3",
      "title": "Mobile payment friction is a recurring theme",
      "body": "Four of six items point to the same checkout drop-off."
    }
  ],
  "total": 3,
  "limit": 50,
  "offset": 0,
  "has_more": false
}

// With ?format=full
{
  "data": [
    {
      "id": "9f2c1e04-7b3a-4d51-9c88-1a6e2f0b47d3",
      "kind": "pattern",
      "title": "Mobile payment friction is a recurring theme",
      "body": "Four of six items point to the same checkout drop-off.",
      "status": "new",
      "created_at": "2026-07-15T14:30:00Z",
      "confidence": "high",
      "do_next": "Priya's checkout usability study already located the drop-off at the card-entry step.",
      "caveat": "All data is from support tickets — may overrepresent frustrated users.",
      "missing_voice": "Priya"
    }
  ],
  ...
}

Review fields

FieldValuesDescription
confidencehigh · medium · lowHow strongly the evidence supports this finding. High = 3+ independent data points; medium = 2; low = 1 or inferred.
do_nextstring or nullInherited work, never a task: one more completed result from another member that the reader now has for free. Usually null, by design — the body already hands over the work, and this only fills when a second member's finished result also applies. If what would go here is something the reader ought to go and do, the field stays null rather than turning into advice.
caveatstring or nullA condition or limitation that could invalidate the finding if ignored.
missing_voicestring or nullThe name of a member whose existing work would strengthen this reader's — just the name, checked against your project's real members. A name we cannot match to the roster is dropped rather than returned, so this field never invents a person.

These fields are always present in format=full responses — they may be null when not applicable, and do_next and missing_voice usually are. That is the intended behaviour rather than a gap: both exist to hand over a second member's finished work, and most findings have only the one the body already carries.

You decide what to show your users. A simple card might render only title and body. A richer view might surface do_next as the inherited finding a member can build on. A team dashboard might show all fields including confidence and missing voices.

Trigger analysis

POST/v1/projects/:id/analyze

Triggers a full re-analysis of the project immediately, bypassing the automatic debounce timer. Useful when you want fresh wisdom on demand — for example, after a batch ingest or before showing results to a user.

Returns 202 Accepted immediately. Analysis runs in the background; poll List wisdom or wait for your webhook to receive results.

curl -X POST https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/analyze \
  -H "Authorization: Bearer gw_proj_yourkey"
{ "message": "Analysis started." }

Wisdom kinds

The engine surfaces wisdom in seven kinds. Six describe what a group is building, never what it is failing to do. The seventh is different in kind: it is addressed to one person, at the moment they announce work, and carries another member's finished result rather than the engine's own conclusion.

KindDescription
convergenceTwo people reached the same finding from different directions. It names both, and what each of them proved.
opportunitySomething the group's own work is already pointing at, that nobody has picked up yet.
tensionTwo views worth putting together to reach a stronger conclusion, stated as the actual difference.
patternA theme running across several contributions that none of them called a theme.
directionThe natural next question the group's collective work is building toward.
decisionSomething the group has arrived at together, and what led there.
handoffSomeone announced work they are about to do, and the group already held finished work by someone else that bears on it: a number, a result, a decision, with the name of who produced it. Composed only from facts already in memory, never inferred. Silent unless the fact is specific, sourced, and not the announcer's own.

Project API keys

Project keys (gw_proj_...) are scoped to a single project. Use them in production apps instead of your personal key — if a project key leaks, you revoke just that key without touching your account or other projects.

Project keys can ingest data, read items, and read wisdom. They cannot create new projects or manage other keys — that requires your personal key.

Create a project key

POST/v1/projects/:id/keys

The full key is only returned once on creation. Store it immediately — subsequent GET /keys calls only show a redacted preview.

FieldTypeDescription
namestringLabel for this key (e.g. "Production", "Staging") required
curl -X POST https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/keys \
  -H "Authorization: Bearer gw_your_personal_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "Production"}'
{
  "id": "key_xyz",
  "name": "Production",
  "key": "gw_proj_a1b2c3d4e5f6...",
  "created_at": "2026-06-30T10:00:00Z"
}

Revoke a key

DELETE/v1/projects/:id/keys/:keyId

Immediately invalidates the key. Any requests using it will receive a 401.

curl -X DELETE https://testgroupwisdom.com/v1/projects/5d96994a-9164-44bb-891c-caf57eb6760d/keys/key_xyz \
  -H "Authorization: Bearer gw_your_personal_key"

Webhook setup

Set a webhook_url on any project (at creation or via PATCH). GroupWisdom will POST to that URL each time new wisdom is generated — typically within 3–5 seconds of an ingest call.

Your endpoint must return a 2xx status. Failed deliveries are retried twice — after 5 seconds, then 30 seconds. If all three attempts fail, the delivery is dropped — poll /v1/projects/:id/wisdom as a long-term fallback.

Use webhook.site to inspect payloads during development.

Webhook payload

Every webhook request includes an X-GroupWisdom-Signature header — a HMAC-SHA256 signature of the raw body using your webhook_secret. Always verify this before trusting the payload.

// Node.js — verify webhook signature
import { createHmac } from "crypto"

function verifyWebhook(rawBody, signature, secret) {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex")
  return expected === signature
}

app.post("/groupwisdom-hook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["x-groupwisdom-signature"]
  if (!verifyWebhook(req.body, sig, process.env.GW_WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature")
  }
  const payload = JSON.parse(req.body)
  // handle payload.insights ...
  res.sendStatus(200)
})

Your webhook_secret is returned when you set a webhook_url via PATCH. Store it securely — it is only shown once.

Payload shape

Webhook payloads include the full wisdom shape, equivalent to ?format=full, so you have every review field without a follow-up GET.

If you built against the old names, nothing has changed for you. The payload still carries an insights array with identical contents alongside wisdom, and event is still insights.created. Existing receivers keep working untouched; use wisdom for anything new.

POST https://your-server.com/groupwisdom-hook
Content-Type: application/json
X-GroupWisdom-Signature: sha256=abc123...

{
  "event": "insights.created",
  "group_id": "5d96994a-9164-44bb-891c-caf57eb6760d",
  "wisdom": [
    {
      "id": "9f2c1e04-7b3a-4d51-9c88-1a6e2f0b47d3",
      "kind": "pattern",
      "title": "Mobile payment friction is a recurring theme",
      "body": "Four of six items point to the same checkout drop-off.",
      "status": "new",
      "created_at": "2026-07-15T14:30:00Z",
      "confidence": "high",
      "do_next": "Priya's checkout usability study already located the drop-off at the card-entry step.",
      "caveat": null,
      "missing_voice": "Priya"
    }
  ]
}

JavaScript / TypeScript example

Using the SDK (recommended):

import GroupWisdom from '@groupwisdom/sdk'

const gw = new GroupWisdom({ apiKey: process.env.GROUPWISDOM_API_KEY })

// Send items from multiple contributors
await gw.ingest("5d96994a-9164-44bb-891c-caf57eb6760d", [
  { title: "User retention dropped 12% in APAC", type: "note", contributed_by: "Sarah" },
  { title: "APAC pricing change rolled out last week", type: "note", contributed_by: "James" },
])

// Poll for wisdom — or set a webhook_url to receive it automatically
const wisdom = await gw.listWisdom("5d96994a-9164-44bb-891c-caf57eb6760d")
wisdom.data.forEach(w => console.log(w.title, "—", w.body))

Using raw fetch (if you prefer no dependency):

const GW_KEY = process.env.GROUPWISDOM_API_KEY;
const BASE   = "https://testgroupwisdom.com/v1";

await fetch(`${BASE}/projects/5d96994a-9164-44bb-891c-caf57eb6760d/ingest`, {
  method: "POST",
  headers: { "Authorization": `Bearer ${GW_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ items: [
    { title: "User retention dropped 12% in APAC", type: "note", contributed_by: "Sarah" },
  ]}),
});

Python example

import os, requests

GW_KEY = os.environ["GROUPWISDOM_API_KEY"]
BASE   = "https://testgroupwisdom.com/v1"
HEADERS = {"Authorization": f"Bearer {GW_KEY}", "Content-Type": "application/json"}

def ingest(project_id, items):
    r = requests.post(f"{BASE}/projects/{project_id}/ingest",
                      json={"items": items}, headers=HEADERS)
    r.raise_for_status()
    return r.json()

def get_insights(project_id, kind=None):
    params = {"kind": kind} if kind else {}
    r = requests.get(f"{BASE}/projects/{project_id}/insights",
                     params=params, headers=HEADERS)
    r.raise_for_status()
    return r.json()

# Usage
ingest("5d96994a-9164-44bb-891c-caf57eb6760d", [
    {"title": "User retention dropped 12% in APAC", "type": "note"},
    {"title": "APAC pricing change rolled out last week", "type": "note"},
])

insights = get_insights("5d96994a-9164-44bb-891c-caf57eb6760d", kind="pattern")
for ins in insights:
    print(f"[{ins['kind']}] {ins['title']}: {ins['body']}")