Hub API Guide
The canonical reference for what the Hub API can do and how to use it — for human developers, AI knowledge systems, and software tooling alike.
Give This to Your AI
Hub is designed to work with AI assistants, and this guide is the canonical reference describing how Hub's API actually works. If you use an AI assistant — ChatGPT, Claude, Gemini, a custom GPT, or another AI that can read documentation — giving it this guide helps it work with Hub correctly instead of guessing.
Providing this documentation to an AI helps it:
- Understand what Hub can do.
- Discover which endpoints actually exist.
- Use the correct routes and parameters instead of inventing plausible-looking ones.
- Prefer Search when discovering a record it doesn't already have an ID for.
- Avoid guessing at undocumented capabilities.
- Produce more reliable, correctly-shaped API calls.
This guide does not, by itself, do any of the following:
- It does not authenticate an AI.
- It does not provide API credentials.
- It does not grant access to Hub.
Authorization is still controlled entirely by API keys, sessions, permissions, company (tenant) isolation, and module entitlements — reading this guide is harmless on its own, and teaches an AI how Hub works, not what it's allowed to do. What it's allowed to do is determined by whatever credentials and scope it's actually given.
Choose the Format That Fits Your AI
This guide is published in three forms, generated from one canonical source so they can't drift apart. Pick whichever matches how your AI (or tooling) actually consumes documentation.
Humans
Read this HTML guide: https://justwhatsneeded.com/api-guide.html. Formatted for browsers, with the same content as the other two formats plus page styling and navigation.
AI Knowledge
A plain-Markdown version of this exact guide is published at:
https://justwhatsneeded.com/api-guide.md
This is the form to use for:
- ChatGPT Custom GPT Knowledge
- Claude Projects and Claude knowledge uploads
- Gemini Gems
- Microsoft Copilot knowledge systems
- Any other AI system that accepts Markdown or plain-text documentation uploads
Software / Tools
The detailed machine contract — every field, schema, and response shape — is the OpenAPI specification:
https://hub.justwhatsneeded.com/docs/openapi.yaml
This is the form to use for:
- API tooling
- SDK generation
- MCP servers
- API clients
- Automated integrations
Example Instructions for Your AI
Two copy-ready examples — pick whichever matches how you're giving your AI this guide.
If your AI can retrieve public web pages:
Use the Hub API Guide at https://justwhatsneeded.com/api-guide.html as the canonical reference for Hub API capabilities, routing conventions, authentication requirements, parameters, responses, and usage guidance. Consult it before assuming an endpoint exists.
If you're uploading this guide as knowledge (Custom GPT, Claude Project, Gemini Gem, etc.):
Use the Hub API Guide at https://justwhatsneeded.com/api-guide.md as your primary knowledge source for Hub API behavior. Prefer documented search capabilities when locating unknown records and direct resource endpoints once IDs are known.
Not every AI can open a URL on its own — it needs browsing, web retrieval, a connector, or a configured action to do that. If yours can't, use the second example and actually upload or paste the Markdown version into its knowledge configuration instead of just handing it the link.
Document Metadata
| Title | Hub API Guide |
| Status | Canonical |
| Audience | Human developers, AI knowledge systems, and software/tooling |
| API Guide version | 3.0 |
| Hub API / OpenAPI version | 1.2.0 |
| Last updated | 2026-07-20 |
| Canonical URL (HTML) | https://justwhatsneeded.com/api-guide.html |
| Markdown / AI knowledge URL | https://justwhatsneeded.com/api-guide.md |
| OpenAPI specification | https://hub.justwhatsneeded.com/docs/openapi.yaml |
| Purpose | Authoritative instructions for discovering and using supported Hub API capabilities. |
AI Agent Guidance
- This page is the canonical public Hub API reference. Consult it before assuming an endpoint or capability exists.
- Hub uses
resourceandactionquery-parameter routing — not REST-style paths. See Routing. - When the user describes a record but doesn't know its ID or location, use search first — it is the preferred discovery mechanism.
- Once a resource ID is known (from a prior search, a prior response, or the user), use the direct resource endpoint (e.g.
tasks&action=get) rather than searching again. - Do not enumerate every project or resource merely to locate an unknown record when search can satisfy the request directly.
- Tenant isolation, authorization, API-key scope, and module entitlement rules must always be respected — there is no supported way around them, and none should be assumed.
- A zero-result search is not evidence that a record doesn't exist. It only means nothing matched inside the caller's own tenant and currently-enabled modules.
- Do not infer or assume access to a module that is disabled for the current company.
- Destructive or modifying actions (create, update, complete, delete) must never be inferred from a search request — search is read-only discovery.
Contents
- Authentication
- Routing
- Projects
- Tasks
- Ideas
- Comments
- Search
- Pagination
- Errors
- Machine-Readable Reference
Authentication
API key (recommended for developers and AI agents). Send your API key in the X-API-Key header on every request. Keys are scoped to one company — a request can only read and write that company's own data.
X-API-Key: your-api-key-here
Content-Type: application/json
Browser session (Hub's own web UI). A logged-in Hub browser session (a valid PHPSESSID cookie) is also accepted and behaves identically to a full-scope API key. This exists for Hub's own first-party pages, not third-party integrations — external developers and AI agents should use an API key instead.
API key scopes. A company's own key is always full-scope. A company may also issue narrower, purpose-specific scoped keys from its Admin panel. A scoped key can only perform the exact resource/action pairs its scope allows — anything else is rejected with the same response as an invalid key, so a caller can never distinguish "wrong scope" from "no key at all."
| Scope | Allowed |
|---|---|
full |
Every resource and action (the company's own key). |
ai_agent |
Read-only projects; read/write (no delete) on tasks and ideas; general search. Intended for external AI integrations. |
quick_capture |
tasks&action=create only. Intended for lightweight, single-purpose capture tools. |
Base URL: https://hub.justwhatsneeded.com/api.php
Your company's full-scope API key is shown in the welcome email and on the Admin panel. Keep it private — it grants full read/write access to your company's data. Never paste a real key into a prompt, log, or shared document.
Routing
Hub's API is resource/action routed, not REST-path routed. Every request targets /api.php with two query-string parameters:
| Param | Meaning |
|---|---|
resource |
The noun being operated on — e.g. tasks, projects, ideas, comments, search. |
action |
The verb — e.g. list, get, create, update, complete, promote, search. |
There is no path-templated equivalent like /tasks/42 — every operation is /api.php?resource=...&action=..., with GET query parameters or a JSON POST body carrying the rest of the request. GET is used for read operations, POST for anything that writes. Responses are always JSON.
This applies uniformly, including to search: it is ?resource=search&action=search, not a bare ?action=search — the resource/action pair is always required, even when a resource has only one action.
Projects
List Projects
GET /api.php?resource=projects&action=list — scope: full, ai_agent
Purpose: List every project belonging to the caller's company, most recently updated first.
When to use: Browsing or picking a project when you don't yet know its ID.
When not to use: Don't page through this to find one task or record — use search instead.
Optional parameters: show_all (boolean) — include hidden (display=0) projects. Default false.
Module entitlement: Requires the projects module enabled for the company; otherwise 403.
Tenant isolation: Only the caller's own company's projects are ever returned.
Example response
[
{
"id": 11,
"company_id": 1,
"name": "JWN - All",
"description": "Main project board",
"status": "active",
"display": 1,
"client": "JWN",
"created_at": "2025-01-10 09:00:00",
"updated_at": "2026-06-01 14:22:00",
"url": "/project.php?id=11"
}
]
Create Project
POST /api.php?resource=projects&action=create — scope: full
Purpose: Create a new project.
When not to use: Not available to ai_agent-scoped keys (read-only on projects) — use a full-scope key, or ask the user to create the project.
Tenant isolation: Created under the caller's own company automatically — there is no company_id field to set.
| Field | Type | Notes |
|---|---|---|
name (required) |
string | Project name |
description |
string | Optional description |
status |
string | active · on-hold · completed · archived (default: active) |
client |
string | Client label, e.g. JWN |
Example request
{ "name": "Q3 Marketing Campaign", "description": "All tasks for the Q3 push", "status": "active", "client": "JWN" }
Example response
{ "id": 14, "message": "Project created" }
Tasks
List Tasks
GET /api.php?resource=tasks&action=list&project_id=11 — scope: full, ai_agent
Purpose: List tasks, optionally filtered to one project or one category.
When to use: You already know the project (or category) and want every task in it.
When not to use: Looking for one specific task by title or keyword across many projects — use search instead of listing and scanning client-side.
Optional parameters: project_id (integer) — restrict to one project. category (string) — restrict to a category; when set, non-done tasks are returned unless include_done is also set. include_done (boolean) — only relevant alongside category.
Validation: No pagination on this endpoint — it returns every matching row. For large result sets or keyword filtering, prefer search, which supports limit/offset.
Module entitlement: Requires the tasks module; otherwise 403.
Tenant isolation: Only tasks whose project belongs to the caller's company are returned.
Example response
[
{
"id": 42,
"project_id": 11,
"title": "Review contract",
"description": null,
"tags": null,
"status": "todo",
"priority": "high",
"category": null,
"due_date": "2026-06-20",
"version": 1,
"created_at": "2026-06-10 08:00:00",
"updated_at": "2026-06-10 08:00:00",
"url": "/project.php?id=11&task=42"
}
]
Create Task
POST /api.php?resource=tasks&action=create — scope: full, ai_agent, quick_capture
Purpose: Create a new task inside a project.
Common mistakes / agent guidance: project_id is required in practice — the backend has an internal fallback project for one specific company only, so never omit project_id and never assume a default project exists; always supply a real, known project ID.
Tenant isolation: The target project must belong to the caller's company, or the request fails with 404.
| Field | Type | Notes |
|---|---|---|
project_id (required) |
integer | Parent project ID. Always pass this explicitly. |
title (required) |
string | Task title |
description |
string | Optional detail |
tags |
string or array | Comma-separated or array; normalized and de-duplicated server-side |
status |
string | todo · in-progress · blocked · done (default: todo) |
priority |
string | low · normal · high · urgent (default: normal) |
category |
string | Optional free-text category |
due_date |
string | ISO date: YYYY-MM-DD |
Example request
{ "project_id": 11, "title": "Review contract", "priority": "high", "status": "todo", "due_date": "2026-06-20" }
Example response
{ "id": 42, "message": "Task created", "url": "/project.php?id=11&task=42" }
Update Task
POST /api.php?resource=tasks&action=update — scope: full, ai_agent
Purpose: Update one or more fields on an existing task. Only include fields you want to change.
| Field | Type | Notes |
|---|---|---|
id (required) |
integer | Task ID to update |
title |
string | |
description |
string | |
tags |
string or array | |
status |
string | todo · in-progress · blocked · done |
priority |
string | low · normal · high · urgent |
due_date |
string | YYYY-MM-DD |
version |
integer | Optional. If supplied and stale, the request fails with 409 and returns the current row — use this to avoid silently overwriting a concurrent edit. |
Example request
{ "id": 42, "status": "in-progress", "priority": "urgent" }
Example response
{ "id": 42, "title": "Review contract", "status": "in-progress", "priority": "urgent", "version": 2 }
Complete Task
POST /api.php?resource=tasks&action=complete — scope: full, ai_agent
Purpose: Convenience wrapper that sets status=done. Idempotent — completing an already-complete task just succeeds again.
Required parameters: id (integer)
Example request
{ "id": 42 }
Example response
{ "id": 42, "status": "done", "version": 3 }
Ideas
A lighter-weight backlog for things worth capturing before they're ready to become a task. Full field-level detail is in the OpenAPI specification; the two most commonly used actions are summarized here.
Create Idea
POST /api.php?resource=ideas&action=create — scope: full, ai_agent
Purpose: Capture an idea for later triage — not yet committed engineering work.
Required parameters: title (string)
Optional parameters: description, status (idea · reviewing · ready; default idea), created_by
Example response
{ "id": 210, "title": "...", "status": "idea", "url": "/ideas.php" }
Promote Idea
POST /api.php?resource=ideas&action=promote — scope: full, ai_agent
Purpose: Turns an idea into a real task under a given project. The idea itself is kept (not deleted), status set to ready, promoted_task_id linked to the new task.
Required parameters: id (the idea), project_id (where the new task should live)
Common mistakes / agent guidance: An idea can only be promoted once — a second attempt fails with 400.
Example response
{ "idea": { "...": "updated idea, status: ready" }, "task": { "...": "newly created task" } }
Comments
List Comments
GET /api.php?resource=comments&action=list&task_id=42 — scope: full
Purpose: List comments on a task, newest first.
Required parameters: task_id (integer)
When not to use: Not available to ai_agent-scoped keys today — use a full-scope key.
Example response
[
{ "id": 7, "task_id": 42, "author": "Bill", "body": "Ready for review", "created_at": "2026-06-11 10:30:00" }
]
Create Comment
POST /api.php?resource=comments&action=create — scope: full
Purpose: Adds a comment to a task.
| Field | Type | Notes |
|---|---|---|
task_id (required) |
integer | Task to comment on |
author (required) |
string | Author name, e.g. Bill or Claude |
body (required) |
string | Comment text |
Example request
{ "task_id": 42, "author": "Bill", "body": "Ready for review" }
Example response
{ "id": 8, "message": "Comment added" }
Search
The general-purpose, canonical way to find a record when you don't already know its ID. This is the preferred discovery mechanism for AI agents — see AI Agent Guidance.
GET /api.php?resource=search&action=search&q=password — scope: full, ai_agent
Purpose: Search across Hub resources by keyword. Today this covers tasks only — the response shape and the type parameter are designed so more resource types (documents, ideas, and others) can be added later without changing this contract, but only task is implemented right now.
When to use: The user describes a record (by title, keyword, or ID) but you don't already know exactly where it lives.
When not to use: Once you have a concrete ID, use the resource's own get action instead of searching again. Don't use search as a substitute for a targeted list when you already know the exact project/filter and have no free-text to match.
Route: GET /api.php?resource=search&action=search — deliberately not the bare ?action=search form; Hub's routing convention (see Routing) requires an explicit resource on every request, even one with a single action.
Authentication & scope: API key or browser session, same as every other endpoint. Included in the ai_agent scoped-key allowlist — this is the intended search mechanism for AI-agent integrations. Not available to quick_capture-scoped keys.
Required parameters:
| Param | Notes |
|---|---|
q |
Search text. Missing or blank (after trimming) → 400. |
Optional parameters:
| Param | Default | Notes |
|---|---|---|
type |
all supported types | Comma-separated resource types. Only task is implemented today, so task is currently the only accepted value; requesting any other type (e.g. document) returns 400 rather than silently returning nothing. |
project_id |
none | Restrict task results to one project. |
status |
none | todo · in-progress · done · blocked |
priority |
none | low · normal · high · urgent |
include_completed |
false |
When false (default), status=done tasks are excluded. |
limit |
25 |
Maximum 100. Values above 100 are clamped to 100, not rejected. Zero, negative, or non-numeric values → 400. |
offset |
0 |
Negative or non-numeric values → 400. |
Matching behavior: Matches against task title and description: case-insensitive, partial (substring) match. A literal % or _ in q is treated as a literal character, never a SQL wildcard. If q is purely numeric, it is also matched exactly against the task's numeric id.
Ranking / order: Results are ordered by, in priority order: (1) exact numeric ID match, (2) exact title match, (3) title contains the query, (4) description contains the query, then (5) most-recently-updated first as the tiebreaker within any of the above.
Validation rules and limits: A filter that matches nothing — an unrecognized status/priority value, or a project_id with no matching tasks — returns an empty results array, not an error. A project_id belonging to a different company also returns an empty array, identically to any other non-matching filter — this is deliberate and matches Hub's tenant-isolation convention of never revealing, even indirectly, whether a record exists outside the caller's own company.
Module entitlement: Each result type checks the entitlement for its own module independently. Today that means: if the tasks module is disabled for the company, the task type simply contributes zero results — the whole search request still succeeds. This is intentional: a future search spanning several resource types must not have every type blocked just because one of that company's modules happens to be off.
Tenant isolation: Only tasks belonging to the caller's own company are ever returned, enforced the same way as every other tasks endpoint.
Response shape: A JSON object with query, results (an array), and pagination. Each result includes:
| Field | Notes |
|---|---|
type |
Currently always "task". |
id, project_id, project_name, title, status, priority |
Same values as the tasks endpoints. |
description_excerpt |
A bounded preview of the description, centered on the match when there is one. null if the task has no description. |
matched_fields |
Which of id, title, description actually matched — e.g. ["title","description"]. |
url |
The same canonical /project.php?id=...&task=... link the tasks endpoints return. |
pagination is { "limit": ..., "offset": ..., "has_more": ... } — see Pagination.
Status / error responses: 200 success (including a legitimate zero-result search) · 400 missing/blank q, unsupported type, or invalid limit/offset · 401 missing/invalid authentication.
Common mistakes / agent guidance: Don't request type=document, type=idea, or type=all — only task exists today, and anything else is a 400, not an empty result. Don't treat a zero-result search as proof a record doesn't exist anywhere — it only means nothing matched in this tenant's currently-searchable data.
Search Examples
1. Normal keyword search
GET /api.php?resource=search&action=search&q=password
X-API-Key: your-api-key-here
{
"query": "password",
"results": [
{
"type": "task",
"id": 1104,
"project_id": 11,
"project_name": "Platform Polish",
"title": "Add Admin Password Reset",
"description_excerpt": "Platform administrators need a safe way to reset...",
"status": "todo",
"priority": "high",
"matched_fields": ["title", "description"],
"url": "/project.php?id=11&task=1104"
}
],
"pagination": { "limit": 25, "offset": 0, "has_more": false }
}
2. Search restricted to a project
GET /api.php?resource=search&action=search&q=reset&project_id=11
X-API-Key: your-api-key-here
3. Numeric task-ID search
GET /api.php?resource=search&action=search&q=1104
X-API-Key: your-api-key-here
{ "query": "1104", "results": [ { "type": "task", "id": 1104, "matched_fields": ["id"] } ], "pagination": { "limit": 25, "offset": 0, "has_more": false } }
4. Pagination
GET /api.php?resource=search&action=search&q=onboarding&limit=10&offset=10
X-API-Key: your-api-key-here
5. Zero-result response
GET /api.php?resource=search&action=search&q=zzz_no_such_term_zzz
X-API-Key: your-api-key-here
{ "query": "zzz_no_such_term_zzz", "results": [], "pagination": { "limit": 25, "offset": 0, "has_more": false } }
6. Invalid resource type → 400
GET /api.php?resource=search&action=search&q=x&type=document
X-API-Key: your-api-key-here
{ "error": "Unsupported type(s): document. Supported types: task" }
7. Unauthenticated request → 401
GET /api.php?resource=search&action=search&q=password
{ "error": "Unauthenticated" }
Pagination
Hub uses limit/offset pagination. Today, search is the only endpoint that paginates; other list actions currently return every matching row in one response. A paginated response includes a pagination object:
| Field | Meaning |
|---|---|
limit |
The effective page size actually used (after any clamping). |
offset |
How many matching rows were skipped before this page. |
has_more |
true if requesting the next page (same limit, offset advanced by limit) would return more results. |
To page through results: keep the same filters and limit, and increase offset by limit each time until has_more is false.
Errors
| Code | Meaning |
|---|---|
200 |
Success — request completed, data returned. (A legitimate zero-result search is still a 200.) |
201 |
Created — resource was successfully created |
400 |
Bad request — missing required field, invalid value, or an unsupported search type |
401 |
Unauthorized — missing or invalid API key/session, or a scoped key attempting an action outside its scope (identical response to an invalid key) |
404 |
Not found — resource doesn't exist, or belongs to another company (the two are never distinguished, by design) |
409 |
Version conflict on tasks&action=update when a supplied version is stale — response includes the current row |
Error responses always return a JSON object with an error field:
{ "error": "Missing required field: title" }
Machine-Readable Reference
The full, detailed machine contract — every field, every schema, every response shape — lives in one place: the OpenAPI specification. This guide is the operational usage guide; the OpenAPI document is the authoritative detailed schema.
| OpenAPI spec | https://hub.justwhatsneeded.com/docs/openapi.yaml |
| Format | OpenAPI 3.0.3, YAML |
| Markdown / AI knowledge version of this guide | https://justwhatsneeded.com/api-guide.md |
| Structured data (HTML version only) | <script type="application/ld+json"> in the page <head> (Schema.org TechArticle / WebAPI) |
The OpenAPI specification currently documents Projects, Tasks, Ideas, and Search in full schema detail. If you need a resource's exact field types, enums, or nullability and this guide doesn't spell it out, the OpenAPI document is authoritative.
Keeping This Guide Current
This guide has one canonical source — docs/api-guide-source.md in the jwn-hub repository — generated into api-guide.html, api-guide.md, and cross-checked against docs/openapi.yaml by scripts/generate-api-guide.php. Nobody edits html/api-guide.html or html/api-guide.md directly; both are build output and get overwritten the next time the generator runs.
Whenever a public Hub API capability, route, parameter, response contract, authentication rule, scope, module entitlement, or error behavior changes: edit docs/api-guide-source.md, run php scripts/generate-api-guide.php, and update docs/openapi.yaml — all before that work is considered complete. This is a standing Hub engineering requirement (Hub Engineering Standard, Section 8), not a one-off effort tied to any single change.
© 2026 Just What's Needed · Back to Hub overview