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:

This guide does not, by itself, do any of the following:

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:

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:

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

Contents

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" }

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