Documentation
Learn how Header works and explore the API for building integrations.
What is Header
In the era of AI, the expectations on knowledge workers are expanding across domains, but the time to learn hasn't kept up. Current tools help you consume more content — none help you actually improve how you or your agents work.
Header is a continuous learning system that turns information into behavior change. You define what you want to learn, connect the sources you trust, and Header synthesizes relevant insights from hundreds of sources into focused briefings you can act on.
The Learning Loop
- Goal — Define what you want to learn
- Synthesize — Header surfaces relevant insights from your sources
- Experiment — Act on insights, observe results
- Persist — What works becomes behavior change, reshaping your goals, your habits, and your agents
Topics
What are Topics?
A Topic is the top-level container in Header. It groups together the source groups you want to follow and the goals that define how you want to analyze that content.
- Name — a short label (e.g., "AI Research", "Startup News")
- Source groups — the content feeds attached to this topic
- Goals — one or more goals, each with its own lens on the same sources
- Public / Curated — topics can be shared publicly for others to subscribe to or fork
When you create a topic, a default goal is created automatically with it. You can add more goals later to analyze the same sources from different angles. For example, an "AI Research" topic could have one goal tracking inference performance and another tracking safety research — same sources, different perspectives.
Public topics appear in the catalog for other users to discover. They can subscribe to receive the default goal's briefings, or fork the topic to create their own copy with a custom goal.
Goals
What are Goals?
A Goal lives inside a topic and describes what you want to learn from its sources, written in plain language. Each goal has a description, optional keywords for fine-tuning relevance, and relevance questions that guide the AI when filtering content.
- Description — natural language explanation of your objective (e.g., "Advances in local LLM inference on consumer hardware")
- Keywords — terms that signal relevance (e.g.,
llama.cpp,quantization,GGUF) - Relevance questions — questions the AI uses to decide if content matters (e.g., "Does this discuss running models locally?")
- Scheduling — set a frequency (daily, weekly, etc.) to receive briefings by email automatically
Goals are the unit that owns briefings. When a briefing is generated, Header pulls content from the goal's source groups, filters it through your relevance criteria, and synthesizes a focused digest. A single topic can have multiple goals, each producing its own briefings from a different perspective on the same content.
Sources
Content Streams
Sources are the content feeds that Header ingests. Each source has a type that determines how content is fetched and processed:
- RSS — any valid RSS or Atom feed URL. Header polls the feed and processes new entries.
- YouTube — a YouTube channel URL. Header fetches video metadata and transcripts for analysis.
- Newsletter — a dedicated email address. Forward newsletters to it and Header parses them into content.
- Reddit — a subreddit URL. Header ingests posts and comments from the community.
Sources are shared resources — any user can add a source, and all users benefit from the cached content. You organize sources into source groups to control which content flows into which goals.
Source Groups
Organizing Sources
Source groups let you organize sources into logical collections. They support nesting (groups within groups) for hierarchical organization, and can be cloned by other users to share curated collections.
- Flat groups — a simple list of sources (e.g., "AI Research Blogs")
- Nested groups — groups containing other groups (e.g., "Tech" containing "AI Blogs" and "Dev Tools")
- Cloning — copy a public group into your own account to customize it
OPML Import
OPML (Outline Processor Markup Language) is a standard XML format used by RSS readers to export and import feed subscriptions. If you use an RSS reader like Feedly, Inoreader, or NetNewsWire, you can export your feeds as an OPML file and import them directly into Header.
During import, each feed in the OPML file becomes a source, and the folder structure is preserved as source groups. Upload the file through the web interface or use the Imports API.
FreshRSS Import
If you run a self-hosted FreshRSS instance, Header can connect to it directly via the Google Reader-compatible API. Provide your FreshRSS instance URL, username, and API password, and Header will import your feeds and categories.
You can import into a single group or create one group per FreshRSS category. Validate your credentials first with the validate endpoint before running the full import.
Briefings
Cross-Source Synthesis
Briefings are where Header's value compounds. Unlike feed-level summaries or one-off research tools, Header synthesizes across all of your sources to surface patterns and insights you wouldn't find reading any single feed. For each goal, the system:
- Collects recent content from all connected source groups
- Filters entries through your relevance criteria and keywords
- Uses LLMs to analyze, rank, and synthesize the most relevant content
- Produces a structured digest with key developments, takeaways, and source links
Briefings can be generated on-demand via the API or web interface, or scheduled to run automatically (daily, weekly, etc.) with results delivered by email. You can also make briefings public and share them with a link, or subscribe to other users' public topics to receive their briefings.
API Overview
Base URL
https://joinheader.com/api/v2
Authentication
All API requests require a Bearer token in the Authorization header.
Create an API key from your account settings,
then include it in every request:
Authorization: Bearer hdr_sk_...
OpenAPI Schema
The full machine-readable API specification is available at
/api/v2/openapi.json.
Use it with any OpenAPI-compatible client or code generator.
Skill Version
/api/v2/skill/version
is a public, unauthenticated endpoint that reports the latest published
header-briefing skill version.
The skill polls it to self-update — no token required.
For AI agents: Fetch /api/v2/openapi.json to get the
complete request/response schemas, parameter types, and error codes. The tables below
provide a human-friendly overview.
Integration pattern: Agents can plug into Header's
learning loop programmatically.
Use GET /topics/dashboard to discover a user's topics,
POST /goals/{goal_id}/briefings to trigger briefing generation,
and GET /briefings/{id} to retrieve synthesized insights.
Agents can act on briefing content — extracting key developments,
feeding them into downstream workflows, or updating goals based on
what the user has learned. This closes the loop.
Sources
| Method | Path | Description |
|---|---|---|
| GET | /sources | List all sources |
| GET | /sources/{source_id} | Get a single source |
| POST | /sources | Create a new source |
| PUT | /sources/{source_id} | Update a source (owner only) |
| DELETE | /sources/{source_id} | Delete a source (owner only) |
| GET | /sources/{source_id}/entries | Get cached content entries (paginated) |
| POST | /sources/{source_id}/refresh | Trigger content re-ingestion (async) |
| POST | /sources/{source_id}/validate | Validate source URL is reachable |
| POST | /sources/preview | Detect type, verify URL (RSS/Reddit/YouTube), and return display name |
| POST | /sources/batch | Create multiple sources (optionally add to a group) |
| POST | /sources/recommend | Ask Header to propose verified sources for a goal (returns signed envelope) |
| POST | /sources/recommend/commit | Commit selected recommendations into a source group (idempotent) |
Create Source
// POST /api/v2/sources
// type: "rss" | "youtube" | "newsletter" | "reddit" | "web" | "api"
{
"name": "Hacker News",
"url": "https://news.ycombinator.com/rss",
"type": "rss"
}
// Response
{
"id": "src_abc123",
"name": "Hacker News",
"type": "rss",
"url": "https://news.ycombinator.com/rss",
"is_active": true,
"is_private": false,
"metadata": {},
"owner_id": "user_xyz",
"last_ingested": null,
"created_at": "2026-03-19T12:00:00Z"
}
Preview Source
// POST /api/v2/sources/preview
// Detect source type and fetch name before creating
{
"url": "https://news.ycombinator.com/rss"
}
// Response
{
"url": "https://news.ycombinator.com/rss",
"name": "Hacker News",
"type": "rss"
}
Batch Create Sources
// POST /api/v2/sources/batch
// Create multiple sources at once, optionally adding them to a group
{
"sources": [
{ "url": "https://news.ycombinator.com/rss", "type": "rss", "name": "Hacker News" },
{ "url": "https://feeds.arstechnica.com/arstechnica/index", "type": "rss", "name": "Ars Technica" }
],
"source_group_id": "sg_abc123" // optional — auto-adds created sources to this group
}
// Response
{
"results": [
{ "url": "https://news.ycombinator.com/rss", "source_id": "src_abc123" },
{ "url": "https://feeds.arstechnica.com/...", "source_id": "src_def456" }
]
}
Recommend + Commit Sources
// Two-phase flow: ask Header to propose sources for a goal, then commit
// the selected ones into a canonical per-topic group.
// POST /api/v2/sources/recommend
{
"topic_name": "AI Research",
"goal_description": "Track notable AI research papers and industry announcements.",
"existing_source_group_ids": ["sg_abc123"] // skip sources already in these groups
}
// Response — verified cards + a signed envelope to echo back on /commit
{
"recommendation_id": "rec_01H...",
"cards": [
{
"name": "Import AI",
"url": "https://jack-clark.net/",
"feed_url": "https://jack-clark.net/feed/",
"source_type": "rss",
"why_relevant": "Weekly AI research summary from a longtime practitioner.",
"signals": { "posting_cadence": "weekly", "last_post_date": "2026-04-18" },
"quality_score": 0.87
}
],
"envelope": "eyJ...",
"signature": "3f9c..."
}
// POST /api/v2/sources/recommend/commit
// Echo the envelope + signature from /recommend and the URLs you want.
{
"envelope": "eyJ...",
"signature": "3f9c...",
"selected_card_urls": ["https://jack-clark.net/"]
}
// Response — sources are attached to a canonical "{topic} - Recommended sources" group
{
"group_id": "sg_xyz789",
"created_source_ids": ["src_new456"]
}
Source Groups
| Method | Path | Description |
|---|---|---|
| GET | /source-groups | List public and user's own groups |
| GET | /source-groups/{group_id} | Get a single group with members |
| POST | /source-groups | Create a new source group |
| PUT | /source-groups/{group_id} | Update a source group (owner only) |
| DELETE | /source-groups/{group_id} | Delete a source group (owner only) |
| POST | /source-groups/{group_id}/clone | Clone a public group into your account |
| GET | /source-groups/{group_id}/members | List group members |
| POST | /source-groups/{group_id}/members | Add a member (source or nested group) |
| DELETE | /source-groups/{group_id}/members/{member_id} | Remove a member from group |
| POST | /source-groups/{group_id}/members/bulk | Add multiple members at once |
| POST | /source-groups/{group_id}/members/bulk-remove | Remove multiple members at once |
| GET | /source-groups/{group_id}/entries | Get content entries for all sources in group (paginated) |
Add Member
// POST /api/v2/source-groups/{group_id}/members
// member_id can be a source ID or another group ID (for nesting)
{
"member_id": "src_abc123"
}
// Response: 204 No Content
Bulk Add Members
// POST /api/v2/source-groups/{group_id}/members/bulk
{
"member_ids": ["src_abc123", "src_def456", "sg_nested789"]
}
// Response
{
"added_count": 2,
"already_members": 1,
"added_ids": ["src_abc123", "src_def456"],
"skipped_cycles": []
}
List Members
// GET /api/v2/source-groups/{group_id}/members
// Response
{
"members": [
{ "id": "src_abc123", "name": "Hacker News", "type": "rss" },
{ "id": "sg_nested789", "name": "AI Sources", "type": "source_group" }
],
"total": 2,
"expanded_source_count": 5 // total leaf sources including nested groups
}
Topics
| Method | Path | Description |
|---|---|---|
| GET | /topics/dashboard | Get your dashboard (custom + subscribed topics) |
| GET | /topics/catalog | Browse public topics |
| POST | /topics | Create a topic with a default goal and source groups |
| GET | /topics/{topic_id} | Get a single topic |
| POST | /topics/{topic_id}/fork | Fork a public topic with your own goal |
| POST | /topics/{topic_id}/subscribe | Subscribe to a public topic |
| DELETE | /topics/{topic_id}/subscribe | Unsubscribe from a topic |
Create Topic
// POST /api/v2/topics
// Creates a topic with a default goal and triggers the first briefing.
{
"name": "AI Research",
"source_group_ids": ["sg_abc123"],
"goal_description": "Advances in local LLM inference on consumer hardware",
"keywords": ["llama.cpp", "quantization"]
}
// Response:
{
"topic": { /* TopicResponse */ },
"first_briefing_id": "br_xyz789"
}
Fork Topic
// POST /api/v2/topics/{topic_id}/fork
// Creates your own copy of a public topic with a custom goal.
{
"goal_name": "Safety Research Focus",
"goal_description": "Track AI safety papers and alignment research",
"keywords": ["alignment", "RLHF", "interpretability"]
}
Goals
| Method | Path | Description |
|---|---|---|
| GET | /goals | List your goals |
| GET | /goals/{goal_id} | Get a single goal |
| PUT | /goals/{goal_id} | Update a goal (owner only) |
| DELETE | /goals/{goal_id} | Delete a goal (owner only) |
| POST | /goals/{goal_id}/source-groups | Link a source group to goal |
| DELETE | /goals/{goal_id}/source-groups/{sg_id} | Unlink a source group from goal |
| GET | /goals/{goal_id}/feed | Get preprocessed feed entries (paginated) |
Goals are created automatically when you create or fork a topic. Use POST /topics to create a topic with a default goal, or POST /topics/{id}/fork to fork a public topic with a custom goal. Use PUT /goals/{id} to update an existing goal's description, keywords, or schedule.
Update Goal
// PUT /api/v2/goals/{goal_id}
// All fields are optional — only include what you want to change.
{
"description": "Updated focus on transformer architectures",
"keywords": ["transformers", "attention"],
"schedule_enabled": true,
"schedule_frequency_days": 3,
"schedule_hour": 8,
"source_group_ids": ["sg_abc123", "sg_def456"]
}
Link Source Group to Goal
// POST /api/v2/goals/{goal_id}/source-groups
{
"source_group_id": "sg_abc123"
}
// Response: 204 No Content
Briefings
| Method | Path | Description |
|---|---|---|
| GET | /goals/{goal_id}/briefings | List briefings for a goal |
| POST | /goals/{goal_id}/briefings | Generate a new briefing (async) |
| GET | /briefings/{briefing_id} | Get a single briefing |
| DELETE | /briefings/{briefing_id} | Delete a briefing (owner only) |
| PUT | /briefings/{briefing_id}/sharing | Toggle briefing public/private |
Generate Briefing
// POST /api/v2/goals/{goal_id}/briefings
// All fields are optional. Omit body entirely for defaults.
{
"max_entries": 5, // optional: limit number of source entries
"max_age_days": 7 // optional: only include entries from last N days
}
// Response: 201 — poll GET /briefings/{id} until status is COMPLETED
{
"id": "br_xyz789",
"goal_id": "goal_abc123",
"status": "IN_PROGRESS",
"created_at": "2026-02-26T12:00:00Z",
"estimated_duration_seconds": 90, // static estimate fixed at create time — does NOT count down; compute remaining as estimate - (now - created_at)
"source_count": 12 // sources the estimate was based on (both null on briefings predating this field)
}
Toggle Briefing Sharing
// PUT /api/v2/briefings/{briefing_id}/sharing
{
"is_public": true
}
// Response
{
"briefing_id": "br_xyz789",
"is_public": true,
"share_url": "https://joinheader.com/briefings/br_xyz789"
}
Imports
| Method | Path | Description |
|---|---|---|
| POST | /imports/opml | Import sources from OPML file (multipart upload, max 10 MB) |
| POST | /imports/freshrss/validate | Validate FreshRSS connection credentials |
| POST | /imports/freshrss | Import sources from a FreshRSS instance |
Import OPML
// POST /api/v2/imports/opml
// Content-Type: multipart/form-data
// Fields:
// file: .opml or .xml file (max 10 MB)
// source_group_id: (optional) add imported sources to this group
// new_group_name: (optional) create a new group for imported sources
// Response
{
"imported_count": 12,
"failed_count": 1,
"total_feeds": 13,
"source_ids": ["src_abc123", "..."],
"failures": [{ "title": "Bad Feed", "url": "...", "error": "..." }],
"source_group_id": "sg_abc123"
}
Import FreshRSS
// POST /api/v2/imports/freshrss/validate
{
"url": "https://freshrss.example.com",
"username": "admin",
"api_password": "your-api-password"
}
// POST /api/v2/imports/freshrss
{
"url": "https://freshrss.example.com",
"username": "admin",
"api_password": "your-api-password",
"category_mode": "single_group", // "single_group" | "per_category"
"group_name": "My FreshRSS Feeds" // optional: name for the new group
}
Subscriptions
Subscribe to public topics or individual goals to receive their briefings. Topic-level subscriptions are available under Topics. Goal-level subscriptions below allow more granular control.
| Method | Path | Description |
|---|---|---|
| POST | /goals/{goal_id}/subscribe | Subscribe to a public goal |
| DELETE | /goals/{goal_id}/subscribe | Unsubscribe from a goal |
| GET | /goals/{goal_id}/subscription | Get subscription status and subscriber count |
| GET | /subscriptions | List all goals you are subscribed to |
Preferences
| Method | Path | Description |
|---|---|---|
| GET | /preferences | Get your preferences (returns defaults if not set) |
| PUT | /preferences | Update your preferences |
Update Preferences
// PUT /api/v2/preferences
{
"briefing_length": "long" // "standard" | "long"
}
Billing
| Method | Path | Description |
|---|---|---|
| GET | /billing/subscription | Get current subscription status (tier, trial state, flip-acknowledgment flag) |
| POST | /billing/trial/start | Start the 15-day Pro trial. One per email, lifetime. |
| POST | /billing/create-checkout | Create a Polar checkout session |
| POST | /billing/portal | Create customer portal session |
| POST | /billing/acknowledge-tier-flip | Dismiss the post-trial / post-Pro-cancel acknowledgment modal |
GET /billing/subscription response
// Free user, trial unused — eligible to start a trial
{
"status": "free",
"tier": "free", // "free" | "paid" | "poweruser"
"trial_started_at": null,
"trial_ends_at": null,
"trial_used": false, // lifetime "trial consumed" flag
"trial_active": false, // true while inside the 15-day window
"can_start_trial": true, // gates the /trial/start CTA
"needs_tier_flip_acknowledgment": false,
"tier_flip_kind": null, // "trial_expired" | "pro_lapsed" | null
"can_upgrade": true,
"can_manage": false
}
POST /billing/trial/start
// Empty request body. Auth: Bearer <clerk-jwt|hdr_sk_...>
// 200 — trial started; same response shape as GET /subscription
{
"status": "free",
"tier": "paid",
"trial_started_at": "2026-05-08T19:00:00Z",
"trial_ends_at": "2026-05-23T19:00:00Z",
"trial_used": true,
"trial_active": true,
"can_start_trial": false,
"needs_tier_flip_acknowledgment": false,
"tier_flip_kind": null,
"can_upgrade": true,
"can_manage": false
}
// 409 — trial already consumed (one per email, lifetime)
{
"detail": {
"error_code": "TRIAL_ALREADY_USED",
"message": "You've already used your free trial."
}
}
// 409 — caller is already on a paid tier
{
"detail": {
"error_code": "ALREADY_PAID",
"message": "You're already on a paid plan."
}
}
Tier gates & error contract
Pro-feature endpoints reject free-tier callers with a typed JSON error body so an agent or SDK can branch programmatically and recover without parsing prose.
The shape on every tier-gate error is the same:
{
"detail": {
"error_code": "TOPIC_LIMIT_FREE",
"message": "Creating custom topics is a Pro feature."
}
}
Two suffixes, one per failure mode:
| Suffix | HTTP | Meaning | Recovery |
|---|---|---|---|
*_FREE |
403 | Caller is on the free tier; the action is Pro-only. | Ask the user whether they want to proceed with the free trial (only if can_start_trial=true) or upgrade directly to Pro. On "trial" choice → call POST /billing/trial/start and retry the original request. On "upgrade" choice → call POST /billing/create-checkout and direct them to the returned URL. Never auto-pick a path. |
*_QUOTA |
429 | Caller is on a paid tier but at their cap (10 topics, 7 manual briefings per rolling 24h). | Wait for the cap to reset, delete a topic to make room, or email [email protected] with the use case (no higher public tier exists). |
Concrete error codes you may encounter:
| Endpoint | Free-tier code | Paid-cap code |
|---|---|---|
| POST /topics | TOPIC_LIMIT_FREE |
TOPIC_LIMIT_QUOTA |
| POST /goals/{id}/briefings | MANUAL_BRIEFING_FREE |
MANUAL_BRIEFING_QUOTA |
| PUT /topics/{id} | EDIT_FREE |
— |
| PUT /goals/{id} | EDIT_FREE |
— |
Trial-start endpoint codes (POST /billing/trial/start):
| HTTP | Code | Cause |
|---|---|---|
| 200 | — | Trial started successfully. |
| 409 | TRIAL_ALREADY_USED |
The lifetime trial has been consumed. Either it expired or it's still active. |
| 409 | ALREADY_PAID |
The caller is already on a paid tier; no trial available. |
Recovery flow for agents: on any 403 with a *_FREE code,
fetch GET /billing/subscription and ask the user whether
they want to proceed with the free trial (if can_start_trial=true) or
upgrade directly to Pro. Both options are valid; never auto-pick. On a trial choice,
call POST /billing/trial/start and retry the original request; the same
Authorization header then unblocks all *_FREE endpoints for
the next 15 days. On an upgrade choice, call POST /billing/create-checkout
and direct the user to the returned URL. After a trial expires or a Pro subscription
lapses, schedules on the user's custom topics are paused server-side; on the next
upgrade they auto-resume — no manual re-enable required.
API Keys
API keys provide programmatic access to the Header API.
Keys have a scope that controls
which HTTP methods are allowed: read (GET only) or full (all methods).
API key requests share your account's usage limits. For example, if your plan allows 2 briefings per day and you generate one from the website, an agent using your API key can only generate 1 more before hitting the cap.
| Method | Path | Description |
|---|---|---|
| POST | /api-keys | Create a new API key (409 if at limit) |
| GET | /api-keys | List your API keys (secrets are never returned) |
| DELETE | /api-keys/{key_id} | Revoke an API key |
Create API Key
// POST /api/v2/api-keys
{
"name": "My Integration",
"scope": "read" // "read" (GET only) | "full" (all methods)
}
// Response: 201 — save the secret, it is only shown once
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "My Integration",
"scope": "read",
"key_prefix": "hdr_sk_abc1",
"secret": "hdr_sk_abc123...full_key_here",
"created_at": "2026-03-19T12:00:00Z"
}
Authentication with API keys: Use the Authorization header
with your key: Authorization: Bearer hdr_sk_...
Telemetry
The opt-in usage-telemetry ingest for the Header CLI skill. Auth is optional —
a valid API key attributes events to your account; without one they are anonymous.
The endpoint accepts a JSON array of usage events (max 100), dedupes on a
client-supplied event_id, and returns 202.
Usage metadata only: no code, file paths, repo names, or client IP are stored.
| Method | Path | Description |
|---|---|---|
| POST | /telemetry/events | Ingest a batch of opt-in skill usage events (auth optional) |
Experiments
Experiments are A/B tests your coding agent runs locally with the Header skill — comparing harness changes or model swaps and proving which actually wins on cost. The skill syncs each experiment to your account on every lifecycle change (defined → run → analyzed → merged), and they show up on your Experiments page. Available to every account — not a Pro feature.
Privacy: prompt bodies, override file contents, agent logs, and source code are never sent.
Tasks carry only a title, a sha256, and a byte count; arms carry an overrides
path only.
| Method | Path | Description |
|---|---|---|
| POST | /experiments | Sync one experiment (the skill calls this; idempotent — 201 created / 200 updated) |
| GET | /experiments | List your experiments, newest first. Filters: ?repo=, ?status=, ?since= |
| GET | /experiments/{id} | Get one experiment with its arms, tasks, and A/B result |
Sync an experiment
// POST /api/v2/experiments — re-sent on every edit; idempotent on client_key
{
"v": 1,
"client_key": "<installation_id>:<experiment_id>", // upsert key
"submitted_at": "2026-05-28T21:44:36Z",
"experiment": {
"kind": "model-swap", // harness-change | model-swap | generic
"description": "Opus vs Sonnet on triage",
"status": "analyzed" // defined | run | analyzed | merged
},
"result": null // the A/B result object once status ≥ analyzed
}
// Response: 201 created (200 on a later update). The client surfaces "url".
{
"id": "exp_srv_abc123",
"client_key": "<installation_id>:<experiment_id>",
"url": "https://joinheader.com/experiments/exp_srv_abc123",
"status": "stored"
}
You don't call this by hand. Install the Header skill in your coding agent and it syncs experiments for you. Sync is best-effort and never blocks your work.