---
title: "Read an AI persona and brand voice brief, poll until it is done"
method: GET
path: "/api/v1/tools/persona-brief"
tags: ["v1", "Free tools"]
---

# Read an AI persona and brand voice brief, poll until it is done

`GET /api/v1/tools/persona-brief`

## Request

- Base URL: `https://app.serpmantics.com` (the document declares no server; this is the origin it was published from)
- URL: `https://app.serpmantics.com/api/v1/tools/persona-brief`
- Auth: one of:
  - none
  - HTTP bearer

Returns the brief started by `POST /api/v1/tools/persona-brief`. While `status` is `pending`,
`fetching`, `analyzing` or `synthesizing`, call again every 3 seconds: `progress` and `pages`
show what has been read so far. When `status` is `done`, `brief` holds the structured brief,
`brief.writingPrompt` the ready-to-paste prompt and `markdown` the full document. When `status`
is `failed`, `failureReason` says why (NO_USABLE_PAGE, LLM_UNAVAILABLE, TIMEOUT, INTERNAL).

A page with a `failureReason` did not contribute to the brief. ANALYSIS_FAILED is the only one
on a page that was read: it keeps its `title` and `wordCount`, stays counted in
`progress.pagesFetched` and never enters `progress.pagesAnalyzed` nor `metrics`.

No quota is consumed. The `briefId` is random: whoever holds it can read the brief, which is
what makes share links work. Briefs expire after 90 days. Reads that are not attributed to an
account are subject to three anti-abuse counters, all per IP address and per UTC day:
the total number of requests, the reads of a single brief, and the number of different
briefs read. All three are far above any legitimate polling, including the busiest paid
account: crossing any of them returns 429 READ_RATE_LIMITED with `Retry-After`. Once the
cost ceiling is reached, further requests keep being refused until it resets, whether or
not they carry a valid briefId.

## Query parameters

- `briefId` string, required

## Response `200`

The brief, in progress or finished.

- object
  - `success` boolean
  - `data` object
    - `briefId` string, required
    - `status` 'pending' | 'fetching' | 'analyzing' | 'synthesizing' | 'done' | 'failed', required
    - `failureReason` 'NO_USABLE_PAGE' | 'LLM_UNAVAILABLE' | 'TIMEOUT' | 'INTERNAL' | 'null', nullable, required: Why the brief failed, null unless status is failed.
    - `createdAt` string, date-time, required: ISO 8601, UTC.
    - `updatedAt` string, date-time, required: ISO 8601, UTC. Refreshed at every page and every step.
    - `outputLanguage` 'fr' | 'en' | 'es' | 'de' | 'it' | 'null', nullable, required: Language requested for the brief, null when the service chooses it.
    - `brandName` string, nullable, required
    - `progress` object, required
      - `pagesTotal` integer, required
      - `pagesFetched` integer, required: Pages read, including pages whose analysis then failed.
      - `pagesFailed` integer, required: Pages that could not be read. Read-stage only, so pagesFetched + pagesFailed = pagesTotal once reading is over. A page read but not analysed is NOT counted here, it carries status failed in pages[].
      - `pagesAnalyzed` integer, required: Pages actually analysed by the model, and the only ones the brief is built from. Count the failures in pages[], never as pagesFetched minus pagesAnalyzed.
    - `pages` object[], required
      - `url` string, required
      - `finalUrl` string, nullable, required: URL after redirects.
      - `status` 'pending' | 'ok' | 'failed', required
      - `failureReason` 'BLOCKED_ADDRESS' | 'INVALID_URL' | 'FETCH_FAILED' | 'BOT_PROTECTION' | 'NOT_HTML' | 'TOO_SHORT' | 'TOO_LARGE' | 'TOO_COMPLEX' | 'TIMEOUT' | 'HTTP_ERROR' | 'ANALYSIS_FAILED' | 'null', nullable, required: Why the page did not contribute. TOO_COMPLEX, the page markup exceeds the element cap and was refused whole, never analysed in part. ANALYSIS_FAILED, the page was read but its analysis failed.
      - `httpStatus` integer, nullable, required
      - `title` string, nullable, required
      - `wordCount` integer, nullable, required
      - `language` string, nullable, required
    - `warnings` string[], required: Empty until the brief is done.
    - `metrics` object, nullable, required: Style metrics computed in code on the analyzed pages, never by the model. Null until computed; a failed brief may carry them when its pages were measured before the failure. A metric that cannot be computed is null, never 0.
      - `pagesAnalyzed` integer
      - `wordsAnalyzed` integer
      - `dominantLanguage` string, nullable
      - `sentenceLength` object: Words per sentence, mean, median and p90.
      - `longSentenceRatio` number
      - `paragraphLengthMean` number
      - `questionsPer1000Words` number
      - `exclamationsPer1000Words` number
      - `pronouns` object, nullable: Per 1000 words, French and English only.
      - `readability` object, nullable
      - `headingsPer1000Words` number
      - `listItemsPer1000Words` number
      - `boldPer1000Words` number
      - `punctuationPer1000Words` object
      - `emojiPer1000Words` number
      - `topExpressions` object[]
        - `expression` string
        - `count` integer
    - `brief` object, nullable, required: The structured brief, set when status is done.
      - `schemaVersion` integer
      - `language` 'fr' | 'en' | 'es' | 'de' | 'it'
      - `sourceLanguage` string, nullable
      - `summary` string
      - `consistency` object
      - `authorPersona` object
      - `readerPersona` object
      - `tone` object: Six axes scored 1 to 5, with evidence.
      - `writingStyle` object
      - `vocabulary` object
      - `structure` object
      - `doList` string[]
      - `dontList` string[]
      - `examples` object[]: Quotes verified in the pages.
        - `quote` string
        - `sourceUrl` string
        - `illustrates` string
      - `writingPrompt` string: Complete system prompt, ready to paste into any AI writing tool.
      - `writingPromptCompact` string
      - `sampleText` object, nullable
        - `topic` string
        - `text` string
      - `reviewChecklist` string[]: Review questions, possibly empty.
    - `markdown` string, nullable, required: The full brief as a Markdown document, set when status is done.
    - `quality` object, nullable, required
      - `examplesProposed` integer
      - `examplesVerified` integer
      - `expressionsProposed` integer
      - `expressionsVerified` integer
    - `models` object, nullable, required: Models actually used for the page analysis and the synthesis.
      - `map` string, nullable
      - `synthesis` string, nullable

## Other responses

- `400`: INVALID_BRIEF_ID, briefId is missing or malformed.
- `404`: NOT_FOUND, no brief matches this briefId.
- `429`: READ_RATE_LIMITED, anti-abuse ceiling on reads that are not attributed to an account. Three counters, all per IP address and per UTC day: the total number of requests (the cost ceiling), the reads of a single brief (hammering one resource), and the number of different briefs read (scanning for identifiers). All three are far above any legitimate polling, and none of them is a usage quota: reading a brief consumes no quota at all. The cost ceiling counts every request, refused ones included, because a refused request still costs server work; the two others only count requests they let through. `details.limit`, `Retry-After` and `details.reset_at` describe the counter that was crossed. Reads authenticated with an API key are not subject to them.
- `502`: UPSTREAM_ERROR, the generation service did not answer.

---

[API](https://skmtc.dev/serpmantics/apis/api-serpmantics.md) · [All operations](https://skmtc.dev/serpmantics/apis/api-serpmantics/llms.txt) · [OpenAPI document](https://skmtc.dev/serpmantics/apis/api-serpmantics/revisions/e71b28ef35a8?raw)
