> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usenotra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Content Gaps and Writer

> Find the questions AI engines answer without you, plan a brief, approve it, and let the writer draft the article. Plus the GEO settings page.

The **Improve** group of the GEO sidebar turns scan results into work. **Content Gaps** lists the questions where engines recommend someone else, **Write** plans and drafts articles that answer them, and **Settings** controls what a scan covers.

## Content gaps

The **Content Gaps** page (`/geo/gaps`) is subtitled "Questions engines answer without mentioning you". It has two gap types, switchable with the **Gap type** control, plus a text filter and an **All engines** filter.

### Prompt gaps

A prompt gap is a tracked prompt where competitors are mentioned and your brand is absent. Notra computes it from the latest answer per prompt and engine over the last 30 days (first turn only, up to 400 checks):

1. For each prompt, count the checks where you were mentioned and the engines where you were missing.
2. The prompt is a gap when you were missing from at least half of the engines that answered it.
3. The **Brand mentions** column lists the tracked competitors named in those answers. Only competitors you track (by name or synonym) are counted, so keep the competitor list complete.
4. The **Opportunity** score is `(1 - ownMentionRate) * competitorCount * engineCoverage`, where `engineCoverage` is the number of engines that answered. Gaps are sorted by opportunity, highest first, and shown as a five-step meter.

The table columns are **Content** (the prompt, with its title when one exists), **Opportunity**, **Missing engines**, and **Brand mentions**. Each row carries a write action whose label follows the brief's state: **Write** when no brief exists, **Review** for a draft brief, **Writing** while the writer runs, and **Open post** once the article exists.

### Search gaps

A search gap is a Google Search Console query suggestion that you have not turned into a tracked prompt yet. The table shows **Content** and **Impressions** (the sum of impressions across the queries behind the suggestion). Search gaps require the Search Console connection on the [Prompts page](/geo/prompts-and-competitors#search-console-suggestions); without it the page says **No search gaps** with "Connect Search Console on Prompts to pull queries you don't cover yet."

### Empty states

* **No scan yet**: the project has no scan results; press **Run scan**.
* **No prompt gaps**: "Engines already mention you on the questions you track."
* **Scanning engines**: gaps appear once the running scan finishes.

### Gaps from the API

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl https://api.usenotra.com/v1/projects/$PROJECT_ID/geo/gaps \
  -H "Authorization: Bearer $NOTRA_API_KEY"
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "promptGaps": [
    {
      "id": "prm_123",
      "prompt": "what tools should i use for automating changelogs",
      "title": null,
      "engines": ["openai/gpt-5.6-sol", "google/gemini-3.5-flash"],
      "competitors": ["Beamer", "Headway"],
      "ownMentionRate": 0.25,
      "engineCoverage": 4,
      "opportunity": 6,
      "brief": null
    }
  ],
  "searchGaps": [
    { "id": "sug_456", "prompt": "changelog generator for github", "title": null, "impressions": 1240, "brief": null }
  ],
  "hasScanData": true,
  "organization": { "id": "org_123", "slug": "acme", "name": "Acme", "logo": null }
}
```

`brief` is `null` or `{ briefId, status, postId, workingTitle }` when a brief has already been planned for that gap. `hasScanData` is `false` until the project has at least one scan result. This route needs the `briefs.read` scope.

## The Write flow

Writing is a two-step flow: plan a brief, then approve it. Approval hands the brief to the writer, which produces a draft article in **Content**.

<Steps>
  <Step title="Open the Write dialog">
    From a gap row press **Write**, or open the **Write** page and press **New article**. The dialog has five sections: **Prompt** (required), **Format** (required), **Brand identity**, **Sitemap**, and **Competitors**. Formats offered in the dashboard are **Guide** ("A long article that answers the prompt directly"), **Listicle** ("A numbered list that buyers can scan and cite"), and **Comparison** ("Compares the brand with its alternatives"). The sitemap section lets the planner pick internal links from the pages of your site; the competitors section chooses which tracked competitors the article should position against.
  </Step>

  <Step title="Plan the brief">
    Notra researches the topic against your brand context, the current gap prompts, the chosen competitors, and the sitemap pages, and writes a brief with a working title, audience, intent, sections with claims, questions to answer, internal links, and an acceptance checklist. Planning books AI credits. The brief opens for review in **Draft** state.
  </Step>

  <Step title="Approve">
    Approving claims the brief and starts the writer. The Write page lists every brief with its state: **Draft**, **Queued**, **Writing**, **Done**, or **Failed**. While it runs the page shows **Writing the article**.
  </Step>

  <Step title="Open the draft in Content">
    When the run completes, the brief points at a post and opens in **Content** as a blog post draft, where you edit and publish it like any other post. A failed brief can be approved again.
  </Step>
</Steps>

### Billing

Planning a brief reserves AI credits and does not count toward a plan quota. The approved writer run is billed as a long-form post: it draws from the plan's long-form post quota first and falls back to AI credits when the quota is used up. Both steps fail with `402` when neither is available. See [Billing](/organization/billing).

### Plan a brief from the API

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.usenotra.com/v1/projects/$PROJECT_ID/geo/briefs \
  -H "Authorization: Bearer $NOTRA_API_KEY" \
  -H "Content-Type: application/json" \
  --max-time 330 \
  -d '{
    "topic": "what tools should i use for automating changelogs",
    "contentSubtype": "guide",
    "sourceKind": "gap",
    "sourceId": "prm_123",
    "competitorIds": ["cmp_1", "cmp_2"],
    "autoApprove": false
  }'
```

<ParamField body="topic" type="string" required>
  3 to 200 characters. Replaced by the source prompt when `sourceKind` is `gap`, `prompt`, or `search_console`.
</ParamField>

<ParamField body="autoApprove" type="boolean" default="false">
  Start the writer in the same call instead of leaving the brief in draft.
</ParamField>

<ParamField body="contentSubtype" type="string">
  One of `guide`, `comparison`, `listicle`, `how-to`, `faq`, `alternatives`.
</ParamField>

<ParamField body="brandVoiceIds" type="string[]">
  Up to 8 ids; only the first is used and it overrides the project's brand identity.
</ParamField>

<ParamField body="competitorIds" type="string[]">
  Up to 25 tracked competitor ids to position against.
</ParamField>

<ParamField body="sitemapId" type="string">
  Sitemap whose pages the planner may link to.
</ParamField>

<ParamField body="sourceKind" type="string">
  One of `manual`, `gap`, `prompt`, `search_console`.
</ParamField>

<ParamField body="sourceId" type="string">
  Gap, prompt, or search-console suggestion id. An open brief for the same source is reused instead of planning a new one.
</ParamField>

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "briefId": "brf_01J...",
  "brief": {
    "targetPrompt": "what tools should i use for automating changelogs",
    "intent": "Buyer comparing changelog automation tools",
    "contentSubtype": "guide",
    "workingTitle": "How to automate changelogs from GitHub activity",
    "audience": "Engineering leads at B2B SaaS companies",
    "jobToBeDone": "Ship release notes without writing them by hand",
    "sections": [{ "heading": "Why changelogs fall behind", "goal": "Name the pain", "claims": ["..."] }],
    "questionsToAnswer": ["..."],
    "internalLinks": [{ "url": "https://acme.com/docs", "anchor": "docs", "why": "..." }],
    "acceptanceChecklist": ["..."]
  },
  "status": "draft",
  "runId": null,
  "postId": null,
  "organization": { "id": "org_123", "slug": "acme", "name": "Acme", "logo": null }
}
```

<Warning>
  Planning is synchronous. If it exceeds four minutes the API returns `409` while work may still finish in Notra. Do not retry; list the project's briefs to find the result. This endpoint allows 10 requests per 10 minutes per organization.
</Warning>

### Approve and follow the run

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.usenotra.com/v1/projects/$PROJECT_ID/geo/briefs/$BRIEF_ID/approve \
  -H "Authorization: Bearer $NOTRA_API_KEY"
```

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "runId": "run_01J...",
  "organization": { "id": "org_123", "slug": "acme", "name": "Acme", "logo": null }
}
```

The response is `202 Accepted`. Only briefs in `draft` or `failed` can be approved; anything else returns `409`. The limit is 20 requests per 10 minutes per organization.

Poll `GET /v1/projects/{projectId}/geo/briefs/{briefId}` until `status` is `completed` or `failed`. The brief object carries `status`, `autoApproved`, `runId`, `postId`, `humanized`, `error`, `createdAt`, `updatedAt`, and `completedAt`. `GET /v1/projects/{projectId}/geo/briefs` lists every brief with `id`, `topic`, `workingTitle`, `status`, `postId`, and `createdAt`. Once `postId` is set, read the article through the [Posts API](/api-reference/content/list-posts).

## GEO settings

The **Settings** page (`/geo/settings`) is subtitled "How your brand is identified and where prompts are scanned." It autosaves; the header shows **Saving...** and **Saved**, or "Add a company name to save" when the name is empty.

| Section              | Fields                                                                                                                                                                                                                                                                     |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Brand                | **Company name** ("The primary name we match in answers") and **Aliases** (up to 10: "Other spellings, product names, or the bare domain")                                                                                                                                 |
| **Tracked domains**  | The brand website is always recorded (read-only here; change it in Brand → Identity). Extra sites besides that website, up to 20. Same ingest token; subdomains of a listed domain are included.                                                                           |
| **Conversion paths** | Paths that count as a conversion when an AI referral reaches them, for example `/signup`. Prefix match.                                                                                                                                                                    |
| **Scan schedule**    | **Automatic scans** switch and **Frequency**: Daily (default), 48 hours, 3 days, Weekly, 2 weeks, 30 days. "Manual scans always work."                                                                                                                                     |
| **Languages**        | "Languages your prompts are scanned in. English is on by default." Up to 4 languages                                                                                                                                                                                       |
| **Models**           | "Each enabled provider runs on every prompt." Pick models per provider from the catalog, up to 64. A **Zero data retention** toggle limits scans to models with ZDR; it needs the ZDR add-on and is forced off without it. Models without ZDR can be individually approved |

Competitors are managed on the Competitors page, and the Google Search Console link lives on the Prompts page, not here.

### Settings from the API

`PATCH` writes the full settings document and re-arms the recurring scan.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH https://api.usenotra.com/v1/projects/$PROJECT_ID/geo/settings \
  -H "Authorization: Bearer $NOTRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "companyName": "Notra",
    "aliases": ["usenotra", "usenotra.com"],
    "languages": ["en", "de"],
    "engines": ["anthropic/claude-sonnet-5", "openai/gpt-5.6-sol", "google/gemini-3.5-flash"],
    "enforceZdr": false,
    "nonZdrApprovedEngines": [],
    "enabled": true,
    "scanIntervalHours": 24
  }'
```

<ParamField body="companyName" type="string" required>
  1 to 128 characters.
</ParamField>

<ParamField body="aliases" type="string[]" required>
  Up to 10 entries.
</ParamField>

<ParamField body="conversionPaths" type="string[]">
  Paths that count as a conversion when an AI referral reaches them. Prefix match. Omit to keep the stored list.
</ParamField>

<ParamField body="domains" type="string[]">
  Additional hostnames that send AI traffic to this project, besides the brand website. Up to 20. Subdomains of a listed domain are included. Omit to keep the stored list.
</ParamField>

<ParamField body="languages" type="string[]" required>
  1 to 4 supported language codes. An unknown code is rejected with `400`.
</ParamField>

<ParamField body="engines" type="string[]" required>
  1 to 64 model ids from the catalog this organization can see (the ids `GET /geo/settings` returns). An unknown id is rejected with `400`. Engines not visible to the caller keep their stored selection.
</ParamField>

<ParamField body="enforceZdr" type="boolean" required>
  Forced off without the ZDR add-on.
</ParamField>

<ParamField body="nonZdrApprovedEngines" type="string[]" required>
  Up to 64 model ids allowed to run despite lacking ZDR.
</ParamField>

<ParamField body="enabled" type="boolean" required>
  Whether recurring scans run.
</ParamField>

<ParamField body="scanIntervalHours" type="integer" required>
  One of 24, 48, 72, 168, 336, 720.
</ParamField>

The response echoes the stored `settings` together with `scanStartedAt`, `lastScanAt`, and `isScanning`, and a `competitors` array that is read-only here. Both routes need the `geo-settings` scope.

<CardGroup cols={2}>
  <Card title="Plan a content brief" icon="code" href="/api-reference/geo/plan-a-content-brief">
    Full request and response schema.
  </Card>

  <Card title="Blog posts" icon="file-text" href="/content/blog-posts">
    What happens to the draft once it lands in Content.
  </Card>
</CardGroup>
