---
title: "Generate an AI persona and brand voice brief from the pages of a website (free tool)"
method: POST
path: "/api/v1/tools/persona-brief"
tags: ["v1", "Free tools"]
---

# Generate an AI persona and brand voice brief from the pages of a website (free tool)

`POST /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

Reads public pages of a website and produces an editorial persona and writing style brief:
who writes, for whom, tone on 6 axes, vocabulary, structure, do and don't lists, verified
quotes from the pages, style metrics measured on the text, and a ready-to-paste writing
prompt for any AI writing tool (`brief.writingPrompt`, plus `brief.writingPromptCompact`
of at most 1500 characters).

**Asynchronous, poll the result.** This call only starts the job and answers HTTP 202 with a
`briefId` and a `pollUrl`. Call `GET /api/v1/tools/persona-brief?briefId=...` every 3 seconds
until `status` is `done` or `failed`. A brief takes about 80 seconds plus 5 seconds per page,
rounded up to the half minute: about 2 min for 3 pages, 2 min 30 s for 10 pages.
MCP clients must call the `get_persona_brief` tool again until the status is terminal.

**Identical requests are reused for 72 hours.** The same set of URLs, in any order, with the
same `outputLanguage` and the same `brandName`, returns the brief already computed instead of
computing it again: HTTP 200 with `reused` true, `status` already `done`, the original
`briefId` and `reusedCreatedAt`, the date of the brief it reuses. Nothing is created, and your
daily quota is given back, so a reused answer costs you nothing. The pages of a reused brief
are listed in the order of the original request, not in the order you just sent. Send
`force` true to skip the lookup and compute a new brief, which answers 202 and consumes quota.

**Addresses.** Duplicate URLs, compared once normalized, are read once. A private, local or
blocked address is not refused by this call: the brief is created, and that page ends with
`status` failed and `failureReason` BLOCKED_ADDRESS (the brief fails with NO_USABLE_PAGE when
no other page could be read).

**Tiers, per UTC day.** Every tier is capped at 10 URLs per brief since 21 September 2026.
- Without an API key, free anonymous tier: 3 briefs per day per IP address, 10 URLs per brief.
- Signed-in account on the web interface, plan without API access: 10 briefs per day, 10 URLs per brief.
- API key of an account whose plan includes API access, pro tier: 50 briefs per day, 10 URLs per brief.

The API key of an account whose plan does NOT include API access does not authenticate, and the
call returns 401. Remove the `Authorization` header to use the free anonymous tier instead.

Quota headers: `X-RateLimit-Limit`, `X-RateLimit-Used`, `X-RateLimit-Remaining`,
`X-RateLimit-Reset` (Unix time of the next UTC midnight). `Retry-After` is set on 429 and 503.
A 502 or 503 answer gives the consumed quota back, and so does a 429 FREE_CAPACITY_REACHED
and a 200 reused answer; SERVICE_UNAVAILABLE consumes none.

**Request format.** Send the body as JSON with `Content-Type: application/json`: any other
content type returns 415. Without an `Authorization` header, a request sent by a web page of
another site (an `Origin` other than the SERPmantics app, or `Sec-Fetch-Site: cross-site`)
returns 403. Server-to-server calls send no `Origin` header and are not affected.

## Request body

- object
  - `urls` string[], required: Public http(s) URLs of representative pages of one website, 5 to 10 articles give the best brief. At most 10 URLs, whatever the tier, each of at most 2048 characters.
  - `outputLanguage` 'fr' | 'en' | 'es' | 'de' | 'it': Language in which the brief is written. When omitted, the service chooses it.
  - `brandName` string: Optional brand name, on a single line, used in the brief.
  - `force` boolean: True to compute a new brief even when an identical one exists, which answers 202 and consumes quota. Any value that is not a boolean returns 400 INVALID_BODY.

## Response `200`

An identical brief computed less than 72 hours ago was reused. Nothing was created, the daily
quota is given back, and the brief is already done: read it at pollUrl, no polling loop needed.

- object
  - `success` boolean
  - `data` object
    - `briefId` string: Identifier of the EXISTING brief, not a new one.
    - `status` 'done'
    - `reused` boolean
    - `reusedCreatedAt` string, date-time, nullable: ISO 8601, UTC, creation date of the brief being reused.
    - `pollUrl` string

## Other responses

- `202`: Brief accepted. Poll pollUrl until status is done or failed.
- `400`: Invalid body, codes INVALID_INPUT, TOO_MANY_URLS, INVALID_URL, INVALID_BRAND_NAME, INVALID_OUTPUT_LANGUAGE, INVALID_BODY. No quota is consumed. INVALID_BODY names the faulty field in details.field, and is what a non-boolean `force` returns. TOO_MANY_URLS counts the array as sent, duplicates included (details.max, details.received). INVALID_URL names the faulty URL in details.index and details.url, and gives details.reason, NOT_HTTP_URL or TOO_LONG (more than 2048 characters once normalized, each non-ASCII character being percent-encoded into several).
- `401`: UNAUTHORIZED, the Authorization header does not authenticate (unknown key, or key of a plan without API access). Remove the header to use the free anonymous tier.
- `403`: CROSS_SITE_FORBIDDEN, request without an Authorization header sent by a web page of another site (Origin other than the SERPmantics app, or Sec-Fetch-Site cross-site). Refused before any quota is consumed. Server-to-server calls send no Origin header and are not affected.
- `405`: METHOD_NOT_ALLOWED
- `415`: UNSUPPORTED_MEDIA_TYPE, the body is not sent with Content-Type application/json (a charset parameter is accepted). Refused before authentication and before any quota is consumed.
- `429`: RATE_LIMITED (daily limit of your tier, see details.reset_at and details.signup_url or details.pricing_url) or FREE_CAPACITY_REACHED (free anonymous capacity of the day used up, see details.reset_at and details.signup_url). Retry-After is set. FREE_CAPACITY_REACHED gives your quota back, the shared free capacity is what ran out and not your own limit, so your daily counter is not decremented. RATE_LIMITED means your own quota is already spent, and the refusal itself consumes nothing more.
- `502`: UPSTREAM_ERROR, the generation service did not answer. The quota is given back.
- `503`: SERVICE_BUSY, too many briefs in progress: retry after Retry-After seconds, the quota is given back. SERVICE_UNAVAILABLE, the daily counter itself is unavailable: the request is refused before anything is created, no quota is consumed, and Retry-After says when to try again. Reading a brief is not affected.

---

[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/885ab1bdaa96?raw)
