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

MethodPathDescription
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

MethodPathDescription
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

MethodPathDescription
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

MethodPathDescription
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

MethodPathDescription
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

MethodPathDescription
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.

MethodPathDescription
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

MethodPathDescription
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

MethodPathDescription
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:

SuffixHTTPMeaningRecovery
*_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:

EndpointFree-tier codePaid-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):

HTTPCodeCause
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.

MethodPathDescription
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.

MethodPathDescription
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.

MethodPathDescription
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.