# Senso — full documentation > The context layer for AI agents. Every documentation page with a markdown source > follows in full. Pages rendered from code are listed at the end, with their URLs. --- # Agent Skills Source: https://docs.senso.ai/docs/agent-skills Install the Senso skills so your agent can run the Verification Loop, keep your team's shared context, and work with your knowledge base. ## What are agent skills? Skills are instructions your agent loads to use the Senso CLI. You say what you want in plain words; your agent runs the commands, and stops for the decisions only you can make. New to Senso? [Connect Your Agent](/docs/connect-your-agent) installs the CLI and the skills and signs you in, from one prompt. ## Before you install **Use an agent that can run terminal commands**, such as Claude Code, Codex or Cursor. GitHub Copilot, Gemini CLI and Cline are supported too. You also need the [Senso CLI](/docs/senso-cli), installed and signed in. ## Install the skills ```bash senso skills install quickstart verification-loop-setup verification-loop \ shared-context-setup shared-context context-layer gap-report generate-verify publish --global ``` **Install all nine.** They hand work to each other by name, so a flow stops at the first skill that is missing. - `--global` makes them available in every project. Leave it off to install into the current project only, for example `.claude/skills/` for Claude Code, where they can be committed with your code. - `--agent claude`, `cursor`, `codex`, `copilot`, `gemini` or `cline` installs for one agent. Without it, the skills install for every supported agent on your machine. **Start a fresh agent session after installing.** Agents load skills when a session starts. ## The skills Three skills are where you start. The rest are steps they use, and you can also call on them directly. | Skill | What it does | Start it by saying | |-------|--------------|--------------------| | `quickstart` | Says what Senso is, and takes you to the Verification Loop or Shared Context. | "Get me started with Senso." | | `verification-loop` | Finds an AI question in your industry where a competitor is named and you are not, answers it from your documents, and publishes a checked page. See [Verification Loop](/docs/use-cases/verification-loop). | "Let's go through the Verification Loop." | | `shared-context` | Settles the questions your team's agents could not answer, and saves decisions so the next person starts from them. See [Shared Context](/docs/shared-context). | "Let's go through Shared Context." | | `verification-loop-setup` | One-time setup for the loop: your material, your industry, your brand name, and the shape and voice of your pages. | Runs on its own the first time, or "Set my industry." | | `shared-context-setup` | Adds a first document when your knowledge base is empty. | Runs on its own the first time. | | `context-layer` | Saves, finds, organizes and hands off documents, and imports your website. | "Save this to Senso." "What did we decide about pricing?" | | `gap-report` | Shows what your knowledge base could not answer or back up, and records what was done about it. | "What are our gaps?" | | `generate-verify` | Drafts content from your knowledge base, and checks each claim against it. | "Draft an article for this question." "Is this accurate?" | | `publish` | Publishes an approved draft where AI models can cite it, and takes it down again. | "Publish this." "Unpublish it." | ## Manage installed skills | Command | What it does | |---------|--------------| | `senso skills list` | List the skills installed in this project. Add `--global` for global ones. | | `senso skills list-available` | List the skills you can install. | | `senso skills remove ` | Remove a skill. Add `--global` to remove a global install. | To update the skills, run the install command again. ## Next steps - [Connect Your Agent](/docs/connect-your-agent) — install everything and sign in, from one prompt - [Verification Loop](/docs/use-cases/verification-loop) — what the loop does, step by step - [Shared Context](/docs/shared-context) — how your team builds on each other's work - [CLI Reference](/docs/cli-reference) — every command your agent can run --- # AI Visibility Analytics Source: https://docs.senso.ai/docs/analytics What AI models said about you — mention rate, share of voice, and the three citation metric families, with the raw counts behind every number. Your prompts run against AI models on a schedule. These endpoints report **what those answers said about you** — how often you were named, who was named alongside you, and which pages got cited. Everything under `/org/analytics` is **read-only** and scoped to the organization the key belongs to. The org is never a path or query parameter, so there is no cross-org read to get wrong. ```bash curl "https://apiv2.senso.ai/api/v1/org/analytics/summary" \ -H "X-API-Key: $SENSO_API_KEY" ``` ## The shape of every response Aggregate endpoints return the same five blocks, in the same order: | Block | What it holds | |-------|---------------| | `window` | The effective date range: `from`, `to`, `days`, and `latest_data_day` | | `totals` | Every raw count — all numerators **and** all denominators | | `metrics` | Every ratio derived from `totals`, each as `{ value, display }` | | `data_quality` | A `low` / `medium` / `high` confidence flag from the answered-run count | | `notes` + `definitions` | Prose caveats and one-line metric definitions, generated per response | A ratio is **`null` whenever its denominator is zero**. Null means "not measured" — it is never a silent `0%` that reads like a measured result. ## The metrics ### Mention rate **Mention rate** is the share of answered runs in which your brand was named. ``` mention_rate = mentioned_count / answered_count ``` An **answer** (or run) is one execution of one prompt against one model in one location on one day. `answered_count` counts the non-empty answers; runs that returned nothing are excluded, so an outage does not count against you. Mention rate is **answer grain**: an answer that names you three times still counts once. If you want occurrences, use `mention_total`. ### Share of voice **Share of Voice** is your share of every brand mention the models made. It is instance grain: it counts every occurrence of a brand name, not answers, so a brand named twice in one answer counts twice. ``` share_of_voice = mention_total / brand_mention_total ``` `brand_mention_total` is mention instances across **every brand the models named** — your tracked competitors, brands you have never heard of, all of them. This is the same Share of Voice the Senso app reports on your dashboard, and the two are expected to match exactly. There is one Share of Voice metric and one denominator. Every endpoint that reports it — summary, mentions, prompts — computes it the same way, so the number never changes meaning as you move between them. Prompt rows additionally carry a latest-answer figure alongside the window one; see below. `totals` also carries `tracked_mention_total` — mention instances across you plus your tracked competitors. It is deliberately **not** the Share of Voice denominator. For an organization with no tracked competitors configured, `tracked_mention_total` equals `mention_total`, so dividing by it reports 100% whenever you were mentioned at all. Reconciled against production data in July 2026, three of four sampled organizations tracked no competitors, so that denominator would have returned 100.0% where the app showed 37 to 40 percent. Even when it is not degenerate, that denominator depends on your configuration: add a competitor and it grows, so the number falls without anything changing in the answers. `brand_mention_total` does not move when you edit your competitor list. The raw count is still returned, so you can compute the head-to-head view yourself: ``` head_to_head = mention_total / tracked_mention_total # your own math, not a Senso metric ``` Do not call that result Share of Voice, and do not compare it with the number on the dashboard. This API does not enumerate the untracked brands by name. They exist only inside `brand_mention_total`. ### Two share-of-voice figures on a prompt row Rows from `/org/analytics/prompts` carry share of voice twice. The two are not redundant — they answer different questions over different spans: | Field | Question it answers | Built from | |-------|---------------------|-----------| | `share_of_voice` | Across the whole window, what was your share of brand mentions for this prompt? | every answer in the window | | `latest.share_of_voice` | Right now, what is your share of brand mentions for this prompt? | the newest answer per model and location | The Senso app's prompt table shows the latest figure. So when you are reconciling a single prompt against the app, compare `latest.share_of_voice`. Comparing the window aggregate will disagree, and it will be right to disagree, because it measures a different span. Use the window figure for trends and for ranking prompts against each other: it is built from many answers, so one unusual run moves it far less than it moves the latest figure. > The same distinction applies to the rest of the `latest` block. `latest.answer_count` and `latest.mentioned_count` describe the newest answers only, not the window. ### Average rank ``` avg_rank = rank_sum / mentioned_count ``` The mean ordinal position of your brand across the answers that mentioned it — 1 means named first. **Lower is better**, and it ships as `{ "value": 2.4, "display": "#2.4" }` so nobody reads it as a percentage. `avg_rank` is `null` when you were never mentioned. That is "never ranked", not "ranked last". ### Sentiment Counts only: `positive + neutral + negative = mentioned_count`. There is deliberately no averaged sentiment score — averaging categorical labels invents a number. ### Citation tiers Every cited URL is classified into exactly one tier by its domain: | Tier (stored value) | Product name | Meaning | |---------------------|--------------|---------| | `primary` | Owned | One of your own websites | | `tracked` | Tracked | One of your configured tracked sources | | `secondary` | External | Any other, third-party domain | The tiers **partition citations** — each cited URL is exactly one tier — but they do **not** partition answers. One answer can carry citations of two or three tiers at once. > **"External" means the `secondary` tier only.** The older Partner API uses "external" to mean everything you do not own (tracked and secondary combined). The two are not comparable — an SDK external field and a Partner external field count different things. ### The two denominators: D and S This is the one thing worth reading twice. Every citation metric divides by one of exactly two denominators: ``` D = cited_run_count answers with at least 1 citation (any tier) → Rate, Coverage S = cited_total citation instances (every cited URL) → Share ``` `D` is answer grain: an answer citing five of your pages contributes **1**. `S` is instance grain: that same answer contributes **5**. A metric built on `D` and a metric built on `S` are never comparable — never average them, never sum them, never chart them on a shared axis without labels. Both denominators are returned explicitly on every citation response, so you can always check which one a number used. ### Family 1 — Citation Rate (answer grain, per tier) *Of the answers that cited anything, what share cited this tier?* ``` primary_citation_rate = primary_cited_run_count / D tracked_citation_rate = tracked_cited_run_count / D external_citation_rate = external_cited_run_count / D ``` The three rates are **independent, not a partition**. An answer citing both an owned page and a third-party page counts toward both numerators, so the three can sum past 100%. That is expected and correct. ### Family 2 — Citation Coverage (answer grain, per source) *Of the answers that cited anything, what share cited THIS domain or THIS page?* ``` citation_coverage(source) = (distinct answers citing this source) / D ``` Citation Coverage is Citation Rate's per-source sibling: same denominator `D`, narrower numerator. It appears on `/citations/domains` and `/citations/pages`. Because the numerator counts **distinct answers**, coverage is always at most 100%. A tier rate is **not** the sum of its sources' coverages — two sources cited in the same answer would double-count it. ### Family 3 — Citation Share (instance grain, per tier or per source) *Of all the citation instances we saw, what fraction went to this tier or this source?* ``` primary_citation_share = primary_cited_total / S tracked_citation_share = tracked_cited_total / S external_citation_share = external_cited_total / S citation_share(source) = (citation instances for this source) / S ``` The three tier shares **do** partition citations and must sum to exactly 100%. If they do not, that is a producer bug, not rounding — and the API says so in `notes[]` when it detects the mismatch. Per-source shares also sum to 100% across **all** sources in the window — not across the page of results you happen to be looking at. ### Rate and Share disagree, and that is the insight Rate and Coverage count **distinct answers**. Share counts **individual cited URLs**. Take a 30-day window: | Count | Value | |-------|-------| | `cited_run_count` (D) | 100 | | `primary_cited_run_count` | 40 | | `cited_total` (S) | 800 | | `primary_cited_total` | 60 | | Metric | Math | Result | |--------|------|--------| | `primary_citation_rate` | 40 / 100 | 40.0% | | `primary_citation_share` | 60 / 800 | 7.5% | | `citations_per_answer` | 800 / 100 | 8.00 | Both numbers are correct and they describe different things. You were cited in **40% of the answers that cited anything** — good visibility. But you were only **7.5% of the citations inside those answers** — every answer that cites you cites roughly eight sources, and seven of them are not yours. High rate with low share means: *you get cited, but you are outnumbered inside every answer.* That is a content-volume problem, not a discoverability problem. Reversing them — low rate, high share — means the opposite: when you show up you dominate the answer, but you rarely show up. ### Citations per answer ``` citations_per_answer = S / D ``` An **intensity**, not a rate: how densely answers cite. It is unbounded above and never carries a percent sign. `4.2` means the average citing answer cited 4.2 URLs. ## Counts ship with every ratio Every payload carries the additive raw counts — numerators **and** denominators — next to the computed ratio. This is deliberate: - **You can check our math.** Every ratio in `metrics` is derivable from the numbers in `totals`. - **You can recombine windows.** Counts are additive; ratios are not. Sum the counts across periods, then divide once. - **You can build a metric we did not ship.** If you want "answers that cited you as a share of all answered runs", divide `primary_cited_run_count` by `answered_count` yourself — just do not call it a Citation Rate, because it is not on `D`. Recomputing the headline metric from a `/summary` response: ```python import os, requests KEY = os.environ["SENSO_API_KEY"] BASE = "https://apiv2.senso.ai/api/v1" HEADERS = {"X-API-Key": KEY} data = requests.get(f"{BASE}/org/analytics/summary", headers=HEADERS).json() t = data["totals"] mention_rate = t["mentioned_count"] / t["answered_count"] # ÷ answered_count primary_rate = t["primary_cited_run_count"] / t["cited_run_count"] # ÷ D primary_share = t["primary_cited_total"] / t["cited_total"] # ÷ S print(f"mention rate {mention_rate:.1%} (API: {data['metrics']['mention_rate']['display']})") print(f"Citation Rate {primary_rate:.1%} (API: {data['metrics']['primary_citation_rate']['display']})") print(f"Citation Share {primary_share:.1%} (API: {data['metrics']['primary_citation_share']['display']})") ``` ```bash curl "https://apiv2.senso.ai/api/v1/org/analytics/summary" \ -H "X-API-Key: $SENSO_API_KEY" ``` Guard each division: any of those denominators can be zero, which is exactly why the API returns `null` rather than a number. ## Notes, definitions, and data quality These endpoints are built for **agents acting for a human** as much as for developers reading a spec, so every response explains itself. **`notes[]`** is an array of prose caveats generated for *that* response — not boilerplate. They tell you things the numbers cannot, for example: - that the window defaulted to the 30 days ending on your most recent day with data, rather than today - that `D = 0`, so every citation metric is `null` rather than `0%` - that part of your window predates the 396-day rollup retention limit and was pruned - that the per-tier instance totals do not sum to `cited_total`, so tier shares in this window are unreliable - that no brand mentions were recorded at all, so `share_of_voice` is `null` rather than `0%` **`definitions{}`** maps each metric present in that response to its one-line canonical definition, naming the denominator. It is projected from the same source as `GET /org/analytics/glossary`, so a definition can never drift between endpoints. **`data_quality`** is a deterministic flag from the answered-run count over the window: `low` below 100, `medium` below 400, `high` otherwise. It ships with `answered_count` and a `reasons[]` array. Low does not mean the number is wrong — it means a single run moves it a lot. If you are surfacing these numbers through an agent or an LLM, **pass `notes[]` and `definitions{}` through to the model and let it surface them.** They exist precisely so a summary does not invent a denominator. ## Answer text: latest only `latest_answers` and `GET /org/analytics/answers/latest` return **the single most recent answer per prompt, model, and location** — with the full response text, its citations, and its competitor mentions. Historical answer **text is not retained**. History exists only as the daily additive counts in the series endpoints. So: - Do not treat the latest answers as a sample of your window — they are a snapshot of right now. - Each answer carries its own `run_at`, and those can differ from row to row. - Each answer's `citations` list is capped at 50 entries by the rollup pipeline. - `competitor_mentions` covers your **tracked** competitors only. Untracked brands are counted in `brand_mention_total` but are never enumerated by name. ### `from` and `to` on `/answers/latest` filter collection time, not history `GET /org/analytics/answers/latest` accepts `from` and `to` (`YYYY-MM-DD`, same validation as everywhere else: `from` on or before `to`, 365-day maximum span). They filter on **`run_at` — when each stored answer was collected**. Both are optional, and unlike the window-scoped endpoints there is no 30-day default here: omit them and you get every stored latest answer. They do **not** turn this endpoint into a historical feed. It still returns only the newest answer per prompt, model, and location, so narrowing the window **hides** the combinations whose latest answer falls outside it. It never returns an older answer in place of the newest one, because older answer text is not stored. > Read it as "show me only the combinations refreshed in this period", never as "show me what the models said in this period". A worked example from a live organization: 2,392 answers with no window, 2,388 for the last four days (the four that had not been re-run recently drop out), and **0** for a window in the year 2000 — an empty result, not the whole snapshot. ```bash curl -G "https://apiv2.senso.ai/api/v1/org/analytics/answers/latest" \ -H "X-API-Key: $SENSO_API_KEY" \ --data-urlencode "from=2026-07-24" \ --data-urlencode "to=2026-07-28" ``` For what the models said over a period, use `/org/analytics/mentions` or `/org/analytics/citations` — those read the daily rollups and are genuinely historical. ## The window Window-scoped endpoints take `from` and `to` as `YYYY-MM-DD`, both optional, up to 365 days apart. Leave them off and the window defaults to the **30 days ending on the most recent day that has data for your active model and location filter** — not today. This matters: monitoring runs on a schedule, so anchoring on today would make every unparameterized request look empty between runs. The day used is echoed back as `window.latest_data_day`, and the defaulting is called out in `notes[]`. Daily rollups are pruned after 396 days, so windows reaching further back are silently truncated by retention. `/answers/latest` takes the same two parameters with the same validation, but they mean something different there — they filter on each answer's `run_at` and cannot return history. See **`from` and `to` on `/answers/latest`** above. ## Endpoint reference All paths are relative to `https://apiv2.senso.ai/api/v1` and all are `GET`. | Path | What it answers | Key parameters | |------|-----------------|----------------| | `/org/analytics/glossary` | What does each metric mean, and what is its denominator? | none | | `/org/analytics/filters` | Which models, locations, prompt types, tags and competitors actually have data? | none | | `/org/analytics/summary` | Where do we stand, and how does that compare with the previous equal-length window? | `from`, `to`, `models`, `location`, `prompt_type`, `tag` | | `/org/analytics/mentions` | How has visibility moved over time — mentions, share of voice, rank, sentiment? | shared filters, `group_by` (`day` or `week`) | | `/org/analytics/citations` | How has citation performance moved over time, at both grains and in both metric families? | shared filters, `group_by` | | `/org/analytics/citations/domains` | Which domains do the models cite, and how much of that is ours? | shared filters, `tier`, `domain_contains`, `sort`, `limit`, `offset` | | `/org/analytics/citations/pages` | Which exact URLs get cited, and which prompts drive them? | shared filters, `tier`, `domain`, `domain_contains`, `url_contains`, `sort`, `limit`, `offset` | | `/org/analytics/prompts` | Which prompts are we winning, and which are we invisible on? | shared filters, `search`, `sort`, `order`, `limit`, `offset` | | `/org/analytics/prompts/{promptId}` | One prompt end to end: its metric history plus its latest full answers | shared filters, `include_answers` | | `/org/analytics/answers/latest` | What did the models actually say, verbatim? | `from`, `to` (on `run_at`), `models`, `location`, `prompt_type`, `tag`, `mentioned`, `cited`, `citation_tier`, `limit`, `offset` | The **shared filters** accepted by every window-scoped endpoint: | Parameter | Values | Notes | |-----------|--------|-------| | `from`, `to` | `YYYY-MM-DD` | Both optional, 365 days maximum span, `from` on or before `to` | | `models` | comma-separated model ids | Use the ids from `/org/analytics/filters` | | `location` or `locations` | comma-separated locations | Exact match, case sensitive, e.g. `US`, `US/California`. Both spellings are the same filter | | `prompt_type` | `awareness`, `consideration`, `evaluation`, `decision` | The canonical funnel stages | | `tag` | tag name | Does not apply to the two citation-source endpoints | **`location` and `locations` are the same parameter.** The plural is an alias, and it exists for a specific reason: an unrecognized filter name is not an error, it is simply no filter — so a caller who guessed the plural would have received *more* data than they asked for, silently, with no way to tell. Both names take a comma-separated list of exact location codes. Values are case-sensitive: `US/California` matches, `us/california` does not. Use `/org/analytics/filters` to see the codes your organization actually has data for. ### Which filters each endpoint takes Verified against production, endpoint by endpoint: | Endpoint | `models` | `location(s)` | `prompt_type` | `from`/`to` | `tag` | |----------|----------|---------------|---------------|-------------|-------| | `/glossary`, `/filters` | — | — | — | — | — | | `/summary` | yes | yes | yes | yes | yes | | `/mentions` | yes | yes | yes | yes | yes | | `/citations` | yes | yes | yes | yes | yes | | `/citations/domains` | yes | yes | yes | yes | **no** | | `/citations/pages` | yes | yes | yes | yes | **no** | | `/prompts` | yes | yes | yes | yes | yes | | `/prompts/{promptId}` | yes | yes | — | yes | — | | `/answers/latest` | yes | yes | yes | yes, on `run_at` | yes | Three things to read off that table: - **`tag` does not narrow the citation-source endpoints.** The domain and webpage rollups have no prompt grain, so there is nothing for a prompt tag to match. Passing `tag` to `/citations/domains` or `/citations/pages` is not an error and not a silent empty result — you get the whole cited-source landscape, covering all prompts. Both endpoints say so in `notes[]` when you pass a tag. If you need tagged prompts only, get the tag's citation picture from `/citations` or `/prompts`, which are prompt grain. - **`/glossary` and `/filters` take no filters at all.** They describe the vocabulary and the available filter values; they report no metrics. - **`prompt_type` and `tag` are redundant on `/prompts/{promptId}`.** The path already names one prompt, so they can only exclude it entirely. Paging defaults to `limit=50` (`limit=25` on `/answers/latest`) and is capped at 100. Sorting on the cited-source endpoints is `sort=citations` (default) or `sort=coverage`; on `/prompts` it is `mention_rate` (default), `share_of_voice`, `citations`, `answered`, or `text`, with `order=asc` or `order=desc`. ## Examples ### Read the glossary once Have your agent call this before it quotes any number, so it never invents a denominator. ```bash curl "https://apiv2.senso.ai/api/v1/org/analytics/glossary" \ -H "X-API-Key: $SENSO_API_KEY" ``` Each entry carries a `metric`, its `definition`, its `denominator`, and a `gotcha`: ```json { "entries": [ { "metric": "primary_citation_rate", "definition": "Of the answers that cited anything, the share that cited one of your own domains (tier: primary / Owned).", "denominator": "D = cited_run_count", "gotcha": "The three tier rates are independent, not a partition — an answer citing both an owned and an external page counts toward both, so they can sum past 100%. That is expected." } ] } ``` ### The one-call dashboard ```bash curl -G "https://apiv2.senso.ai/api/v1/org/analytics/summary" \ -H "X-API-Key: $SENSO_API_KEY" \ --data-urlencode "from=2026-06-01" \ --data-urlencode "to=2026-06-30" \ --data-urlencode "models=chatgpt,perplexity" ``` ```json { "window": { "from": "2026-06-01", "to": "2026-06-30", "days": 30, "latest_data_day": "2026-06-30" }, "totals": { "run_count": 1240, "answered_count": 1198, "mentioned_count": 431, "mention_total": 602, "tracked_mention_total": 2410, "brand_mention_total": 5180, "cited_run_count": 100, "primary_cited_run_count": 40, "cited_total": 800, "primary_cited_total": 60 }, "metrics": { "mention_rate": { "value": 0.3597, "display": "36.0%" }, "share_of_voice": { "value": 0.1162, "display": "11.6%" }, "avg_rank": { "value": 2.4, "display": "#2.4" }, "primary_citation_rate": { "value": 0.40, "display": "40.0%" }, "primary_citation_share": { "value": 0.075, "display": "7.5%" }, "citations_per_answer": { "value": 8.0, "display": "8.00 citations per cited answer" } }, "previous_window": { "window": { "from": "2026-05-02", "to": "2026-05-31", "days": 30 } }, "deltas": { "mention_rate": { "delta": 0.041, "direction": "improved", "display": "+4.1 pts" } }, "data_quality": { "level": "high", "answered_count": 1198, "reasons": [] }, "notes": ["..."], "definitions": { "mention_rate": "Share of answered runs in which your brand was named." } } ``` `deltas` compares against the equal-length window immediately before yours and is expressed in **absolute percentage points**. A delta is `null` when either window lacked the denominator to compute the metric, so "no data" never reads as "fell to zero". ### Find the pages the models cite instead of yours ```python import os, requests KEY = os.environ["SENSO_API_KEY"] BASE = "https://apiv2.senso.ai/api/v1" HEADERS = {"X-API-Key": KEY} resp = requests.get(f"{BASE}/org/analytics/citations/pages", headers=HEADERS, params={ "tier": "secondary", # third-party pages only "sort": "coverage", # rank by Citation Coverage, not raw volume "limit": 10, }) data = resp.json() d = data["denominators"]["cited_run_count"] for p in data["pages"]: cov = p["citation_coverage"] print(f'{cov["display"] if cov else "—":>7} {p["url"]}') for prompt in p["top_prompts"]: print(f' driven by: {prompt["prompt_text"]}') print(f"\nCitation Coverage denominator D = {d} answers with at least one citation") ``` ```bash curl -G "https://apiv2.senso.ai/api/v1/org/analytics/citations/pages" \ -H "X-API-Key: $SENSO_API_KEY" \ --data-urlencode "tier=secondary" \ --data-urlencode "sort=coverage" \ --data-urlencode "limit=10" ``` `top_prompts` lists at most five prompts per page — the biggest drivers, not an exhaustive list. URLs are canonical: tracking parameters are stripped and the address normalized when the rollup is written, so they may not match your published links character for character. ### Find the prompts where you are invisible ```bash curl -G "https://apiv2.senso.ai/api/v1/org/analytics/prompts" \ -H "X-API-Key: $SENSO_API_KEY" \ --data-urlencode "sort=mention_rate" \ --data-urlencode "order=asc" \ --data-urlencode "limit=20" ``` Each row carries that prompt's own counts, so read `mention_rate` alongside `answered_count` — a prompt answered twice and a prompt answered 200 times both report a rate. Prompts with **no** denominator (never answered, or no brand mentions at all) sort to the front of an ascending sort: they are unmeasured, not zero. ### Read what a model actually said ```bash curl -G "https://apiv2.senso.ai/api/v1/org/analytics/answers/latest" \ -H "X-API-Key: $SENSO_API_KEY" \ --data-urlencode "mentioned=false" \ --data-urlencode "cited=true" \ --data-urlencode "limit=5" ``` That combination — answers that cited sources but never named you — is usually the most actionable feed in the API. ## Common mistakes **Dividing by the wrong denominator.** `primary_cited_run_count / answered_count` is not a Citation Rate. Citation Rate and Citation Coverage divide by `D` (`cited_run_count`); Citation Share divides by `S` (`cited_total`). Mixing an answer-grain numerator with an instance-grain denominator produces a number that can exceed 100% for no interpretable reason. Both denominators are in every response — use them. **Treating `null` as zero.** A `null` ratio means the denominator was zero: no answered runs, no brand mentions, no citations at all. Charting it as `0%` turns "we did not measure this" into "we measured a failure". The same applies to `avg_rank`: `null` is "never mentioned", not "ranked last". **Averaging per-period rates.** The series points carry different denominators, so the mean of 30 daily mention rates is not the 30-day mention rate. Sum the counts across periods, then divide once. This is exactly why every series point ships its counts. **Reading `share_of_voice` as a head-to-head number.** Its denominator is `brand_mention_total` — every brand the models named, tracked or not — so it is your share of the whole field, not of a race against your configured competitors. It is comparable across organizations and across time, and it does not move when you edit your competitor list. If you divide by `tracked_mention_total` instead, that number is neither comparable across orgs nor equal to what the Senso app shows, and it reads 100% for any org that tracks no competitors. **Reading "external" as "not owned".** In these endpoints `external` is the `secondary` tier only — third-party domains. Tracked sources are their own tier. The older Partner API uses "external" for tracked and secondary combined, and the two are not comparable. **Summing the three tier rates and expecting 100%.** The rates are independent and can total more than 100%. It is the three tier *shares* that partition citations and sum to exactly 100%. --- # API Keys Source: https://docs.senso.ai/docs/api-keys How Senso API keys work, what a key can reach, and how to create, restrict, rotate and revoke one. Senso uses API keys to authenticate requests from your agents, scripts and integrations, and to decide which organization a request acts on. **Every key is secret and belongs to exactly one organization.** Create and manage keys on the [API Keys](/api-keys) page. If you use the CLI, `senso login` creates one for you. A request with a missing, wrong, expired or revoked key gets a `401`. ## Key types Senso has one kind of key. It always starts with `tgr_`. What differs is how you got it. | Key | How you get it | Expires | Access | |-----|----------------|---------|--------| | **API key** | Create it on the [API Keys](/api-keys) page | Never, unless you set an expiry date | Full, or restricted to parts of your knowledge base | | **CLI key** | Run [`senso login`](/docs/senso-cli) and approve it in your browser | After 7 days | Full | A CLI key is named after the device that asked for it, like `senso-cli my-laptop 2026-09-30`, so you can tell it apart on the API Keys page. ## What a key can do **A key has the full permissions of its organization**, whatever the role of the person who created it. It can ingest, search, generate and publish for that organization, and nothing outside it. **A key cannot manage keys.** Creating, changing, restricting and revoking keys all need a signed-in person on the API Keys page. That way a leaked or restricted key cannot create itself a more powerful one. **Knowledge base access is the one thing you can restrict.** A key with no restriction can read and write your whole knowledge base. You can limit it to chosen folders and documents, as a viewer or an editor of each. See Restrict a key, below. ## Send a key Send the key in the `X-API-Key` header on every request: ```bash curl "https://apiv2.senso.ai/api/v1/org/me" \ -H "X-API-Key: $SENSO_API_KEY" ``` ## Protect your keys Anyone holding an unrestricted key can read and change everything your organization has in Senso. - Keep keys on a server, in an environment variable or a secrets manager. Never put one in browser or mobile code. - Don't commit keys to source control, and don't paste them into email or chat. - Use a separate key for each integration or environment, so you can revoke one without breaking the others. - Restrict a key to the parts of your knowledge base it needs. - Set an expiry date on keys that only need to exist for a while. - Revoke keys you no longer use, and rotate keys when someone with access to them leaves. ## Manage your API keys **Only admins can manage keys.** The API Keys page shows for admins only. A custom role can be given API key permissions too. ### Create a key 1. On the [API Keys](/api-keys) page, click **+ New key**. 2. Enter a **Name**, for example `Production key`. 3. Optionally, pick an **Expiry** date. Leave it empty for a key that never expires. 4. Click **Create key**. 5. Copy the key and store it somewhere safe. > The key is shown only once. Senso stores a hash of it, not the key, so it cannot be shown again. If you lose a key, revoke it and create a new one. ### Restrict a key New keys have **Full access**. To limit one to part of your knowledge base: 1. On the API Keys page, click **Configure KB scope** on the key. 2. Choose the folders and documents the key can reach. 3. Click a role badge to switch between **Viewer** and **Editor**. 4. Save. The key now shows as **Restricted**. Removing every restriction gives the key full access again. ### Rotate a key 1. Create a new key with the same access. 2. Switch your integration to the new key. 3. Revoke the old key once nothing uses it. ### Revoke a key Click **Revoke** on the key. It stops working immediately and cannot be restored. **Delete** does the same thing and also removes the key from the list. ## Keys from the CLI `senso login` creates a CLI key through your browser, so you never have to copy or paste a key. You must be an administrator to go through this flow. The CLI then stores the key locally. If you are not an administrator, ask one for an API key and sign in with it instead: ```bash senso login --api-key ``` The CLI checks the key with Senso before storing it, and a key that is rejected is not stored. | | CLI key | |---|---| | **Lifetime** | 7 days. Run `senso logout` and then `senso login` to get a new one. | | **Access** | Full. The organization is the one the approving admin has selected. | | **Sign out** | `senso logout` revokes the key, then forgets it. | | **Your own key** | `senso login --api-key ` stores a key you created instead. `senso logout` forgets it but does not revoke it, since it may be in use elsewhere. | The CLI reads a key from `--api-key` first, then the `SENSO_API_KEY` environment variable, then its stored config. `senso whoami` shows which one it used. See [Senso CLI](/docs/senso-cli). ## When a key fails | Status | What it means | What to do | |--------|---------------|------------| | `401` | The key is missing, wrong, expired or revoked. | Check the header, or create a new key. | | `403` "This action requires user authentication" | You used a key for something only a signed-in person can do, like creating or revoking keys. | Do it on the [API Keys](/api-keys) page. | | `403` | A restricted key can see that folder or document, but as a viewer it cannot change it. | Make the key an editor there, or use another key. | | `404` | The key is restricted, and what you asked for is outside its scope. Senso answers as if it does not exist. | Widen the key's scope, or use another key. | Every other status is on [Errors](/docs/errors). ## Next steps - [Send your first API request](/docs/quickstart) — Use a key in a real request, start to finish - [Permissions](/docs/permissions) — Roles, key scopes and knowledge base access - [Senso CLI](/docs/senso-cli) — Sign in with `senso login` - [Errors](/docs/errors) — Every status the API returns --- # Blog-to-Social Converter Source: https://docs.senso.ai/docs/blog-to-social Ingest a blog post as a raw source, compile it into your knowledge base, and generate verified social content. # Blog-to-Social Converter Scrape a blog post, ingest it as a raw source into your knowledge base, define content types (your output templates), then generate verified content for each one. The output is grounded in your ingested source and comes back as Markdown ready for publishing. ## Prerequisites - A Senso API key (create one on the [API Keys](/api-keys) page) - A Firecrawl API key (get one at [firecrawl.dev](https://firecrawl.dev)) - Python 3.10+ ```bash export SENSO_API_KEY="YOUR_API_KEY" export FIRECRAWL_KEY="YOUR_FIRECRAWL_KEY" pip install requests firecrawl-py rich ``` ## How it works 1. **Scrape** — Firecrawl extracts Markdown from the blog post (your raw source) 2. **Ingest** — `POST /org/kb/upload` to get a presigned S3 URL, PUT the raw source, then poll its KB node roughly every 15 seconds until compiled 3. **Set up brand kit** — `PUT /org/brand-kit` to define your brand voice, persona, and writing rules. The `guidelines` object accepts a fixed set of keys (any other key returns a 400): `brand_name`, `brand_domain`, `brand_description`, `voice_and_tone`, `author_persona`, and `global_writing_rules`. 4. **Create content types** — `POST /org/content-types` to define output templates (e.g. "Tweet Thread", "LinkedIn Post"). The `config` object accepts a fixed set of keys (any other key returns a 400): `template`, `template_spec`, `cta_text`, `cta_destination`, and `writing_rules`. 5. **Create a prompt** — `POST /org/prompts` to define the question driving generation. Prompts have a `type`: `awareness`, `consideration`, `decision`, or `evaluation`. 6. **Generate** — `POST /org/content-generation/sample` with the `geo_question_id` (prompt) and `content_type_id`. The API returns a `sample_job_id`; poll `/org/content-generation/sample-jobs/{sample_job_id}` until completion. The completed job returns verified Markdown, SEO title, URL slug, and editorial metadata. ## Full script ```python import hashlib, os, sys, time, requests from firecrawl import FirecrawlApp SENSO_API_KEY = os.environ["SENSO_API_KEY"] FIRECRAWL_KEY = os.environ["FIRECRAWL_KEY"] BASE = "https://apiv2.senso.ai/api/v1" HEADERS = {"X-API-Key": SENSO_API_KEY, "Content-Type": "application/json"} blog_url = sys.argv[1] if len(sys.argv) > 1 else "https://your-blog.com/great-article" # ── 1. Scrape the blog post ────────────────────────────────── print(f"Scraping {blog_url}...") fc = FirecrawlApp(api_key=FIRECRAWL_KEY) page = fc.scrape_url(blog_url, params={"formats": ["markdown"]}) markdown = page.get("markdown", "") title = page.get("metadata", {}).get("title", "Blog Post") print(f"Got: {title} ({len(markdown)} chars)") # ── 2. Ingest into Senso ───────────────────────────────────── file_bytes = markdown.encode("utf-8") filename = f"{title[:50].replace(' ', '-').lower()}.md" resp = requests.post(f"{BASE}/org/kb/upload", headers=HEADERS, json={ "files": [{ "filename": filename, "file_size_bytes": len(file_bytes), "content_type": "text/markdown", "content_hash_md5": hashlib.md5(file_bytes).hexdigest(), }] }) result = resp.json()["results"][0] content_id = result["content_id"] # Upload to S3. Set Content-Type to the SAME value declared above — # the presigned URL signs it, so a mismatch is rejected by S3. requests.put(result["upload_url"], data=file_bytes, headers={"Content-Type": "text/markdown"}) print(f"Uploaded -> {content_id}") # /org/kb/upload returns a CONTENT id, but the /org/kb/nodes/{id}/... endpoints # need the NODE id — resolve it by matching the content id in the KB tree. nodes = requests.get(f"{BASE}/org/kb/find", headers=HEADERS, params={"q": filename}).json()["nodes"] node_id = next(n["kb_node_id"] for n in nodes if n["content_id"] == content_id) # Poll the KB node until ingestion finishes. (GET /org/content/{id} is for # generated content; a KB node's status is read via /org/kb/nodes/{id}.) while True: node = requests.get(f"{BASE}/org/kb/nodes/{node_id}", headers=HEADERS).json() if node.get("content", {}).get("processing_status") == "complete": break time.sleep(15) print("Processing complete.") # ── 3. Set up brand kit ────────────────────────────────────── # The brand kit defines your org's voice and writing rules. # `guidelines` accepts only the keys below — any other key is # rejected with a 400. The content engine uses them during generation. requests.put(f"{BASE}/org/brand-kit", headers=HEADERS, json={ "guidelines": { "brand_name": "Acme Corp", "brand_domain": "acme.com", "brand_description": "Developer tools for API infrastructure", "voice_and_tone": "Clear, direct, technically confident. Avoid jargon when a simpler word works.", "author_persona": "A senior engineer who has shipped at scale", "global_writing_rules": [ "Use active voice", "Lead with the insight, not the setup", "No em-dashes or semicolons", ], } }) print("Brand kit saved.") # ── 4. Create content types ────────────────────────────────── # Content types are reusable output templates. `config` accepts only # these keys (any other key is rejected with a 400): `template`, # `template_spec`, `cta_text`, `cta_destination`, and `writing_rules`. content_types = [ { "name": "Tweet Thread", "config": { "template": "A thread of 3-5 tweets. First tweet hooks the reader. Last tweet has the CTA. Each tweet ≤280 chars.", "cta_text": "Read the full post", "cta_destination": blog_url, "writing_rules": [ "One idea per tweet", "Use line breaks for readability", "No hashtags in the thread body", ], }, }, { "name": "LinkedIn Post", "config": { "template": "A single LinkedIn post under 1300 characters. Open with a bold statement. End with a question to drive comments.", "cta_text": "Link in comments", "cta_destination": blog_url, "writing_rules": [ "Professional but not stiff", "Use short paragraphs (1-2 sentences)", "Include one concrete metric or example", ], }, }, ] type_ids = {} for ct in content_types: resp = requests.post(f"{BASE}/org/content-types", headers=HEADERS, json=ct) if resp.status_code == 201: data = resp.json() type_ids[ct["name"]] = data["content_type_id"] print(f"Created content type: {ct['name']} -> {data['content_type_id']}") elif resp.status_code == 409: # Already exists — list and find it existing = requests.get(f"{BASE}/org/content-types", headers=HEADERS).json() for t in existing.get("content_types", []): if t["name"] == ct["name"]: type_ids[ct["name"]] = t["content_type_id"] print(f"Content type exists: {ct['name']} -> {t['content_type_id']}") break # ── 5. Create a prompt ──────────────────────────────────────── # A prompt is the question that drives generation. # Types: awareness, consideration, decision, evaluation resp = requests.post(f"{BASE}/org/prompts", headers=HEADERS, json={ "question_text": f"What are the key takeaways from: {title}?", "type": "awareness", }) prompt = resp.json() prompt_id = prompt["prompt_id"] print(f"Created prompt: {prompt_id}") # ── 6. Generate content for each type ───────────────────────── for name, type_id in type_ids.items(): print(f"\nGenerating {name}...") resp = requests.post( f"{BASE}/org/content-generation/sample", headers=HEADERS, json={ "geo_question_id": prompt_id, "content_type_id": type_id, }, ) if resp.status_code == 202: job = resp.json() while True: status = requests.get( f"{BASE}/org/content-generation/sample-jobs/{job['sample_job_id']}", headers=HEADERS, ).json() if status["status"] in {"completed", "failed", "expired"}: break time.sleep(2) if status["status"] != "completed": print(f" Error: {status.get('error', {}).get('message', status['status'])}") continue gen = status["result"] print(f" Content ID: {gen['content_id']}") print(f" SEO Title: {gen['seo_title']}") print(f" URL Slug: {gen['url_slug']}") print(f" Status: {gen['editorial_status']}") print(f" ---") # Print first 300 chars of the generated markdown print(f" {gen['raw_markdown'][:300]}...") else: print(f" Error {resp.status_code}: {resp.text}") ``` ## Run it ```bash python blog_to_social.py https://your-blog.com/great-article ``` Output: ``` Scraping https://your-blog.com/great-article... Got: 10 Lessons from Scaling Our API (3842 chars) Uploaded -> a1b2c3d4-... Processing complete. Brand kit saved. Created content type: Tweet Thread -> e5f6a7b8-... Created content type: LinkedIn Post -> c9d0e1f2-... Created prompt: d3e4f5a6-... Generating Tweet Thread... Content ID: f7a8b9c0-... SEO Title: 10 Lessons from Scaling Our API - Thread URL Slug: 10-lessons-scaling-api-thread Status: draft --- 1/ We scaled our API from 100 to 10,000 requests/sec last year. Here are 10 hard-won lessons... Generating LinkedIn Post... Content ID: a0b1c2d3-... SEO Title: What We Learned Scaling to 10K RPS URL Slug: learned-scaling-10k-rps Status: draft --- After a year of scaling our API infrastructure, here are the insights that actually mattered... ``` ## Key API details **Brand kit** — org-wide voice and writing rules: ```json PUT /org/brand-kit { "guidelines": { "brand_name": "Acme Corp", "brand_domain": "acme.com", "brand_description": "Developer tools for API infrastructure", "voice_and_tone": "Clear, direct, technically confident.", "author_persona": "A senior engineer who has shipped at scale", "global_writing_rules": ["Use active voice", "Lead with the insight"] } } ``` Returns `brand_kit_id` (UUID), `org_id`, `guidelines`, `created_at`, `updated_at`. The `guidelines` object accepts only the keys shown above — any other key is rejected with a 400. (A `PATCH` must include at least one of them.) **Content types** — reusable output templates: ```json POST /org/content-types { "name": "Tweet Thread", "config": { "template": "A thread of 3-5 tweets...", "cta_text": "Read the full post", "cta_destination": "https://your-blog.com/article", "writing_rules": ["One idea per tweet"] } } ``` Returns `content_type_id` (UUID), `name`, `config`, `created_at`, `updated_at`. The `config` object accepts only `template`, `template_spec`, `cta_text`, `cta_destination`, and `writing_rules` (an array of strings) — any other key is rejected with a 400. **Prompts** — the questions that drive generation: ```json POST /org/prompts { "question_text": "What are the key takeaways?", "type": "awareness" } ``` Returns `prompt_id` (UUID), `text`, `type`. Types: `awareness`, `consideration`, `decision`, `evaluation`. **Content generation sample** — generate for one prompt + content type: ```json POST /org/content-generation/sample { "geo_question_id": "uuid", "content_type_id": "uuid" } ``` Returns a queued sample job: | Field | Description | |-------|-------------| | `sample_job_id` | UUID of the async sample generation job | | `status` | `queued`, `running`, `completed`, `failed`, or `expired` | Poll `GET /org/content-generation/sample-jobs/{sample_job_id}`. Completed jobs include `result`: | Field | Description | |-------|-------------| | `result.content_id` | UUID of the generated content item | | `result.version_id` | UUID of this version | | `result.version_num` | Version number (starts at 1) | | `result.raw_markdown` | The generated content as Markdown | | `result.seo_title` | SEO-optimized title | | `result.url_slug` | URL-safe slug | | `result.editorial_status` | `draft` (ready for review) | | `result.publish_status` | `skipped` unless you publish during generation | --- # CLI Reference Source: https://docs.senso.ai/docs/cli-reference Every Senso CLI command and flag, grouped by what you use it for. This is the full list of `senso` commands. To install the CLI and sign in, start with [Senso CLI](/docs/senso-cli). Every command also prints its own help with `senso --help`. ## Global flags These work on every command. | Flag | What it does | |------|--------------| | `--api-key ` | Use this key for one command, instead of the stored one | | `--base-url ` | Send requests to a different API address | | `--output ` | `plain` (default), `json` or `table` | | `--quiet` | Hide the banner and other non-essential output | | `--no-update-check` | Skip the daily check for a newer version | | `-v`, `--version` | Print the CLI version | | `-h`, `--help` | Print help for any command | Use `--output json` in scripts and agents. It prints the API's response as JSON and nothing else. ## Environment variables | Variable | What it does | |----------|--------------| | `SENSO_API_KEY` | The key to use, instead of the stored one | | `SENSO_BASE_URL` | A different API address, like `--base-url` | | `SENSO_CONFIG_DIR` | Where the CLI stores its config, instead of the default folder | | `SENSO_DEBUG=1` | Print every request and its status, with the key hidden | | `SENSO_GAP_SIGNALS=off` | Keep searches out of the gap report, like `--no-gap-signals` | | `SENSO_NO_UPDATE_CHECK=1` | Skip the daily version check, like `--no-update-check` | The CLI uses the key from `--api-key` first, then `SENSO_API_KEY`, then the stored config. `senso whoami` shows which one it used. ## Exit codes | Code | Meaning | |------|---------| | `0` | The command did what you asked | | `1` | Senso refused the request. The message says why | | `2` | The command was wrong: an unknown command, a missing argument or a bad flag | | `3` | No key, or the key was refused | | `4` | What you asked for does not exist | | `5` | The CLI could not reach Senso | ## Authentication Sign in, sign out, and check which organization you are using. | Command | What it does | |---------|--------------| | `senso login` | Sign this device in. Opens an approval page in your browser; an admin approves, and the CLI stores the key it gets. Does nothing if your stored key still works. | | `senso logout` | Forget the stored key. A key that `senso login` created is revoked first; a key you supplied is only forgotten. | | `senso whoami` | Show the organization you are signed in to, the key prefix, and where the key came from. | | Command | Flag | What it does | |---------|------|--------------| | `login` | `--complete` | Finish a login started earlier: wait for the browser approval and store the key. | | `login` | `--interactive` | Paste an existing API key at a prompt instead. Needs a terminal. | | `login` | `--device-name ` | How this device is labeled on the approval page. | | `login` | `--no-browser` | Do not try to open the approval page automatically. | ## Organization Your organization's profile, credits and industry. ### senso org | Command | What it does | |---------|--------------| | `senso org get` | Show the organization profile: name, slug, tier, websites, locations, models and schedule. | | `senso org update` | Update organization details. Only the fields you pass change, but `websites` and `locations` replace their whole list. | | `senso org set-industry ` | Set the industry your organization belongs to, from `senso industries list`. Replaces any previous choice. | | `senso org set-runs` | Turn every scheduled prompt run and content-generation run on or off. | | Command | Flag | What it does | |---------|------|--------------| | `org update` | `--data ` | JSON with any of `name`, `slug`, `logo_url`, `websites` (list of `{"url"}`), `locations` (list of `{"country_code", "region_name"}`). | | `org set-runs` | `--enabled ` | Set to true or false | ### senso credits | Command | What it does | |---------|--------------| | `senso credits balance` | Show the credits available and any spend limit. | | `senso credits history` | Show credit spend per day over a trailing window, oldest first, ending today. | | Command | Flag | What it does | |---------|------|--------------| | `credits history` | `--days ` | Length of the trailing window in days, 1-365 (default 30) | ### senso industries | Command | What it does | |---------|--------------| | `senso industries list` | List the public industry catalog, with each industry's prompt, model and location counts. | | `senso industries answers ` | Read the newest answer for each of your industry's prompts, per model and location, and whether it named your brand. | | `senso industries prompts ` | List the prompts an industry runs. Their IDs are what `import-prompts` and `generate industry-draft` take. | | `senso industries brands ` | Rank the brands named in an industry's answers over a window, by mentions. | | `senso industries brand ` | Everything about one brand in an industry, matched by name. | | `senso industries brand-by-id ` | The same as `brand`, looked up by the `brand_id` it returns. | | `senso industries domain ` | How often a domain, or a full URL with `--url`, was cited in an industry's answers. | | `senso industries import-prompts ` | Copy prompts from your own industry into your organization, with their run history. Prompts you already have are skipped. This activates the organization and starts its scheduled runs. | | Command | Flag | What it does | |---------|------|--------------| | `industries list` | `--search ` | Case-insensitive substring match against name or slug | | `industries list` | `--limit ` | Page size, 1-100 (default 50) | | `industries list` | `--offset ` | Number of industries to skip (default 0) | | `industries list` | `--sort ` | Sort order: name_asc, name_desc, created_asc, created_desc (default name_asc) | | `industries list` | `--live` | Only industries actively running — at least one model enabled and one active prompt | | `industries answers` | `--mentioned ` | Only answers that did (true) or did not (false) name your brand | | `industries answers` | `--models ` | Comma-separated model filter: gpt-4.1, chatgpt, perplexity, aioverview, gemini, linkup, claude-sonnet-4-6, grok | | `industries answers` | `--location ` | One location, e.g. US or US/California (default every location) | | `industries answers` | `--prompt-ids ` | Comma-separated prompt ids to restrict to | | `industries answers` | `--since ` | Only answers collected on or after this day, YYYY-MM-DD | | `industries answers` | `--include-empty` | Include answers where the model returned nothing | | `industries answers` | `--limit ` | Page size, 1-100 (default 25) | | `industries answers` | `--offset ` | Number of answers to skip (default 0) | | `industries prompts` | `--imported ` | Keep only the prompts you already have (true) or do not have yet (false). Omit for all. | | `industries prompts` | `--limit ` | Page size, 1-100 (default 50) | | `industries prompts` | `--offset ` | Number of prompts to skip (default 0) | | `industries brands` | `--from ` | Start of the window, YYYY-MM-DD (default 30 days ago) | | `industries brands` | `--to ` | End of the window, YYYY-MM-DD (default today) | | `industries brands` | `--models ` | Comma-separated model filter | | `industries brands` | `--location ` | One location, as the industry's runs record it: a country code such as US, or a country/region pair such as US/California | | `industries brands` | `--limit ` | Page size, 1-100 (default 100) | | `industries brands` | `--offset ` | Number of brands to skip (default 0) | | `industries brands` | `--no-canonicalize` | Do not merge spelling variants — raw per-spelling rows | | `industries brands` | `--rollup ` | Set to `parent` to fold sub-brands into their parent company | | `industries brands` | `--entity-type ` | Comma-separated types to keep: brand, regulator, publisher, government, generic_term, product_model, forum_social | | `industries brand` | `--from ` | Start of the window, YYYY-MM-DD (default 30 days ago) | | `industries brand` | `--to ` | End of the window, YYYY-MM-DD (default today) | | `industries brand` | `--models ` | Comma-separated model filter | | `industries brand` | `--location ` | One location, as the industry's runs record it: a country code such as US, or a country/region pair such as US/California | | `industries brand-by-id` | `--from ` | Start of the window, YYYY-MM-DD (default 30 days ago) | | `industries brand-by-id` | `--to ` | End of the window, YYYY-MM-DD (default today) | | `industries brand-by-id` | `--models ` | Comma-separated model filter | | `industries brand-by-id` | `--location ` | One location, as the industry's runs record it: a country code such as US, or a country/region pair such as US/California | | `industries domain` | `--from ` | Start of the window, YYYY-MM-DD (default 30 days ago) | | `industries domain` | `--to ` | End of the window, YYYY-MM-DD (default today) | | `industries domain` | `--models ` | Comma-separated model filter | | `industries domain` | `--location ` | One location, as the industry's runs record it: a country code such as US, or a country/region pair such as US/California | | `industries domain` | `--url ` | Look up this full URL instead of the bare domain | | `industries import-prompts` | `--prompt-ids ` | Comma-separated industry prompt ids, 1-100, no duplicates (from `senso industries prompts`) | ### senso history-imports | Command | What it does | |---------|--------------| | `senso history-imports list` | List the 50 most recent history-import jobs, newest first. | | `senso history-imports get ` | Show one history-import job, by the ID `industries import-prompts` returns. | ### senso run-config | Command | What it does | |---------|--------------| | `senso run-config models` | Show the AI models prompts are run against. | | `senso run-config set-models` | Replace the AI models prompts are run against. | | `senso run-config model-options` | List the model names `set-models` accepts. | | `senso run-config scheduler-models` | Show the provider and model pairs the scheduler runs. | | `senso run-config set-scheduler-models` | Replace the scheduler's provider and model pairs. | | `senso run-config schedule` | Show the days of the week prompts run (0 is Sunday). | | `senso run-config set-schedule` | Set the days of the week prompts run. | | Command | Flag | What it does | |---------|------|--------------| | `run-config set-models` | `--data ` | JSON `{"models": [...]}`, at least one name from `run-config model-options`. | | `run-config set-scheduler-models` | `--data ` | JSON `{"models": [...]}` of `provider/model` pairs. | | `run-config set-schedule` | `--data ` | JSON `{"schedule": [...]}`, days 0 (Sunday) to 6 (Saturday). | ## Members and access People, roles and API keys. ### senso members | Command | What it does | |---------|--------------| | `senso members list` | List organization members, with their names, emails and roles. | | Command | Flag | What it does | |---------|------|--------------| | `members list` | `--limit ` | Maximum members to return (max: 1000) | | `members list` | `--offset ` | Number of members to skip (for pagination) | | `members list` | `--search ` | Filter by name or email | | `members list` | `--sort ` | Sort order: name_asc, name_desc, email_asc, email_desc, created_asc, created_desc | ### senso users | Command | What it does | |---------|--------------| | `senso users list` | List the users in the organization, with their user IDs and roles. | | `senso users add` | Add an existing Senso user to the organization by user ID and role ID. | | `senso users get ` | Show one user's role and membership. | | `senso users update ` | Change a user's role in the organization. | | `senso users remove ` | Remove a user from the organization. Their Senso account is not deleted. | | `senso users set-current ` | Make this organization the user's active organization. | | `senso users invite` | Invite someone new by email. Creates their account and adds them with the role you choose. | | `senso users invite-existing` | Add an existing Senso user to the organization by email. | | Command | Flag | What it does | |---------|------|--------------| | `users list` | `--limit ` | Maximum number of users to return | | `users list` | `--offset ` | Number of users to skip (for pagination) | | `users add` | `--data ` | JSON with `user_id`, `role_id` and optional `is_current`. | | `users update` | `--data ` | JSON with `role_id` (required) and optional `is_current`. | | `users invite` | `--email ` | User's email address | | `users invite` | `--given-name ` | First name | | `users invite` | `--family-name ` | Last name | | `users invite` | `--role-id ` | Role to assign — resolve with `senso roles list` | | `users invite` | `--is-current` | Make this org the new user's current org | | `users invite-existing` | `--email ` | Email of an existing Senso user | | `users invite-existing` | `--role-id ` | Role to assign — resolve with `senso roles list` | | `users invite-existing` | `--is-current` | Make this org the user's current org | ### senso roles | Command | What it does | |---------|--------------| | `senso roles list` | List every role in the organization: the built-in admin, collaborator and viewer roles, and any custom ones. | ### senso permissions | Command | What it does | |---------|--------------| | `senso permissions list` | List every permission key, with its name, description and category. | ### senso api-keys | Command | What it does | |---------|--------------| | `senso api-keys list` | List the organization's API keys, with name, expiry and whether each is restricted. | | `senso api-keys get ` | Show one API key's details. The key itself is never shown again after it is created. | | `senso api-keys kb-permissions-get ` | Show the knowledge base restrictions on a key. An empty list means full access. | | Command | Flag | What it does | |---------|------|--------------| | `api-keys list` | `--limit ` | Maximum number of keys to return | | `api-keys list` | `--offset ` | Number of keys to skip (for pagination) | ## Knowledge base Add, organize and search the documents Senso answers from. ### senso kb | Command | What it does | |---------|--------------| | `senso kb root` | Show the knowledge base's root folder. | | `senso kb stats` | Count the documents and folders in the knowledge base. | | `senso kb my-files` | List the files and folders at the top of the knowledge base. | | `senso kb find` | Find files and folders by name. | | `senso kb sync-status` | Show whether recent moves and deletes are still being applied. Not an ingestion signal: use `kb get` for that. | | `senso kb get ` | Show a file or folder. For a document, `content.processing_status` says whether it is ready to search. | | `senso kb children ` | List what is inside a folder. | | `senso kb ancestors ` | List the folders above a file or folder, from the root down. | | `senso kb get-content ` | Show a document's text and metadata, or an earlier version with `--rev`. | | `senso kb download-url ` | Get a temporary download link for an uploaded file, or an earlier version with `--rev`. | | `senso kb create-folder` | Create a folder, at the top level or inside another folder. | | `senso kb rename ` | Rename a file or folder. | | `senso kb move ` | Move a file or folder into another folder. | | `senso kb delete ` | Delete a file or folder. | | `senso kb bulk-delete ` | Delete up to 100 files and folders at once. Folders take everything inside them. If any one cannot be deleted, none are. | | `senso kb create-raw` | Add a document from text or markdown. Senso tags it automatically once it is processed. | | `senso kb update-raw ` | Replace a text document's title and text, as a new version. | | `senso kb patch-raw ` | Update a text document's text, and optionally its title and summary, as a new version. | | `senso kb upload ` | Upload up to 10 files. Each is processed in the background; check with `kb get` before searching it. | | `senso kb update-file ` | Replace an uploaded file with a new version. | | `senso kb tags list ` | List the tags on a file or folder. | | `senso kb tags set ` | Replace all the tags on a file or folder. Names that do not exist yet are created. | | `senso kb tags add ` | Add one tag to a file or folder. A new name is created. | | `senso kb tags remove ` | Remove one tag from a file or folder. | | `senso kb permissions list ` | List who has access to a file or folder, with each grant's ID. | | `senso kb permissions add ` | Give a user or group viewer or editor access to a file or folder. | | `senso kb permissions update ` | Change a grant's role to viewer or editor. | | `senso kb permissions remove ` | Remove a grant. | | Command | Flag | What it does | |---------|------|--------------| | `kb my-files` | `--limit ` | Items per page, 1-50 (the API caps higher values at 50) (default 50) | | `kb my-files` | `--offset ` | Pagination offset (default 0) | | `kb my-files` | `--type ` | Only nodes of this type: folder, content | | `kb my-files` | `--status ` | Only documents in this ingestion state: pending, processing, complete, failed. Ignored with --type folder | | `kb my-files` | `--role ` | Only nodes where the caller holds this role: editor, viewer. Ignored for org-admin keys, which already reach everything | | `kb my-files` | `--sort-by ` | Sort by: name, updated_at, created_at, type, status, role | | `kb my-files` | `--sort-order ` | Sort direction: asc, desc | | `kb my-files` | `--tag-ids ` | Comma-separated tag IDs; only nodes carrying at least one of them | | `kb find` | `--query ` | Name search query | | `kb find` | `--limit ` | Items per page, 1-50 (the API caps higher values at 50) (default 20) | | `kb find` | `--offset ` | Pagination offset (default 0) | | `kb find` | `--type ` | Only nodes of this type: folder, content | | `kb find` | `--status ` | Only documents in this ingestion state: pending, processing, complete, failed. Ignored with --type folder | | `kb find` | `--role ` | Only nodes where the caller holds this role: editor, viewer. Ignored for org-admin keys, which already reach everything | | `kb find` | `--sort-by ` | Sort by: name, updated_at, created_at, type, status, role | | `kb find` | `--sort-order ` | Sort direction: asc, desc | | `kb find` | `--tag-ids ` | Comma-separated tag IDs; only nodes carrying at least one of them | | `kb children` | `--limit ` | Items per page, 1-50 (the API caps higher values at 50) (default 50) | | `kb children` | `--offset ` | Pagination offset (default 0) | | `kb children` | `--type ` | Only nodes of this type: folder, content | | `kb children` | `--status ` | Only documents in this ingestion state: pending, processing, complete, failed. Ignored with --type folder | | `kb children` | `--role ` | Only nodes where the caller holds this role: editor, viewer. Ignored for org-admin keys, which already reach everything | | `kb children` | `--sort-by ` | Sort by: name, updated_at, created_at, type, status, role | | `kb children` | `--sort-order ` | Sort direction: asc, desc | | `kb children` | `--tag-ids ` | Comma-separated tag IDs; only nodes carrying at least one of them | | `kb get-content` | `--rev ` | Retrieve a specific stored version of this content, by version number | | `kb download-url` | `--rev ` | Download a specific stored version of this file, by version number | | `kb create-folder` | `--name ` | Folder name | | `kb create-folder` | `--parent-id ` | Parent folder node ID (omit to create at root) | | `kb rename` | `--name ` | New name | | `kb move` | `--parent-id ` | Target parent folder node ID | | `kb create-raw` | `--data ` | JSON with `text` (required), `title`, `summary`, `kb_folder_node_id`. | | `kb update-raw` | `--data ` | JSON with `title` and `text` (both required), `summary`, `tag_ids`. `tag_ids` replaces all tags. | | `kb patch-raw` | `--data ` | JSON with `text`, `title`, `summary`, `tag_ids`. `tag_ids` replaces all tags. | | `kb upload` | `--folder-id ` | Parent folder node ID to place files in (omit for root) | | `kb tags set` | `--names ` | Comma-separated tag names (created if missing) | | `kb tags set` | `--ids ` | Comma-separated existing tag UUIDs | | `kb tags add` | `--name ` | Tag name (created if missing) | | `kb tags add` | `--id ` | Existing tag UUID | | `kb tags remove` | `--name ` | Tag name to detach | | `kb tags remove` | `--id ` | Existing tag UUID to detach | | `kb permissions add` | `--grantee-type ` | Who the grant is for: user, group | | `kb permissions add` | `--grantee-id ` | The user ID or group ID to grant access to | | `kb permissions add` | `--role ` | Access level to grant: viewer, editor | | `kb permissions update` | `--role ` | The new role: viewer, editor | ### senso ingest | Command | What it does | |---------|--------------| | `senso ingest upload ` | Upload up to 10 files. The same as `kb upload`. | | `senso ingest reprocess ` | Replace a document's file with a new one and process it again. | | Command | Flag | What it does | |---------|------|--------------| | `ingest upload` | `--folder-id ` | Destination folder ID (skip interactive prompt) | ### senso website-import | Command | What it does | |---------|--------------| | `senso website-import start` | Import pages from your organization's website into the knowledge base, and wait until it finishes. The site is the one on file in `senso org get`. | | `senso website-import status` | Show the import in progress and the last one that finished. | | Command | Flag | What it does | |---------|------|--------------| | `website-import start` | `--no-wait` | Return the accepted run immediately instead of polling until the import finishes. | ### senso search | Command | What it does | |---------|--------------| | `senso search ` | Ask a question. Returns an answer written from your knowledge base and the passages it used. | | `senso search context ` | Return the matching passages only, with no answer, to feed into your own model. | | `senso search content ` | Return the matching documents only, one row each, with the IDs to read them or narrow a later search. | | `senso search full ` | The same as `senso search`. | | `senso search stream ` | Stream the answer as it is written, then the passages it used. | | Command | Flag | What it does | |---------|------|--------------| | `search` | `--max-results ` | Maximum number of results (max: 20) (default 5) | | `search` | `--content-ids ` | Restrict search to specific content item IDs (space-separated UUIDs) | | `search` | `--require-scoped-ids` | Only return results from the specified --content-ids (omit to allow fallback to all content) | | `search` | `--no-gap-signals` | Keep this search out of the organization's gap report (sends X-Senso-Signals: off). Use it for probes, tests and monitors — a real question that finds nothing should be left eligible. The search still runs, costs credits and is recorded. Set SENSO_GAP_SIGNALS=off to do this for every search. | | `search context` | `--max-results ` | Maximum results (max: 20) (default 5) | | `search context` | `--content-ids ` | Restrict search to specific content item IDs (space-separated UUIDs) | | `search context` | `--require-scoped-ids` | Only return results from the specified --content-ids | | `search context` | `--no-gap-signals` | Keep this search out of the organization's gap report (sends X-Senso-Signals: off). Use it for probes, tests and monitors — a real question that finds nothing should be left eligible. The search still runs, costs credits and is recorded. Set SENSO_GAP_SIGNALS=off to do this for every search. | | `search content` | `--max-results ` | Maximum results (max: 20) (default 5) | | `search content` | `--content-ids ` | Restrict search to specific content item IDs (space-separated UUIDs) | | `search content` | `--require-scoped-ids` | Only return results from the specified --content-ids | | `search content` | `--no-gap-signals` | Keep this search out of the organization's gap report (sends X-Senso-Signals: off). Use it for probes, tests and monitors — a real question that finds nothing should be left eligible. The search still runs, costs credits and is recorded. Set SENSO_GAP_SIGNALS=off to do this for every search. | | `search full` | `--max-results ` | Maximum results (max: 20) (default 5) | | `search full` | `--content-ids ` | Restrict search to specific content item IDs (space-separated UUIDs) | | `search full` | `--require-scoped-ids` | Only return results from the specified --content-ids | | `search full` | `--no-gap-signals` | Keep this search out of the organization's gap report (sends X-Senso-Signals: off). Use it for probes, tests and monitors — a real question that finds nothing should be left eligible. The search still runs, costs credits and is recorded. Set SENSO_GAP_SIGNALS=off to do this for every search. | | `search stream` | `--max-results ` | Maximum results (max: 20) (default 5) | | `search stream` | `--content-ids ` | Restrict search to specific content item IDs (space-separated UUIDs) | | `search stream` | `--require-scoped-ids` | Only return results from the specified --content-ids | | `search stream` | `--no-gap-signals` | Keep this search out of the organization's gap report (sends X-Senso-Signals: off). Use it for probes, tests and monitors — a real question that finds nothing should be left eligible. The search still runs, costs credits and is recorded. Set SENSO_GAP_SIGNALS=off to do this for every search. | ### senso tags | Command | What it does | |---------|--------------| | `senso tags list` | List every tag in the organization. | | `senso tags create` | Create a tag. Names are unique, ignoring case. | | `senso tags get ` | Show a tag and how many prompts and documents use it. | | `senso tags update ` | Rename a tag. Everything already tagged keeps it. | | `senso tags delete ` | Delete a tag and remove it from everything it was on. | | Command | Flag | What it does | |---------|------|--------------| | `tags list` | `--counts` | Include prompt/content usage counts | | `tags create` | `--name ` | Tag name | | `tags update` | `--name ` | New tag name | ## Brand and context What every piece of generated content inherits. ### senso brand-kit | Command | What it does | |---------|--------------| | `senso brand-kit get` | Show the brand kit: name, domain, description, voice and tone, author persona and writing rules. | | `senso brand-kit set` | Replace the whole brand kit. Any field you leave out is removed. | | `senso brand-kit patch` | Change some brand kit fields and keep the rest. `global_writing_rules` is replaced as a whole. | | Command | Flag | What it does | |---------|------|--------------| | `brand-kit set` | `--data ` | JSON `{"guidelines": {...}}` with any of `brand_name`, `brand_domain`, `brand_description`, `voice_and_tone`, `author_persona`, `global_writing_rules`. | | `brand-kit patch` | `--data ` | JSON `{"guidelines": {...}}` with at least one of the brand kit fields. | ### senso content-types | Command | What it does | |---------|--------------| | `senso content-types list` | List the organization's content types. | | `senso content-types create` | Create a content type from a name and a markdown template. | | `senso content-types get ` | Show a content type and its configuration. | | `senso content-types update ` | Replace a content type's name and configuration. | | `senso content-types patch ` | Change some of a content type's fields and keep the rest. | | `senso content-types delete ` | Delete a content type. | | Command | Flag | What it does | |---------|------|--------------| | `content-types list` | `--limit ` | Maximum number of content types to return (default 50) | | `content-types list` | `--offset ` | Number of items to skip (for pagination) | | `content-types create` | `--data ` | JSON with `name` and `config`: `template` (markdown), `cta_text`, `cta_destination`, `writing_rules`. | | `content-types update` | `--data ` | JSON with `name` and `config`, both required. | | `content-types patch` | `--data ` | JSON with the fields to change. | ### senso product-lines | Command | What it does | |---------|--------------| | `senso product-lines list` | List the organization's product lines. | | `senso product-lines create` | Create a product line, with any details you want to attach. | | `senso product-lines get ` | Show a product line. | | `senso product-lines update ` | Replace a product line's name and details. | | `senso product-lines patch ` | Change some of a product line's fields and keep the rest. | | `senso product-lines delete ` | Delete a product line. | | Command | Flag | What it does | |---------|------|--------------| | `product-lines list` | `--limit ` | Maximum items to return (default 50) | | `product-lines list` | `--offset ` | Number of items to skip (for pagination) | | `product-lines create` | `--data ` | JSON with `name` and `details` (any object). | | `product-lines update` | `--data ` | JSON with `name` and `details`, both required. | | `product-lines patch` | `--data ` | JSON with the fields to change. | ## Prompts and tracking The questions Senso asks AI models, and who and what to watch in the answers. ### senso prompts | Command | What it does | |---------|--------------| | `senso prompts list` | List the organization's prompts: the questions Senso asks AI models on your behalf. | | `senso prompts create` | Create a prompt, with a funnel stage: awareness, consideration, evaluation or decision. | | `senso prompts get ` | Show a prompt and its full run history. | | `senso prompts delete ` | Delete a prompt and its run history. | | `senso prompts tags list ` | List the tags on a prompt. | | `senso prompts tags set ` | Replace all the tags on a prompt. Names that do not exist yet are created. | | `senso prompts tags add ` | Add one tag to a prompt. A new name is created. | | `senso prompts tags remove ` | Remove one tag from a prompt. | | Command | Flag | What it does | |---------|------|--------------| | `prompts list` | `--limit ` | Maximum prompts to return (max: 100) | | `prompts list` | `--offset ` | Number of prompts to skip (for pagination) | | `prompts list` | `--search ` | Filter prompts by question text | | `prompts list` | `--sort ` | Sort order: created_desc, created_asc, text_asc, text_desc, type_asc, type_desc | | `prompts create` | `--data ` | JSON with `question_text` and `type`. | | `prompts tags set` | `--names ` | Comma-separated tag names (created if missing) | | `prompts tags set` | `--ids ` | Comma-separated existing tag UUIDs | | `prompts tags add` | `--name ` | Tag name (created if missing) | | `prompts tags add` | `--id ` | Existing tag UUID | | `prompts tags remove` | `--name ` | Tag name to detach | | `prompts tags remove` | `--id ` | Existing tag UUID to detach | ### senso questions | Command | What it does | |---------|--------------| | `senso questions list` | List the organization's questions. | | `senso questions create` | Create a question, with a funnel stage and optional tags. | | `senso questions patch ` | Change a question's funnel stage or tags. | | `senso questions delete ` | Delete a question. | | Command | Flag | What it does | |---------|------|--------------| | `questions list` | `--type ` | Filter by question type: organization, network (default organization) | | `questions create` | `--data ` | JSON with `question_text`, `type` and `tag_ids`. | | `questions patch` | `--data ` | JSON with `type`, `tag_ids`, or both. | ### senso competitors | Command | What it does | |---------|--------------| | `senso competitors list` | List the competitors you track. | | `senso competitors add` | Track one competitor. | | `senso competitors batch-add` | Track up to 50 competitors at once, for example the ones `competitors suggest` returns. | | `senso competitors suggest` | Suggest competitors, from your website and recent prompt runs. | | `senso competitors update ` | Change a tracked competitor. | | `senso competitors delete ` | Stop tracking a competitor. | | Command | Flag | What it does | |---------|------|--------------| | `competitors add` | `--name ` | Competitor brand name | | `competitors add` | `--url ` | Competitor website URL | | `competitors batch-add` | `--data ` | JSON `{"items": [...]}`, each with `name`, `url`, `source`, and optionally `rationale` and `confidence`. | | `competitors update` | `--name ` | Competitor brand name | | `competitors update` | `--url ` | Competitor website URL | ### senso tracked-sources | Command | What it does | |---------|--------------| | `senso tracked-sources list` | List the rules that classify cited URLs as Owned, Tracked or External. | | `senso tracked-sources add` | Add a rule. New rules start active. A `domain` rule matches the whole registrable domain, so `blog.example.com` is stored as `example.com`. | | `senso tracked-sources update ` | Replace a rule. Pattern, match type and tier are required. | | `senso tracked-sources delete ` | Remove a rule. | | Command | Flag | What it does | |---------|------|--------------| | `tracked-sources add` | `--pattern ` | Value to match cited URLs against, interpreted per --match-type | | `tracked-sources add` | `--match-type ` | Match strategy: domain, host, path_prefix, exact_url | | `tracked-sources add` | `--tier ` | Classification tier: primary (Owned), tracked, secondary (External) | | `tracked-sources add` | `--category ` | Optional sub-category (only meaningful for the 'tracked' tier): affiliated_domain, published_content, social, press | | `tracked-sources add` | `--label